1+ from collections import namedtuple
12import csv
23import re
34import textwrap
@@ -225,45 +226,157 @@ def _normalize_table_file_props(header, sep):
225226def resolve_columns (specs ):
226227 if isinstance (specs , str ):
227228 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
229234
230235
231236def build_table (specs , * , sep = ' ' , defaultwidth = None ):
232237 columns = resolve_columns (specs )
233238 return _build_table (columns , sep = sep , defaultwidth = defaultwidth )
234239
235240
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+ ^
248245 (?:
249- :
250- ( [<^>] ) # <align>
251- ( \d+ ) # <width1>
252- )
253- |
246+ \[
247+ (
248+ (?: [^\s\]] [^\]]* )?
249+ [^\s\]]
250+ ) # <label>
251+ ]
252+ )?
253+ ( [-\w]+ ) # <field>
254254 (?:
255255 (?:
256256 :
257- ( \d+ ) # <width2>
258- )?
257+ ( [<^>] ) # <align>
258+ ( \d+ )? # <width1>
259+ )
260+ |
259261 (?:
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 )
267380
268381
269382def _parse_fmt (fmt ):
@@ -272,117 +385,45 @@ def _parse_fmt(fmt):
272385 width = fmt [1 :]
273386 if width .isdigit ():
274387 return int (width ), align
275- return None , None
388+ elif fmt .isdigit ():
389+ return int (fmt ), '<'
390+ return None
276391
277392
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 ):
352394 if width :
353395 if not isinstance (width , int ):
354396 raise NotImplementedError
355397 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 :
362406 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
369413
370414
371415def _build_table (columns , * , sep = ' ' , defaultwidth = None ):
372416 header = []
373417 div = []
374418 rowfmt = []
375419 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 } '
382423
383- header .append (f' {{:^{ width } }} ' .format (label ))
424+ header .append (f' {{:^{ width } }} ' .format (spec . label ))
384425 div .append ('-' * (width + 2 ))
385- rowfmt .append (f' {{{ field } { colfmt } }} ' )
426+ rowfmt .append (f' {{{ spec . field } { colfmt } }} ' )
386427 return (
387428 sep .join (header ),
388429 sep .join (div ),
0 commit comments