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