1
+ from collections import namedtuple
1
2
import csv
2
3
import re
3
4
import textwrap
@@ -225,45 +226,157 @@ def _normalize_table_file_props(header, sep):
225
226
def resolve_columns (specs ):
226
227
if isinstance (specs , str ):
227
228
specs = specs .replace (',' , ' ' ).strip ().split ()
228
- return _resolve_colspecs (specs )
229
+ resolved = []
230
+ for raw in specs :
231
+ column = ColumnSpec .from_raw (raw )
232
+ resolved .append (column )
233
+ return resolved
229
234
230
235
231
236
def build_table (specs , * , sep = ' ' , defaultwidth = None ):
232
237
columns = resolve_columns (specs )
233
238
return _build_table (columns , sep = sep , defaultwidth = defaultwidth )
234
239
235
240
236
- _COLSPEC_RE = re .compile (textwrap .dedent (r'''
237
- ^
238
- (?:
239
- \[
240
- (
241
- (?: [^\s\]] [^\]]* )?
242
- [^\s\]]
243
- ) # <label>
244
- ]
245
- )?
246
- ( \w+ ) # <field>
247
- (?:
241
+ class ColumnSpec (namedtuple ('ColumnSpec' , 'field label fmt' )):
242
+
243
+ REGEX = re .compile (textwrap .dedent (r'''
244
+ ^
248
245
(?:
249
- :
250
- ( [<^>] ) # <align>
251
- ( \d+ ) # <width1>
252
- )
253
- |
246
+ \[
247
+ (
248
+ (?: [^\s\]] [^\]]* )?
249
+ [^\s\]]
250
+ ) # <label>
251
+ ]
252
+ )?
253
+ ( [-\w]+ ) # <field>
254
254
(?:
255
255
(?:
256
256
:
257
- ( \d+ ) # <width2>
258
- )?
257
+ ( [<^>] ) # <align>
258
+ ( \d+ )? # <width1>
259
+ )
260
+ |
259
261
(?:
260
- :
261
- ( .*? ) # <fmt>
262
- )?
263
- )
264
- )?
265
- $
266
- ''' ), re .VERBOSE )
262
+ (?:
263
+ :
264
+ ( \d+ ) # <width2>
265
+ )?
266
+ (?:
267
+ :
268
+ ( .*? ) # <fmt>
269
+ )?
270
+ )
271
+ )?
272
+ $
273
+ ''' ), re .VERBOSE )
274
+
275
+ @classmethod
276
+ def from_raw (cls , raw ):
277
+ if not raw :
278
+ raise ValueError ('missing column spec' )
279
+ elif isinstance (raw , cls ):
280
+ return raw
281
+
282
+ if isinstance (raw , str ):
283
+ * values , _ = cls ._parse (raw )
284
+ else :
285
+ * values , _ = cls ._normalize (raw )
286
+ if values is None :
287
+ raise ValueError (f'unsupported column spec { raw !r} ' )
288
+ return cls (* values )
289
+
290
+ @classmethod
291
+ def parse (cls , specstr ):
292
+ parsed = cls ._parse (specstr )
293
+ if not parsed :
294
+ return None
295
+ * values , _ = parsed
296
+ return cls (* values )
297
+
298
+ @classmethod
299
+ def _parse (cls , specstr ):
300
+ m = cls .REGEX .match (specstr )
301
+ if not m :
302
+ return None
303
+ (label , field ,
304
+ align , width1 ,
305
+ width2 , fmt ,
306
+ ) = m .groups ()
307
+ if not label :
308
+ label = field
309
+ if fmt :
310
+ assert not align and not width1 , (specstr ,)
311
+ _parsed = _parse_fmt (fmt )
312
+ if not _parsed :
313
+ raise NotImplementedError
314
+ elif width2 :
315
+ width , _ = _parsed
316
+ if width != int (width2 ):
317
+ raise NotImplementedError (specstr )
318
+ elif width2 :
319
+ fmt = width2
320
+ width = int (width2 )
321
+ else :
322
+ assert not fmt , (fmt , specstr )
323
+ if align :
324
+ width = int (width1 ) if width1 else len (label )
325
+ fmt = f'{ align } { width } '
326
+ else :
327
+ width = None
328
+ return field , label , fmt , width
329
+
330
+ @classmethod
331
+ def _normalize (cls , spec ):
332
+ if len (spec ) == 1 :
333
+ raw , = spec
334
+ raise NotImplementedError
335
+ return _resolve_column (raw )
336
+
337
+ if len (spec ) == 4 :
338
+ label , field , width , fmt = spec
339
+ if width :
340
+ if not fmt :
341
+ fmt = str (width )
342
+ elif _parse_fmt (fmt )[0 ] != width :
343
+ raise ValueError (f'width mismatch in { spec } ' )
344
+ elif len (raw ) == 3 :
345
+ label , field , fmt = spec
346
+ if not field :
347
+ label , field = None , label
348
+ elif not isinstance (field , str ) or not field .isidentifier ():
349
+ # XXX This doesn't seem right...
350
+ fmt = f'{ field } :{ fmt } ' if fmt else field
351
+ label , field = None , label
352
+ elif len (raw ) == 2 :
353
+ label = None
354
+ field , fmt = raw
355
+ if not field :
356
+ field , fmt = fmt , None
357
+ elif not field .isidentifier () or fmt .isidentifier ():
358
+ label , field = field , fmt
359
+ else :
360
+ raise NotImplementedError
361
+
362
+ fmt = f':{ fmt } ' if fmt else ''
363
+ if label :
364
+ return cls ._parse (f'[{ label } ]{ field } { fmt } ' )
365
+ else :
366
+ return cls ._parse (f'{ field } { fmt } ' )
367
+
368
+ @property
369
+ def width (self ):
370
+ if not self .fmt :
371
+ return None
372
+ parsed = _parse_fmt (self .fmt )
373
+ if not parsed :
374
+ return None
375
+ width , _ = parsed
376
+ return width
377
+
378
+ def resolve_width (self , default = None ):
379
+ return _resolve_width (self .width , self .fmt , self .label , default )
267
380
268
381
269
382
def _parse_fmt (fmt ):
@@ -272,117 +385,45 @@ def _parse_fmt(fmt):
272
385
width = fmt [1 :]
273
386
if width .isdigit ():
274
387
return int (width ), align
275
- return None , None
388
+ elif fmt .isdigit ():
389
+ return int (fmt ), '<'
390
+ return None
276
391
277
392
278
- def _parse_colspec (raw ):
279
- m = _COLSPEC_RE .match (raw )
280
- if not m :
281
- return None
282
- label , field , align , width1 , width2 , fmt = m .groups ()
283
- if not label :
284
- label = field
285
- if width1 :
286
- width = None
287
- fmt = f'{ align } { width1 } '
288
- elif width2 :
289
- width = int (width2 )
290
- if fmt :
291
- _width , _ = _parse_fmt (fmt )
292
- if _width == width :
293
- width = None
294
- else :
295
- width = None
296
- return field , label , width , fmt
297
-
298
-
299
- def _normalize_colspec (spec ):
300
- if len (spec ) == 1 :
301
- raw , = spec
302
- return _resolve_column (raw )
303
-
304
- if len (spec ) == 4 :
305
- label , field , width , fmt = spec
306
- if width :
307
- fmt = f'{ width } :{ fmt } ' if fmt else width
308
- elif len (raw ) == 3 :
309
- label , field , fmt = spec
310
- if not field :
311
- label , field = None , label
312
- elif not isinstance (field , str ) or not field .isidentifier ():
313
- fmt = f'{ field } :{ fmt } ' if fmt else field
314
- label , field = None , label
315
- elif len (raw ) == 2 :
316
- label = None
317
- field , fmt = raw
318
- if not field :
319
- field , fmt = fmt , None
320
- elif not field .isidentifier () or fmt .isidentifier ():
321
- label , field = field , fmt
322
- else :
323
- raise NotImplementedError
324
-
325
- fmt = f':{ fmt } ' if fmt else ''
326
- if label :
327
- return _parse_colspec (f'[{ label } ]{ field } { fmt } ' )
328
- else :
329
- return _parse_colspec (f'{ field } { fmt } ' )
330
-
331
-
332
- def _resolve_colspec (raw ):
333
- if isinstance (raw , str ):
334
- spec = _parse_colspec (raw )
335
- else :
336
- spec = _normalize_colspec (raw )
337
- if spec is None :
338
- raise ValueError (f'unsupported column spec { raw !r} ' )
339
- return spec
340
-
341
-
342
- def _resolve_colspecs (columns ):
343
- parsed = []
344
- for raw in columns :
345
- column = _resolve_colspec (raw )
346
- parsed .append (column )
347
- return parsed
348
-
349
-
350
- def _resolve_width (spec , defaultwidth ):
351
- _ , label , width , fmt = spec
393
+ def _resolve_width (width , fmt , label , default ):
352
394
if width :
353
395
if not isinstance (width , int ):
354
396
raise NotImplementedError
355
397
return width
356
- elif width and fmt :
357
- width , _ = _parse_fmt (fmt )
358
- if width :
359
- return width
360
-
361
- if not defaultwidth :
398
+ elif fmt :
399
+ parsed = _parse_fmt (fmt )
400
+ if parsed :
401
+ width , _ = parsed
402
+ if width :
403
+ return width
404
+
405
+ if not default :
362
406
return WIDTH
363
- elif not hasattr (defaultwidth , 'get' ):
364
- return defaultwidth or WIDTH
365
-
366
- defaultwidths = defaultwidth
367
- defaultwidth = defaultwidths . get ( None ) or WIDTH
368
- return defaultwidths . get ( label ) or defaultwidth
407
+ elif hasattr (default , 'get' ):
408
+ defaults = default
409
+ default = defaults . get ( None ) or WIDTH
410
+ return defaults . get ( label ) or default
411
+ else :
412
+ return default or WIDTH
369
413
370
414
371
415
def _build_table (columns , * , sep = ' ' , defaultwidth = None ):
372
416
header = []
373
417
div = []
374
418
rowfmt = []
375
419
for spec in columns :
376
- label , field , _ , colfmt = spec
377
- width = _resolve_width (spec , defaultwidth )
378
- if colfmt :
379
- colfmt = f':{ colfmt } '
380
- else :
381
- colfmt = f':{ width } '
420
+ width = spec .resolve_width (defaultwidth )
421
+ colfmt = spec .fmt
422
+ colfmt = f':{ spec .fmt } ' if spec .fmt else f':{ width } '
382
423
383
- header .append (f' {{:^{ width } }} ' .format (label ))
424
+ header .append (f' {{:^{ width } }} ' .format (spec . label ))
384
425
div .append ('-' * (width + 2 ))
385
- rowfmt .append (f' {{{ field } { colfmt } }} ' )
426
+ rowfmt .append (f' {{{ spec . field } { colfmt } }} ' )
386
427
return (
387
428
sep .join (header ),
388
429
sep .join (div ),
0 commit comments