@@ -367,8 +367,14 @@ def escape_empty(val):
367
367
368
368
369
369
tabulate_formats = list (sorted (_table_formats .keys ()))
370
+ multiline_formats = {
371
+ "fancy_grid" : "fancy_grid" ,
372
+ "grid" : "grid" ,
373
+ "simple" : "simple_multiline" }
370
374
371
375
376
+ _multiline_codes = re .compile (r"\r|\n|\r\n" )
377
+ _multiline_codes_bytes = re .compile (b"\r |\n |\r \n " )
372
378
_invisible_codes = re .compile (r"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m" ) # ANSI color codes
373
379
_invisible_codes_bytes = re .compile (b"\x1b \[\d+[;\d]*m|\x1b \[\d*\;\d*\;\d*m" ) # ANSI color codes
374
380
@@ -533,6 +539,10 @@ def _padboth(width, s):
533
539
return fmt .format (s )
534
540
535
541
542
+ def _padnone (ignore_width , s ):
543
+ return s
544
+
545
+
536
546
def _strip_invisible (s ):
537
547
"Remove invisible ANSI color codes."
538
548
if isinstance (s , _text_type ):
@@ -559,16 +569,34 @@ def _visible_width(s):
559
569
return len_fn (_text_type (s ))
560
570
561
571
562
- def _align_column (strings , alignment , minwidth = 0 , has_invisible = True ):
563
- """[string] -> [padded_string]
572
+ def _is_multiline (s ):
573
+ if isinstance (s , _text_type ):
574
+ return bool (re .search (_multiline_codes , s ))
575
+ else : # a bytestring
576
+ return bool (re .search (_multiline_codes_bytes , s ))
577
+
564
578
565
- >>> list(map(str,_align_column(["12.345", "-1234.5", "1.23", "1234.5", "1e+234", "1.0e234"], "decimal")))
566
- [' 12.345 ', '-1234.5 ', ' 1.23 ', ' 1234.5 ', ' 1e+234 ', ' 1.0e234']
579
+ def _multiline_width (multiline_s , line_width_fn = len ):
580
+ """Visible width of a potentially multiline content."""
581
+ return max (map (line_width_fn , re .split ("[\r \n ]" , multiline_s )))
567
582
568
- >>> list(map(str,_align_column(['123.4', '56.7890'], None)))
569
- ['123.4', '56.7890']
570
583
571
- """
584
+ def _choose_width_fn (has_invisible , enable_widechars , is_multiline ):
585
+ """Return a function to calculate visible cell width."""
586
+ if has_invisible :
587
+ line_width_fn = _visible_width
588
+ elif enable_widechars : # optional wide-character support if available
589
+ line_width_fn = wcwidth .wcswidth
590
+ else :
591
+ line_width_fn = len
592
+ if is_multiline :
593
+ width_fn = lambda s : _multiline_width (s , line_width_fn )
594
+ else :
595
+ width_fn = line_width_fn
596
+ return width_fn
597
+
598
+
599
+ def _align_column_choose_padfn (strings , alignment , has_invisible ):
572
600
if alignment == "right" :
573
601
if not PRESERVE_WHITESPACE :
574
602
strings = [s .strip () for s in strings ]
@@ -587,31 +615,46 @@ def _align_column(strings, alignment, minwidth=0, has_invisible=True):
587
615
for s , decs in zip (strings , decimals )]
588
616
padfn = _padleft
589
617
elif not alignment :
590
- return strings
618
+ padfn = _padnone
591
619
else :
592
620
if not PRESERVE_WHITESPACE :
593
621
strings = [s .strip () for s in strings ]
594
622
padfn = _padright
623
+ return strings , padfn
595
624
596
- enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
597
- if has_invisible :
598
- width_fn = _visible_width
599
- elif enable_widechars : # optional wide-character support if available
600
- width_fn = wcwidth .wcswidth
601
- else :
602
- width_fn = len
603
625
604
- s_lens = list (map (len , strings ))
626
+ def _align_column (strings , alignment , minwidth = 0 ,
627
+ has_invisible = True , enable_widechars = False , is_multiline = False ):
628
+ """[string] -> [padded_string]"""
629
+ strings , padfn = _align_column_choose_padfn (strings , alignment , has_invisible )
630
+ width_fn = _choose_width_fn (has_invisible , enable_widechars , is_multiline )
631
+
605
632
s_widths = list (map (width_fn , strings ))
606
633
maxwidth = max (max (s_widths ), minwidth )
607
- if not enable_widechars and not has_invisible :
608
- padded_strings = [padfn (maxwidth , s ) for s in strings ]
609
- else :
610
- # enable wide-character width corrections
611
- visible_widths = [maxwidth - (w - l ) for w , l in zip (s_widths , s_lens )]
612
- # wcswidth and _visible_width don't count invisible characters;
613
- # padfn doesn't need to apply another correction
614
- padded_strings = [padfn (w , s ) for s , w in zip (strings , visible_widths )]
634
+ # TODO: refactor column alignment in single-line and multiline modes
635
+ if is_multiline :
636
+ if not enable_widechars and not has_invisible :
637
+ padded_strings = [
638
+ "\n " .join ([padfn (maxwidth , s ) for s in ms .splitlines ()])
639
+ for ms in strings ]
640
+ else :
641
+ # enable wide-character width corrections
642
+ s_lens = [max ((len (s ) for s in re .split ("[\r \n ]" , ms ))) for ms in strings ]
643
+ visible_widths = [maxwidth - (w - l ) for w , l in zip (s_widths , s_lens )]
644
+ # wcswidth and _visible_width don't count invisible characters;
645
+ # padfn doesn't need to apply another correction
646
+ padded_strings = ["\n " .join ([padfn (w , s ) for s in (ms .splitlines () or ms )])
647
+ for ms , w in zip (strings , visible_widths )]
648
+ else : # single-line cell values
649
+ if not enable_widechars and not has_invisible :
650
+ padded_strings = [padfn (maxwidth , s ) for s in strings ]
651
+ else :
652
+ # enable wide-character width corrections
653
+ s_lens = list (map (len , strings ))
654
+ visible_widths = [maxwidth - (w - l ) for w , l in zip (s_widths , s_lens )]
655
+ # wcswidth and _visible_width don't count invisible characters;
656
+ # padfn doesn't need to apply another correction
657
+ padded_strings = [padfn (w , s ) for s , w in zip (strings , visible_widths )]
615
658
return padded_strings
616
659
617
660
@@ -682,9 +725,15 @@ def _format(val, valtype, floatfmt, missingval="", has_invisible=True):
682
725
return "{0}" .format (val )
683
726
684
727
685
- def _align_header (header , alignment , width , visible_width ):
728
+ def _align_header (header , alignment , width , visible_width , is_multiline = False ):
686
729
"Pad string header to width chars given known visible_width of the header."
687
- width += len (header ) - visible_width
730
+ if is_multiline :
731
+ header_lines = re .split (_multiline_codes , header )
732
+ padded_lines = [_align_header (h , alignment , width , visible_width ) for h in header_lines ]
733
+ return "\n " .join (padded_lines )
734
+ # else: not multiline
735
+ ninvisible = max (0 , len (header ) - visible_width )
736
+ width += ninvisible
688
737
if alignment == "left" :
689
738
return _padright (width , header )
690
739
elif alignment == "center" :
@@ -1173,17 +1222,17 @@ def tabulate(tabular_data, headers=(), tablefmt="simple",
1173
1222
1174
1223
# optimization: look for ANSI control codes once,
1175
1224
# enable smart width functions only if a control code is found
1176
- plain_text = '\n ' .join (['\t ' .join (map (_text_type , headers ))] + \
1225
+ plain_text = '\t ' .join (['\t ' .join (map (_text_type , headers ))] + \
1177
1226
['\t ' .join (map (_text_type , row )) for row in list_of_lists ])
1178
1227
1179
1228
has_invisible = re .search (_invisible_codes , plain_text )
1180
1229
enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
1181
- if has_invisible :
1182
- width_fn = _visible_width
1183
- elif enable_widechars : # optional wide-character support if available
1184
- width_fn = wcwidth .wcswidth
1230
+ if tablefmt in multiline_formats and _is_multiline (plain_text ):
1231
+ tablefmt = multiline_formats .get (tablefmt , tablefmt )
1232
+ is_multiline = True
1185
1233
else :
1186
- width_fn = len
1234
+ is_multiline = False
1235
+ width_fn = _choose_width_fn (has_invisible , enable_widechars , is_multiline )
1187
1236
1188
1237
# format rows and columns, convert numeric values to strings
1189
1238
cols = list (izip_longest (* list_of_lists ))
@@ -1208,15 +1257,15 @@ def tabulate(tabular_data, headers=(), tablefmt="simple",
1208
1257
# align columns
1209
1258
aligns = [numalign if ct in [int ,float ] else stralign for ct in coltypes ]
1210
1259
minwidths = [width_fn (h ) + MIN_PADDING for h in headers ] if headers else [0 ]* len (cols )
1211
- cols = [_align_column (c , a , minw , has_invisible )
1260
+ cols = [_align_column (c , a , minw , has_invisible , enable_widechars , is_multiline )
1212
1261
for c , a , minw in zip (cols , aligns , minwidths )]
1213
1262
1214
1263
if headers :
1215
1264
# align headers and add headers
1216
1265
t_cols = cols or [['' ]] * len (headers )
1217
1266
t_aligns = aligns or [stralign ] * len (headers )
1218
1267
minwidths = [max (minw , width_fn (c [0 ])) for minw , c in zip (minwidths , t_cols )]
1219
- headers = [_align_header (h , a , minw , width_fn (h ))
1268
+ headers = [_align_header (h , a , minw , width_fn (h ), is_multiline )
1220
1269
for h , a , minw in zip (headers , t_aligns , minwidths )]
1221
1270
rows = list (zip (* cols ))
1222
1271
else :
@@ -1226,7 +1275,7 @@ def tabulate(tabular_data, headers=(), tablefmt="simple",
1226
1275
if not isinstance (tablefmt , TableFormat ):
1227
1276
tablefmt = _table_formats .get (tablefmt , _table_formats ["simple" ])
1228
1277
1229
- return _format_table (tablefmt , headers , rows , minwidths , aligns )
1278
+ return _format_table (tablefmt , headers , rows , minwidths , aligns , is_multiline )
1230
1279
1231
1280
1232
1281
def _expand_numparse (disable_numparse , column_count ):
@@ -1246,6 +1295,15 @@ def _expand_numparse(disable_numparse, column_count):
1246
1295
return [not disable_numparse ] * column_count
1247
1296
1248
1297
1298
+ def _pad_row (cells , padding ):
1299
+ if cells :
1300
+ pad = " " * padding
1301
+ padded_cells = [pad + cell + pad for cell in cells ]
1302
+ return padded_cells
1303
+ else :
1304
+ return cells
1305
+
1306
+
1249
1307
def _build_simple_row (padded_cells , rowfmt ):
1250
1308
"Format row according to DataRow format without padding."
1251
1309
begin , sep , end = rowfmt
@@ -1262,6 +1320,24 @@ def _build_row(padded_cells, colwidths, colaligns, rowfmt):
1262
1320
return _build_simple_row (padded_cells , rowfmt )
1263
1321
1264
1322
1323
+ def _append_basic_row (lines , padded_cells , colwidths , colaligns , rowfmt ):
1324
+ lines .append (_build_row (padded_cells , colwidths , colaligns , rowfmt ))
1325
+ return lines
1326
+
1327
+
1328
+ def _append_multiline_row (lines , padded_multiline_cells , padded_widths , colaligns , rowfmt , pad ):
1329
+ colwidths = [w - 2 * pad for w in padded_widths ]
1330
+ cells_lines = [c .splitlines () for c in padded_multiline_cells ]
1331
+ nlines = max (map (len , cells_lines )) # number of lines in the row
1332
+ # vertically pad cells where some lines are missing
1333
+ cells_lines = [(cl + [' ' * w ]* (nlines - len (cl ))) for cl , w in zip (cells_lines , colwidths )]
1334
+ lines_cells = [[cl [i ] for cl in cells_lines ] for i in range (nlines )]
1335
+ for ln in lines_cells :
1336
+ padded_ln = _pad_row (ln , 1 )
1337
+ _append_basic_row (lines , padded_ln , colwidths , colaligns , rowfmt )
1338
+ return lines
1339
+
1340
+
1265
1341
def _build_line (colwidths , colaligns , linefmt ):
1266
1342
"Return a string which represents a horizontal line."
1267
1343
if not linefmt :
@@ -1274,47 +1350,50 @@ def _build_line(colwidths, colaligns, linefmt):
1274
1350
return _build_simple_row (cells , (begin , sep , end ))
1275
1351
1276
1352
1277
- def _pad_row (cells , padding ):
1278
- if cells :
1279
- pad = " " * padding
1280
- padded_cells = [pad + cell + pad for cell in cells ]
1281
- return padded_cells
1282
- else :
1283
- return cells
1353
+ def _append_line (lines , colwidths , colaligns , linefmt ):
1354
+ lines .append (_build_line (colwidths , colaligns , linefmt ))
1355
+ return lines
1284
1356
1285
1357
1286
- def _format_table (fmt , headers , rows , colwidths , colaligns ):
1358
+ def _format_table (fmt , headers , rows , colwidths , colaligns , is_multiline ):
1287
1359
"""Produce a plain-text representation of the table."""
1288
1360
lines = []
1289
1361
hidden = fmt .with_header_hide if (headers and fmt .with_header_hide ) else []
1290
1362
pad = fmt .padding
1291
1363
headerrow = fmt .headerrow
1292
1364
1293
1365
padded_widths = [(w + 2 * pad ) for w in colwidths ]
1294
- padded_headers = _pad_row (headers , pad )
1295
- padded_rows = [_pad_row (row , pad ) for row in rows ]
1366
+ if is_multiline :
1367
+ pad_row = lambda row , _ : row # do it later, in _append_multiline_row
1368
+ append_row = partial (_append_multiline_row , pad = pad )
1369
+ else :
1370
+ pad_row = _pad_row
1371
+ append_row = _append_basic_row
1372
+
1373
+ padded_headers = pad_row (headers , pad )
1374
+ padded_rows = [pad_row (row , pad ) for row in rows ]
1296
1375
1297
1376
if fmt .lineabove and "lineabove" not in hidden :
1298
- lines . append ( _build_line ( padded_widths , colaligns , fmt .lineabove ) )
1377
+ _append_line ( lines , padded_widths , colaligns , fmt .lineabove )
1299
1378
1300
1379
if padded_headers :
1301
- lines . append ( _build_row ( padded_headers , padded_widths , colaligns , headerrow ) )
1380
+ append_row ( lines , padded_headers , padded_widths , colaligns , headerrow )
1302
1381
if fmt .linebelowheader and "linebelowheader" not in hidden :
1303
- lines . append ( _build_line ( padded_widths , colaligns , fmt .linebelowheader ) )
1382
+ _append_line ( lines , padded_widths , colaligns , fmt .linebelowheader )
1304
1383
1305
1384
if padded_rows and fmt .linebetweenrows and "linebetweenrows" not in hidden :
1306
1385
# initial rows with a line below
1307
1386
for row in padded_rows [:- 1 ]:
1308
- lines . append ( _build_row ( row , padded_widths , colaligns , fmt .datarow ) )
1309
- lines . append ( _build_line ( padded_widths , colaligns , fmt .linebetweenrows ) )
1387
+ append_row ( lines , row , padded_widths , colaligns , fmt .datarow )
1388
+ _append_line ( lines , padded_widths , colaligns , fmt .linebetweenrows )
1310
1389
# the last row without a line below
1311
- lines . append ( _build_row ( padded_rows [- 1 ], padded_widths , colaligns , fmt .datarow ) )
1390
+ append_row ( lines , padded_rows [- 1 ], padded_widths , colaligns , fmt .datarow )
1312
1391
else :
1313
1392
for row in padded_rows :
1314
- lines . append ( _build_row ( row , padded_widths , colaligns , fmt .datarow ) )
1393
+ append_row ( lines , row , padded_widths , colaligns , fmt .datarow )
1315
1394
1316
1395
if fmt .linebelow and "linebelow" not in hidden :
1317
- lines . append ( _build_line ( padded_widths , colaligns , fmt .linebelow ) )
1396
+ _append_line ( lines , padded_widths , colaligns , fmt .linebelow )
1318
1397
1319
1398
if headers or rows :
1320
1399
return "\n " .join (lines )
0 commit comments