Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/tabulate/__init__.py: 69%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Pretty-print tabular data."""
3from collections import namedtuple
4from collections.abc import Callable, Iterable, Sized
5import dataclasses
6from dataclasses import dataclass
7from decimal import Decimal
8from functools import partial, reduce
9from html import escape as htmlescape
10from importlib.metadata import PackageNotFoundError, version
11import io
12from itertools import chain, zip_longest as izip_longest
13import math
14import re
15import textwrap
16import warnings
18try:
19 import wcwidth # optional wide-character (CJK) support
20except ImportError:
21 wcwidth = None
23try:
24 __version__ = version("tabulate") # installed package
25except PackageNotFoundError:
26 try:
27 from ._version import version as __version__ # editable / source checkout
28 except ImportError:
29 __version__ = "unknown"
32__all__ = ["tabulate", "tabulate_formats", "simple_separated_format"]
34# minimum extra space in headers
35MIN_PADDING = 2
37# Whether or not to preserve leading/trailing whitespace in data.
38PRESERVE_WHITESPACE = False
40# TextWrapper breaks words longer than 'width'.
41_BREAK_LONG_WORDS = True
42# TextWrapper is breaking hyphenated words.
43_BREAK_ON_HYPHENS = True
46_DEFAULT_FLOATFMT = "g"
47_DEFAULT_INTFMT = ""
48_DEFAULT_MISSINGVAL = ""
49# default align will be overwritten by "left", "center" or "decimal"
50# depending on the formatter
51_DEFAULT_ALIGN = "default"
54# if True, enable wide-character (CJK) support
55WIDE_CHARS_MODE = wcwidth is not None
57# Constant that can be used as part of passed rows to generate a separating line
58# It is purposely an unprintable character, very unlikely to be used in a table
59SEPARATING_LINE = "\001"
61Line = namedtuple("Line", ["begin", "hline", "sep", "end"])
64@dataclass
65class DataRow:
66 begin: str
67 sep: str
68 end: str
69 escape_map: dict = None
72# A table structure is supposed to be:
73#
74# --- lineabove ---------
75# headerrow
76# --- linebelowheader ---
77# datarow
78# --- linebetweenrows ---
79# ... (more datarows) ...
80# --- linebetweenrows ---
81# last datarow
82# --- linebelow ---------
83#
84# TableFormat's line* elements can be
85#
86# - either None, if the element is not used,
87# - or a Line tuple,
88# - or a function: [col_widths], [col_alignments] -> string.
89#
90# TableFormat's *row elements can be
91#
92# - either None, if the element is not used,
93# - or a DataRow tuple,
94# - or a function: [cell_values], [col_widths], [col_alignments] -> string.
95#
96# padding (an integer) is the amount of white space around data values.
97#
98# with_header_hide:
99#
100# - either None, to display all table elements unconditionally,
101# - or a list of elements not to be displayed if the table has column headers.
102#
103TableFormat = namedtuple(
104 "TableFormat",
105 [
106 "lineabove",
107 "linebelowheader",
108 "linebetweenrows",
109 "linebelow",
110 "headerrow",
111 "datarow",
112 "padding",
113 "with_header_hide",
114 ],
115)
118def _is_file(f):
119 return isinstance(f, io.IOBase)
122def _is_separating_line_value(value):
123 return type(value) is str and value.strip() == SEPARATING_LINE
126def _is_separating_line(row):
127 row_type = type(row)
128 return (row_type is list or row_type is str) and (
129 (len(row) >= 1 and _is_separating_line_value(row[0]))
130 or (len(row) >= 2 and _is_separating_line_value(row[1]))
131 )
134def _pipe_segment_with_colons(align, colwidth):
135 """Return a segment of a horizontal line with optional colons which
136 indicate column's alignment (as in `pipe` output format)."""
137 w = colwidth
138 if align in ["right", "decimal"]:
139 return ("-" * (w - 1)) + ":"
140 elif align == "center":
141 return ":" + ("-" * (w - 2)) + ":"
142 elif align == "left":
143 return ":" + ("-" * (w - 1))
144 else:
145 return "-" * w
148def _pipe_line_with_colons(colwidths, colaligns):
149 """Return a horizontal line with optional colons to indicate column's
150 alignment (as in `pipe` output format)."""
151 if not colaligns: # e.g. printing an empty data frame (github issue #15)
152 colaligns = [""] * len(colwidths)
153 segments = "|".join(_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths))
154 return f"|{segments}|"
157def _grid_segment_with_colons(colwidth, align):
158 """Return a segment of a horizontal line with optional colons which indicate
159 column's alignment in a grid table."""
160 width = colwidth
161 if align == "right":
162 return ("=" * (width - 1)) + ":"
163 elif align == "center":
164 return ":" + ("=" * (width - 2)) + ":"
165 elif align == "left":
166 return ":" + ("=" * (width - 1))
167 else:
168 return "=" * width
171def _grid_line_with_colons(colwidths, colaligns):
172 """Return a horizontal line with optional colons to indicate column's alignment
173 in a grid table."""
174 if not colaligns:
175 colaligns = [""] * len(colwidths)
176 segments = "+".join(_grid_segment_with_colons(w, a) for a, w in zip(colaligns, colwidths))
177 return f"+{segments}+"
180def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
181 alignment = {
182 "left": "",
183 "right": 'style="text-align: right;"| ',
184 "center": 'style="text-align: center;"| ',
185 "decimal": 'style="text-align: right;"| ',
186 }
187 # hard-coded padding _around_ align attribute and value together
188 # rather than padding parameter which affects only the value
189 values_with_attrs = [
190 " " + alignment.get(a, "") + c + " " for c, a in zip(cell_values, colaligns)
191 ]
192 colsep = separator * 2
193 return (separator + colsep.join(values_with_attrs)).rstrip()
196def _textile_row_with_attrs(cell_values, colwidths, colaligns):
197 cell_values[0] += " "
198 alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."}
199 values = "|".join(alignment.get(a, "") + v for a, v in zip(colaligns, cell_values))
200 return f"|{values}|"
203def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore):
204 # this table header will be suppressed if there is a header row
205 return "<table>\n<tbody>"
208def _html_row_with_attrs(celltag, unsafe, cell_values, colwidths, colaligns):
209 alignment = {
210 "left": ' style="text-align: left;"',
211 "right": ' style="text-align: right;"',
212 "center": ' style="text-align: center;"',
213 "decimal": ' style="text-align: right;"',
214 }
215 if unsafe:
216 values_with_attrs = [
217 "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), c)
218 for c, a in zip(cell_values, colaligns)
219 ]
220 else:
221 values_with_attrs = [
222 "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), htmlescape(c))
223 for c, a in zip(cell_values, colaligns)
224 ]
225 rowhtml = "<tr>{}</tr>".format("".join(values_with_attrs).rstrip())
226 if celltag == "th": # it's a header row, create a new table header
227 rowhtml = f"<table>\n<thead>\n{rowhtml}\n</thead>\n<tbody>"
228 return rowhtml
231def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=""):
232 alignment = {
233 "left": '<style="text-align: left;">',
234 "right": '<style="text-align: right;">',
235 "center": '<style="text-align: center;">',
236 "decimal": '<style="text-align: right;">',
237 }
238 values_with_attrs = [
239 "{}{} {} ".format(celltag, alignment.get(a, ""), header + c + header)
240 for c, a in zip(cell_values, colaligns)
241 ]
242 return "".join(values_with_attrs) + "||"
245def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=False):
246 alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"}
247 tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns])
248 return "\n".join(
249 [
250 ("\\begin{tabular}{" if not longtable else "\\begin{longtable}{")
251 + tabular_columns_fmt
252 + "}",
253 "\\toprule" if booktabs else "\\hline",
254 ]
255 )
258def _asciidoc_row(is_header, *args):
259 """handle header and data rows for asciidoc format"""
261 def make_header_line(is_header, colwidths, colaligns):
262 # generate the column specifiers
264 alignment = {"left": "<", "right": ">", "center": "^", "decimal": ">"}
265 # use the column widths generated by tabulate for the asciidoc column width specifiers
266 asciidoc_alignments = zip(colwidths, [alignment[colalign] for colalign in colaligns])
267 asciidoc_column_specifiers = [f"{align}{width:d}" for width, align in asciidoc_alignments]
268 header_list = ['cols="' + (",".join(asciidoc_column_specifiers)) + '"']
270 # generate the list of options (currently only "header")
271 options_list = []
273 if is_header:
274 options_list.append("header")
276 if options_list:
277 options_list = ",".join(options_list)
278 header_list.append(f'options="{options_list}"')
280 # generate the list of entries in the table header field
282 line = "[{}]\n|====".format(",".join(header_list))
283 return line.rstrip()
285 if len(args) == 2:
286 # two arguments are passed if called in the context of aboveline
287 # print the table header with column widths and optional header tag
288 line = make_header_line(False, *args)
289 return line.rstrip()
291 elif len(args) == 3:
292 # three arguments are passed if called in the context of dataline or headerline
293 # print the table line and make the aboveline if it is a header
295 cell_values, colwidths, colaligns = args
296 data_line = "|" + "|".join(cell_values)
298 if is_header:
299 line = make_header_line(True, colwidths, colaligns) + "\n" + data_line
300 return line.rstrip()
301 else:
302 return data_line.rstrip()
304 else:
305 raise ValueError(
306 "_asciidoc_row() requires two (colwidths, colaligns) "
307 "or three (cell_values, colwidths, colaligns) arguments) "
308 )
311LATEX_ESCAPE_RULES = {
312 r"&": r"\&",
313 r"%": r"\%",
314 r"$": r"\$",
315 r"#": r"\#",
316 r"_": r"\_",
317 r"^": r"\^{}",
318 r"{": r"\{",
319 r"}": r"\}",
320 r"~": r"\textasciitilde{}",
321 "\\": r"\textbackslash{}",
322 r"<": r"\ensuremath{<}",
323 r">": r"\ensuremath{>}",
324}
327_latex_row = DataRow("", "&", "\\\\", LATEX_ESCAPE_RULES)
330GITHUB_ESCAPE_RULES = {r"|": r"\|"}
333def _rst_escape_first_column(rows, headers):
334 def escape_empty(val):
335 if isinstance(val, (str, bytes)) and not val.strip():
336 return ".."
337 else:
338 return val
340 new_headers = list(headers)
341 new_rows = []
342 if headers:
343 new_headers[0] = escape_empty(headers[0])
344 for row in rows:
345 new_row = list(row)
346 if new_row:
347 new_row[0] = escape_empty(row[0])
348 new_rows.append(new_row)
349 return new_rows, new_headers
352_table_formats = {
353 "simple": TableFormat(
354 lineabove=Line("", "-", " ", ""),
355 linebelowheader=Line("", "-", " ", ""),
356 linebetweenrows=None,
357 linebelow=Line("", "-", " ", ""),
358 headerrow=DataRow("", " ", ""),
359 datarow=DataRow("", " ", ""),
360 padding=0,
361 with_header_hide=["lineabove", "linebelow"],
362 ),
363 "plain": TableFormat(
364 lineabove=None,
365 linebelowheader=None,
366 linebetweenrows=None,
367 linebelow=None,
368 headerrow=DataRow("", " ", ""),
369 datarow=DataRow("", " ", ""),
370 padding=0,
371 with_header_hide=None,
372 ),
373 "grid": TableFormat(
374 lineabove=Line("+", "-", "+", "+"),
375 linebelowheader=Line("+", "=", "+", "+"),
376 linebetweenrows=Line("+", "-", "+", "+"),
377 linebelow=Line("+", "-", "+", "+"),
378 headerrow=DataRow("|", "|", "|"),
379 datarow=DataRow("|", "|", "|"),
380 padding=1,
381 with_header_hide=None,
382 ),
383 "simple_grid": TableFormat(
384 lineabove=Line("┌", "─", "┬", "┐"),
385 linebelowheader=Line("├", "─", "┼", "┤"),
386 linebetweenrows=Line("├", "─", "┼", "┤"),
387 linebelow=Line("└", "─", "┴", "┘"),
388 headerrow=DataRow("│", "│", "│"),
389 datarow=DataRow("│", "│", "│"),
390 padding=1,
391 with_header_hide=None,
392 ),
393 "rounded_grid": TableFormat(
394 lineabove=Line("╭", "─", "┬", "╮"),
395 linebelowheader=Line("├", "─", "┼", "┤"),
396 linebetweenrows=Line("├", "─", "┼", "┤"),
397 linebelow=Line("╰", "─", "┴", "╯"),
398 headerrow=DataRow("│", "│", "│"),
399 datarow=DataRow("│", "│", "│"),
400 padding=1,
401 with_header_hide=None,
402 ),
403 "heavy_grid": TableFormat(
404 lineabove=Line("┏", "━", "┳", "┓"),
405 linebelowheader=Line("┣", "━", "╋", "┫"),
406 linebetweenrows=Line("┣", "━", "╋", "┫"),
407 linebelow=Line("┗", "━", "┻", "┛"),
408 headerrow=DataRow("┃", "┃", "┃"),
409 datarow=DataRow("┃", "┃", "┃"),
410 padding=1,
411 with_header_hide=None,
412 ),
413 "mixed_grid": TableFormat(
414 lineabove=Line("┍", "━", "┯", "┑"),
415 linebelowheader=Line("┝", "━", "┿", "┥"),
416 linebetweenrows=Line("├", "─", "┼", "┤"),
417 linebelow=Line("┕", "━", "┷", "┙"),
418 headerrow=DataRow("│", "│", "│"),
419 datarow=DataRow("│", "│", "│"),
420 padding=1,
421 with_header_hide=None,
422 ),
423 "double_grid": TableFormat(
424 lineabove=Line("╔", "═", "╦", "╗"),
425 linebelowheader=Line("╠", "═", "╬", "╣"),
426 linebetweenrows=Line("╠", "═", "╬", "╣"),
427 linebelow=Line("╚", "═", "╩", "╝"),
428 headerrow=DataRow("║", "║", "║"),
429 datarow=DataRow("║", "║", "║"),
430 padding=1,
431 with_header_hide=None,
432 ),
433 "fancy_grid": TableFormat(
434 lineabove=Line("╒", "═", "╤", "╕"),
435 linebelowheader=Line("╞", "═", "╪", "╡"),
436 linebetweenrows=Line("├", "─", "┼", "┤"),
437 linebelow=Line("╘", "═", "╧", "╛"),
438 headerrow=DataRow("│", "│", "│"),
439 datarow=DataRow("│", "│", "│"),
440 padding=1,
441 with_header_hide=None,
442 ),
443 "colon_grid": TableFormat(
444 lineabove=Line("+", "-", "+", "+"),
445 linebelowheader=_grid_line_with_colons,
446 linebetweenrows=Line("+", "-", "+", "+"),
447 linebelow=Line("+", "-", "+", "+"),
448 headerrow=DataRow("|", "|", "|"),
449 datarow=DataRow("|", "|", "|"),
450 padding=1,
451 with_header_hide=None,
452 ),
453 "outline": TableFormat(
454 lineabove=Line("+", "-", "+", "+"),
455 linebelowheader=Line("+", "=", "+", "+"),
456 linebetweenrows=None,
457 linebelow=Line("+", "-", "+", "+"),
458 headerrow=DataRow("|", "|", "|"),
459 datarow=DataRow("|", "|", "|"),
460 padding=1,
461 with_header_hide=None,
462 ),
463 "simple_outline": TableFormat(
464 lineabove=Line("┌", "─", "┬", "┐"),
465 linebelowheader=Line("├", "─", "┼", "┤"),
466 linebetweenrows=None,
467 linebelow=Line("└", "─", "┴", "┘"),
468 headerrow=DataRow("│", "│", "│"),
469 datarow=DataRow("│", "│", "│"),
470 padding=1,
471 with_header_hide=None,
472 ),
473 "rounded_outline": TableFormat(
474 lineabove=Line("╭", "─", "┬", "╮"),
475 linebelowheader=Line("├", "─", "┼", "┤"),
476 linebetweenrows=None,
477 linebelow=Line("╰", "─", "┴", "╯"),
478 headerrow=DataRow("│", "│", "│"),
479 datarow=DataRow("│", "│", "│"),
480 padding=1,
481 with_header_hide=None,
482 ),
483 "heavy_outline": TableFormat(
484 lineabove=Line("┏", "━", "┳", "┓"),
485 linebelowheader=Line("┣", "━", "╋", "┫"),
486 linebetweenrows=None,
487 linebelow=Line("┗", "━", "┻", "┛"),
488 headerrow=DataRow("┃", "┃", "┃"),
489 datarow=DataRow("┃", "┃", "┃"),
490 padding=1,
491 with_header_hide=None,
492 ),
493 "mixed_outline": TableFormat(
494 lineabove=Line("┍", "━", "┯", "┑"),
495 linebelowheader=Line("┝", "━", "┿", "┥"),
496 linebetweenrows=None,
497 linebelow=Line("┕", "━", "┷", "┙"),
498 headerrow=DataRow("│", "│", "│"),
499 datarow=DataRow("│", "│", "│"),
500 padding=1,
501 with_header_hide=None,
502 ),
503 "double_outline": TableFormat(
504 lineabove=Line("╔", "═", "╦", "╗"),
505 linebelowheader=Line("╠", "═", "╬", "╣"),
506 linebetweenrows=None,
507 linebelow=Line("╚", "═", "╩", "╝"),
508 headerrow=DataRow("║", "║", "║"),
509 datarow=DataRow("║", "║", "║"),
510 padding=1,
511 with_header_hide=None,
512 ),
513 "fancy_outline": TableFormat(
514 lineabove=Line("╒", "═", "╤", "╕"),
515 linebelowheader=Line("╞", "═", "╪", "╡"),
516 linebetweenrows=None,
517 linebelow=Line("╘", "═", "╧", "╛"),
518 headerrow=DataRow("│", "│", "│"),
519 datarow=DataRow("│", "│", "│"),
520 padding=1,
521 with_header_hide=None,
522 ),
523 "pipe": TableFormat(
524 lineabove=_pipe_line_with_colons,
525 linebelowheader=_pipe_line_with_colons,
526 linebetweenrows=None,
527 linebelow=None,
528 headerrow=DataRow("|", "|", "|", GITHUB_ESCAPE_RULES),
529 datarow=DataRow("|", "|", "|", GITHUB_ESCAPE_RULES),
530 padding=1,
531 with_header_hide=["lineabove"],
532 ),
533 "orgtbl": TableFormat(
534 lineabove=None,
535 linebelowheader=Line("|", "-", "+", "|"),
536 linebetweenrows=None,
537 linebelow=None,
538 headerrow=DataRow("|", "|", "|"),
539 datarow=DataRow("|", "|", "|"),
540 padding=1,
541 with_header_hide=None,
542 ),
543 "jira": TableFormat(
544 lineabove=None,
545 linebelowheader=None,
546 linebetweenrows=None,
547 linebelow=None,
548 headerrow=DataRow("||", "||", "||"),
549 datarow=DataRow("|", "|", "|"),
550 padding=1,
551 with_header_hide=None,
552 ),
553 "presto": TableFormat(
554 lineabove=None,
555 linebelowheader=Line("", "-", "+", ""),
556 linebetweenrows=None,
557 linebelow=None,
558 headerrow=DataRow("", "|", ""),
559 datarow=DataRow("", "|", ""),
560 padding=1,
561 with_header_hide=None,
562 ),
563 "pretty": TableFormat(
564 lineabove=Line("+", "-", "+", "+"),
565 linebelowheader=Line("+", "-", "+", "+"),
566 linebetweenrows=None,
567 linebelow=Line("+", "-", "+", "+"),
568 headerrow=DataRow("|", "|", "|"),
569 datarow=DataRow("|", "|", "|"),
570 padding=1,
571 with_header_hide=None,
572 ),
573 "psql": TableFormat(
574 lineabove=Line("+", "-", "+", "+"),
575 linebelowheader=Line("|", "-", "+", "|"),
576 linebetweenrows=None,
577 linebelow=Line("+", "-", "+", "+"),
578 headerrow=DataRow("|", "|", "|"),
579 datarow=DataRow("|", "|", "|"),
580 padding=1,
581 with_header_hide=None,
582 ),
583 "rst": TableFormat(
584 lineabove=Line("", "=", " ", ""),
585 linebelowheader=Line("", "=", " ", ""),
586 linebetweenrows=None,
587 linebelow=Line("", "=", " ", ""),
588 headerrow=DataRow("", " ", ""),
589 datarow=DataRow("", " ", ""),
590 padding=0,
591 with_header_hide=None,
592 ),
593 "mediawiki": TableFormat(
594 lineabove=Line(
595 '{| class="wikitable" style="text-align: left;"',
596 "",
597 "",
598 "\n|+ <!-- caption -->\n|-",
599 ),
600 linebelowheader=Line("|-", "", "", ""),
601 linebetweenrows=Line("|-", "", "", ""),
602 linebelow=Line("|}", "", "", ""),
603 headerrow=partial(_mediawiki_row_with_attrs, "!"),
604 datarow=partial(_mediawiki_row_with_attrs, "|"),
605 padding=0,
606 with_header_hide=None,
607 ),
608 "moinmoin": TableFormat(
609 lineabove=None,
610 linebelowheader=None,
611 linebetweenrows=None,
612 linebelow=None,
613 headerrow=partial(_moin_row_with_attrs, "||", header="'''"),
614 datarow=partial(_moin_row_with_attrs, "||"),
615 padding=1,
616 with_header_hide=None,
617 ),
618 "html": TableFormat(
619 lineabove=_html_begin_table_without_header,
620 linebelowheader="",
621 linebetweenrows=None,
622 linebelow=Line("</tbody>\n</table>", "", "", ""),
623 headerrow=partial(_html_row_with_attrs, "th", False),
624 datarow=partial(_html_row_with_attrs, "td", False),
625 padding=0,
626 with_header_hide=["lineabove"],
627 ),
628 "unsafehtml": TableFormat(
629 lineabove=_html_begin_table_without_header,
630 linebelowheader="",
631 linebetweenrows=None,
632 linebelow=Line("</tbody>\n</table>", "", "", ""),
633 headerrow=partial(_html_row_with_attrs, "th", True),
634 datarow=partial(_html_row_with_attrs, "td", True),
635 padding=0,
636 with_header_hide=["lineabove"],
637 ),
638 "latex": TableFormat(
639 lineabove=_latex_line_begin_tabular,
640 linebelowheader=Line("\\hline", "", "", ""),
641 linebetweenrows=None,
642 linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
643 headerrow=_latex_row,
644 datarow=_latex_row,
645 padding=1,
646 with_header_hide=None,
647 ),
648 "latex_raw": TableFormat(
649 lineabove=_latex_line_begin_tabular,
650 linebelowheader=Line("\\hline", "", "", ""),
651 linebetweenrows=None,
652 linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
653 headerrow=DataRow("", "&", "\\\\", {}),
654 datarow=DataRow("", "&", "\\\\", {}),
655 padding=1,
656 with_header_hide=None,
657 ),
658 "latex_booktabs": TableFormat(
659 lineabove=partial(_latex_line_begin_tabular, booktabs=True),
660 linebelowheader=Line("\\midrule", "", "", ""),
661 linebetweenrows=None,
662 linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""),
663 headerrow=_latex_row,
664 datarow=_latex_row,
665 padding=1,
666 with_header_hide=None,
667 ),
668 "latex_longtable": TableFormat(
669 lineabove=partial(_latex_line_begin_tabular, longtable=True),
670 linebelowheader=Line("\\hline\n\\endhead", "", "", ""),
671 linebetweenrows=None,
672 linebelow=Line("\\hline\n\\end{longtable}", "", "", ""),
673 headerrow=_latex_row,
674 datarow=_latex_row,
675 padding=1,
676 with_header_hide=None,
677 ),
678 "tsv": TableFormat(
679 lineabove=None,
680 linebelowheader=None,
681 linebetweenrows=None,
682 linebelow=None,
683 headerrow=DataRow("", "\t", ""),
684 datarow=DataRow("", "\t", ""),
685 padding=0,
686 with_header_hide=None,
687 ),
688 "textile": TableFormat(
689 lineabove=None,
690 linebelowheader=None,
691 linebetweenrows=None,
692 linebelow=None,
693 headerrow=DataRow("|_. ", "|_.", "|"),
694 datarow=_textile_row_with_attrs,
695 padding=1,
696 with_header_hide=None,
697 ),
698 "asciidoc": TableFormat(
699 lineabove=partial(_asciidoc_row, False),
700 linebelowheader=None,
701 linebetweenrows=None,
702 linebelow=Line("|====", "", "", ""),
703 headerrow=partial(_asciidoc_row, True),
704 datarow=partial(_asciidoc_row, False),
705 padding=1,
706 with_header_hide=["lineabove"],
707 ),
708}
710# "github" is an alias for "pipe": both produce GitHub-flavored Markdown with
711# alignment colons in the separator row.
712_table_formats["github"] = _table_formats["pipe"]
715tabulate_formats = sorted(_table_formats.keys())
717# The table formats for which multiline cells will be folded into subsequent
718# table rows. The key is the original format specified at the API. The value is
719# the format that will be used to represent the original format.
720multiline_formats = {
721 "plain": "plain",
722 "simple": "simple",
723 "grid": "grid",
724 "simple_grid": "simple_grid",
725 "rounded_grid": "rounded_grid",
726 "heavy_grid": "heavy_grid",
727 "mixed_grid": "mixed_grid",
728 "double_grid": "double_grid",
729 "fancy_grid": "fancy_grid",
730 "colon_grid": "colon_grid",
731 "pipe": "pipe",
732 "orgtbl": "orgtbl",
733 "jira": "jira",
734 "presto": "presto",
735 "pretty": "pretty",
736 "psql": "psql",
737 "rst": "rst",
738 "github": "github",
739 "outline": "outline",
740 "simple_outline": "simple_outline",
741 "rounded_outline": "rounded_outline",
742 "heavy_outline": "heavy_outline",
743 "mixed_outline": "mixed_outline",
744 "double_outline": "double_outline",
745 "fancy_outline": "fancy_outline",
746}
748# TODO: Add multiline support for the remaining table formats:
749# - mediawiki: Replace \n with <br>
750# - moinmoin: TBD
751# - html: Replace \n with <br>
752# - latex*: Use "makecell" package: In header, replace X\nY with
753# \thead{X\\Y} and in data row, replace X\nY with \makecell{X\\Y}
754# - tsv: TBD
755# - textile: Replace \n with <br/> (must be well-formed XML)
757_multiline_codes = re.compile(r"\r|\n|\r\n")
758_multiline_codes_bytes = re.compile(b"\r|\n|\r\n")
760# Handle ANSI escape sequences for both control sequence introducer (CSI) and
761# operating system command (OSC). Both of these begin with 0x1b (or octal 033),
762# which will be shown below as ESC.
763#
764# CSI ANSI escape codes have the following format, defined in section 5.4 of ECMA-48:
765#
766# CSI: ESC followed by the '[' character (0x5b)
767# Parameter Bytes: 0..n bytes in the range 0x30-0x3f
768# Intermediate Bytes: 0..n bytes in the range 0x20-0x2f
769# Final Byte: a single byte in the range 0x40-0x7e
770#
771# Also include the terminal hyperlink sequences as described here:
772# https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
773#
774# OSC 8 ; params ; uri ST display_text OSC 8 ;; ST
775#
776# Example: \x1b]8;;https://example.com\x5ctext to show\x1b]8;;\x5c
777#
778# Where:
779# OSC: ESC followed by the ']' character (0x5d)
780# params: 0..n optional key value pairs separated by ':' (e.g. foo=bar:baz=qux:abc=123)
781# URI: the actual URI with protocol scheme (e.g. https://, file://, ftp://)
782# ST: ESC followed by the '\' character (0x5c)
783_esc = r"\x1b"
784_csi = rf"{_esc}\["
785_osc = rf"{_esc}\]"
786_st = rf"{_esc}\\"
788_ansi_escape_pat = rf"""
789 (
790 # terminal colors, etc
791 {_csi} # CSI
792 [\x30-\x3f]* # parameter bytes
793 [\x20-\x2f]* # intermediate bytes
794 [\x40-\x7e] # final byte
795 |
796 # terminal hyperlinks
797 {_osc}8; # OSC opening
798 (\w+=\w+:?)* # key=value params list (submatch 2)
799 ; # delimiter
800 ([^{_esc}]+) # URI - anything but ESC (submatch 3)
801 {_st} # ST
802 ([^{_esc}]+) # link text - anything but ESC (submatch 4)
803 {_osc}8;;{_st} # "closing" OSC sequence
804 )
805"""
806_ansi_codes = re.compile(_ansi_escape_pat, re.VERBOSE)
807_ansi_codes_bytes = re.compile(_ansi_escape_pat.encode("utf8"), re.VERBOSE)
808_ansi_color_reset_code = "\033[0m"
810_float_with_thousands_separators = re.compile(
811 r"^(([+-]?[0-9]{1,3})(?:,([0-9]{3}))*)?(?(1)\.[0-9]*|\.[0-9]+)?$"
812)
815def simple_separated_format(separator):
816 """Construct a simple TableFormat with columns separated by a separator.
818 >>> tsv = simple_separated_format("\\t") ; \
819 tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23'
820 True
822 """
823 return TableFormat(
824 None,
825 None,
826 None,
827 None,
828 headerrow=DataRow("", separator, ""),
829 datarow=DataRow("", separator, ""),
830 padding=0,
831 with_header_hide=None,
832 )
835def _isnumber_with_thousands_separator(string):
836 """
837 >>> _isnumber_with_thousands_separator(".")
838 False
839 >>> _isnumber_with_thousands_separator("1")
840 True
841 >>> _isnumber_with_thousands_separator("1.")
842 True
843 >>> _isnumber_with_thousands_separator(".1")
844 True
845 >>> _isnumber_with_thousands_separator("1000")
846 False
847 >>> _isnumber_with_thousands_separator("1,000")
848 True
849 >>> _isnumber_with_thousands_separator("1,0000")
850 False
851 >>> _isnumber_with_thousands_separator("1,000.1234")
852 True
853 >>> _isnumber_with_thousands_separator(b"1,000.1234")
854 True
855 >>> _isnumber_with_thousands_separator("+1,000.1234")
856 True
857 >>> _isnumber_with_thousands_separator("-1,000.1234")
858 True
859 """
860 try:
861 string = string.decode()
862 except (UnicodeDecodeError, AttributeError):
863 pass
865 return bool(re.match(_float_with_thousands_separators, string))
868def _isconvertible(conv, string):
869 try:
870 conv(string)
871 return True
872 except (ValueError, TypeError):
873 return False
876def _isnumber(string):
877 """Detects if something *could* be considered a numeric value, vs. just a string.
879 This promotes types convertible to both int and float to be considered
880 a float. Note that, iff *all* values appear to be some form of numeric
881 value such as eg. "1e2", they would be considered numbers!
883 The exception is things that appear to be numbers but overflow to
884 +/-inf, eg. "1e23456"; we'll have to exclude them explicitly.
886 >>> _isnumber(123)
887 True
888 >>> _isnumber(123.45)
889 True
890 >>> _isnumber("123.45")
891 True
892 >>> _isnumber("123")
893 True
894 >>> _isnumber("spam")
895 False
896 >>> _isnumber("123e45")
897 True
898 >>> _isnumber("123e45678") # evaluates equal to 'inf', but ... isn't
899 False
900 >>> _isnumber("inf")
901 True
902 >>> from fractions import Fraction
903 >>> _isnumber(Fraction(1,3))
904 True
906 """
907 return (
908 # fast path
909 type(string) in (float, int)
910 # covers 'NaN', +/- 'inf', and eg. '1e2', as well as any type
911 # convertible to int/float.
912 or (
913 _isconvertible(float, string)
914 and (
915 # some other type convertible to float
916 not isinstance(string, (str, bytes))
917 # or, a numeric string eg. "1e1...", "NaN", ..., but isn't
918 # just an over/underflow
919 or (
920 not (math.isinf(float(string)) or math.isnan(float(string)))
921 or string.lower() in ["inf", "-inf", "nan"]
922 )
923 )
924 )
925 )
928def _isint(string, inttype=int):
929 """
930 >>> _isint("123")
931 True
932 >>> _isint("123.45")
933 False
934 """
935 return (
936 type(string) is inttype
937 or (
938 (hasattr(string, "is_integer") or hasattr(string, "__array__"))
939 and str(type(string)).startswith("<class 'numpy.int")
940 ) # numpy.int64 and similar
941 or (
942 isinstance(string, (bytes, str)) and _isconvertible(inttype, string)
943 ) # integer as string
944 )
947def _isbool(string):
948 """
949 >>> _isbool(True)
950 True
951 >>> _isbool("False")
952 True
953 >>> _isbool(1)
954 False
955 """
956 return type(string) is bool or (
957 isinstance(string, (bytes, str)) and string in ("True", "False")
958 )
961def _type(string, has_invisible=True, numparse=True):
962 """The least generic type (type(None), int, float, str, unicode).
964 Treats empty string as missing for the purposes of type deduction, so as to not influence
965 the type of an otherwise complete column; does *not* result in missingval replacement!
967 >>> _type(None) is type(None)
968 True
969 >>> _type("") is type(None)
970 True
971 >>> _type("foo") is type("")
972 True
973 >>> _type("1") is type(1)
974 True
975 >>> _type('\x1b[31m42\x1b[0m') is type(42)
976 True
977 >>> _type('\x1b[31m42\x1b[0m') is type(42)
978 True
980 """
982 if has_invisible and isinstance(string, (str, bytes)):
983 string = _strip_ansi(string)
985 if string is None or (isinstance(string, (bytes, str)) and not string):
986 return type(None)
987 elif hasattr(string, "isoformat"): # datetime.datetime, date, and time
988 return str
989 elif _isbool(string):
990 return bool
991 elif numparse and (
992 _isint(string)
993 or (
994 isinstance(string, str)
995 and _isnumber_with_thousands_separator(string)
996 and "." not in string
997 )
998 ):
999 return int
1000 elif numparse and (
1001 _isnumber(string)
1002 or (isinstance(string, str) and _isnumber_with_thousands_separator(string))
1003 ):
1004 return float
1005 elif isinstance(string, bytes):
1006 return bytes
1007 else:
1008 return str
1011def _afterpoint(string):
1012 """Symbols after a decimal point, -1 if the string lacks the decimal point.
1014 >>> _afterpoint("123.45")
1015 2
1016 >>> _afterpoint("1001")
1017 -1
1018 >>> _afterpoint("eggs")
1019 -1
1020 >>> _afterpoint("123e45")
1021 2
1022 >>> _afterpoint("123,456.78")
1023 2
1025 """
1026 if _isnumber(string) or _isnumber_with_thousands_separator(string):
1027 if _isint(string):
1028 return -1
1029 else:
1030 pos = string.rfind(".")
1031 pos = string.lower().rfind("e") if pos < 0 else pos
1032 if pos >= 0:
1033 return len(string) - pos - 1
1034 else:
1035 return -1 # no point
1036 else:
1037 return -1 # not a number
1040def _padleft(width, s):
1041 """Flush right.
1043 >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430'
1044 True
1046 """
1047 fmt = f"{{0:>{width}s}}"
1048 return fmt.format(s)
1051def _padright(width, s):
1052 """Flush left.
1054 >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 '
1055 True
1057 """
1058 fmt = f"{{0:<{width}s}}"
1059 return fmt.format(s)
1062def _padboth(width, s):
1063 """Center string.
1065 >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 '
1066 True
1068 """
1069 fmt = f"{{0:^{width}s}}"
1070 return fmt.format(s)
1073def _padnone(ignore_width, s):
1074 return s
1077def _strip_ansi(s):
1078 r"""Remove ANSI escape sequences, both CSI (color codes, etc) and OSC hyperlinks.
1080 CSI sequences are simply removed from the output, while OSC hyperlinks are replaced
1081 with the link text. Note: it may be desirable to show the URI instead but this is not
1082 supported.
1084 >>> repr(_strip_ansi('\x1B]8;;https://example.com\x1B\\This is a link\x1B]8;;\x1B\\'))
1085 "'This is a link'"
1087 >>> repr(_strip_ansi('\x1b[31mred\x1b[0m text'))
1088 "'red text'"
1090 """
1091 if isinstance(s, str):
1092 return _ansi_codes.sub(r"\4", s)
1093 else: # a bytestring
1094 return _ansi_codes_bytes.sub(r"\4", s)
1097def _visible_width(s):
1098 """Visible width of a printed string. ANSI color codes are removed.
1100 >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world")
1101 (5, 5)
1103 """
1104 # optional wide-character support
1105 if wcwidth is not None and WIDE_CHARS_MODE:
1106 # when already a string, it could contain terminal sequences,
1107 # wcwidth >= 0.3.0 handles ANSI codes internally,
1108 if hasattr(wcwidth, "width"):
1109 return wcwidth.width(str(s))
1110 # while previous versions need them stripped first.
1111 if isinstance(s, (str, bytes)):
1112 return wcwidth.wcswidth(_strip_ansi(str(s)))
1114 # Otherwise, coerce to string, guaranteed to be without any control codes,
1115 # we can use wcswidth() directly.
1116 return wcwidth.wcswidth(str(s))
1117 if isinstance(s, (str, bytes)):
1118 return len(_strip_ansi(s))
1119 else:
1120 return len(str(s))
1123def _is_multiline(s):
1124 if isinstance(s, str):
1125 return bool(re.search(_multiline_codes, s))
1126 else: # a bytestring
1127 return bool(re.search(_multiline_codes_bytes, s))
1130def _multiline_width(multiline_s, line_width_fn=len):
1131 """Visible width of a potentially multiline content."""
1132 return max(map(line_width_fn, re.split("[\r\n]", multiline_s)))
1135def _choose_width_fn(has_invisible, enable_widechars, is_multiline):
1136 """Return a function to calculate visible cell width."""
1137 if has_invisible:
1138 line_width_fn = _visible_width
1139 elif enable_widechars: # optional wide-character support if available
1140 line_width_fn = wcwidth.wcswidth
1141 else:
1142 line_width_fn = len
1143 if is_multiline:
1144 width_fn = lambda s: _multiline_width(s, line_width_fn) # noqa: E731
1145 else:
1146 width_fn = line_width_fn
1147 return width_fn
1150def _align_column_choose_padfn(strings, alignment, has_invisible, preserve_whitespace):
1151 if alignment == "right":
1152 if not preserve_whitespace:
1153 strings = [s.strip() for s in strings]
1154 padfn = _padleft
1155 elif alignment == "center":
1156 if not preserve_whitespace:
1157 strings = [s.strip() for s in strings]
1158 padfn = _padboth
1159 elif alignment == "decimal":
1160 if has_invisible:
1161 decimals = [_afterpoint(_strip_ansi(s)) for s in strings]
1162 else:
1163 decimals = [_afterpoint(s) for s in strings]
1164 maxdecimals = max(decimals)
1165 strings = [s + (maxdecimals - decs) * " " for s, decs in zip(strings, decimals)]
1166 padfn = _padleft
1167 elif not alignment:
1168 padfn = _padnone
1169 else:
1170 if not preserve_whitespace:
1171 strings = [s.strip() for s in strings]
1172 padfn = _padright
1173 return strings, padfn
1176def _align_column_choose_width_fn(has_invisible, enable_widechars, is_multiline):
1177 if has_invisible:
1178 line_width_fn = _visible_width
1179 elif enable_widechars: # optional wide-character support if available
1180 line_width_fn = wcwidth.wcswidth
1181 else:
1182 line_width_fn = len
1183 if is_multiline:
1184 width_fn = lambda s: _align_column_multiline_width( # noqa: E731
1185 s, line_width_fn
1186 )
1187 else:
1188 width_fn = line_width_fn
1189 return width_fn
1192def _align_column_multiline_width(multiline_s, line_width_fn=len):
1193 """Visible width of a potentially multiline content."""
1194 return list(map(line_width_fn, re.split("[\r\n]", multiline_s)))
1197def _flat_list(nested_list):
1198 ret = []
1199 for item in nested_list:
1200 if isinstance(item, list):
1201 ret.extend(item)
1202 else:
1203 ret.append(item)
1204 return ret
1207def _align_column(
1208 strings,
1209 alignment,
1210 minwidth=0,
1211 has_invisible=True,
1212 enable_widechars=False,
1213 is_multiline=False,
1214 preserve_whitespace=False,
1215):
1216 """[string] -> [padded_string]"""
1217 strings, padfn = _align_column_choose_padfn(
1218 strings, alignment, has_invisible, preserve_whitespace
1219 )
1220 width_fn = _align_column_choose_width_fn(has_invisible, enable_widechars, is_multiline)
1222 s_widths = list(map(width_fn, strings))
1223 maxwidth = max(max(_flat_list(s_widths)), minwidth)
1224 # TODO: refactor column alignment in single-line and multiline modes
1225 if is_multiline:
1226 if not enable_widechars and not has_invisible:
1227 padded_strings = [
1228 "\n".join([padfn(maxwidth, s) for s in ms.splitlines()]) for ms in strings
1229 ]
1230 else:
1231 # enable wide-character width corrections
1232 s_lens = [[len(s) for s in re.split("[\r\n]", ms)] for ms in strings]
1233 visible_widths = [
1234 [maxwidth - (w - ln) for w, ln in zip(mw, ml)] for mw, ml in zip(s_widths, s_lens)
1235 ]
1236 # wcswidth and _visible_width don't count invisible characters;
1237 # padfn doesn't need to apply another correction
1238 padded_strings = [
1239 "\n".join([padfn(w, s) for s, w in zip((ms.splitlines() or ms), mw)])
1240 for ms, mw in zip(strings, visible_widths)
1241 ]
1242 else: # single-line cell values
1243 if not enable_widechars and not has_invisible:
1244 padded_strings = [padfn(maxwidth, s) for s in strings]
1245 else:
1246 # enable wide-character width corrections
1247 s_lens = list(map(len, strings))
1248 visible_widths = [maxwidth - (w - ln) for w, ln in zip(s_widths, s_lens)]
1249 # wcswidth and _visible_width don't count invisible characters;
1250 # padfn doesn't need to apply another correction
1251 padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)]
1252 return padded_strings
1255def _more_generic(type1, type2):
1256 types = {
1257 type(None): 0,
1258 bool: 1,
1259 int: 2,
1260 float: 3,
1261 bytes: 4,
1262 str: 5,
1263 }
1264 invtypes = {
1265 5: str,
1266 4: bytes,
1267 3: float,
1268 2: int,
1269 1: bool,
1270 0: type(None),
1271 }
1272 moregeneric = max(types.get(type1, 5), types.get(type2, 5))
1273 return invtypes[moregeneric]
1276def _column_type(strings, has_invisible=True, numparse=True):
1277 """The least generic type all column values are convertible to.
1279 >>> _column_type([True, False]) is bool
1280 True
1281 >>> _column_type(["1", "2"]) is int
1282 True
1283 >>> _column_type(["1", "2.3"]) is float
1284 True
1285 >>> _column_type(["1", "2.3", "four"]) is str
1286 True
1287 >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is str
1288 True
1289 >>> _column_type([None, "brux"]) is str
1290 True
1291 >>> _column_type([1, 2, None]) is int
1292 True
1293 >>> import datetime as dt
1294 >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is str
1295 True
1297 """
1298 types = [_type(s, has_invisible, numparse) for s in strings]
1299 return reduce(_more_generic, types, bool)
1302def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True):
1303 """Format a value according to its deduced type. Empty values are deemed valid for any type.
1305 Unicode is supported:
1307 >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \
1308 tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \
1309 good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \
1310 tabulate(tbl, headers=hrow) == good_result
1311 True
1313 """
1314 if val is None:
1315 return missingval
1316 if isinstance(val, (bytes, str)) and not val:
1317 return ""
1319 if valtype is str:
1320 return f"{val}"
1321 elif valtype is int:
1322 if isinstance(val, str):
1323 val_striped = val.encode("unicode_escape").decode("utf-8")
1324 colored = re.search(r"(\\[xX]+[0-9a-fA-F]+\[\d+[mM]+)([0-9.]+)(\\.*)$", val_striped)
1325 if colored:
1326 total_groups = len(colored.groups())
1327 if total_groups == 3:
1328 digits = colored.group(2)
1329 if digits.isdigit():
1330 val_new = colored.group(1) + format(int(digits), intfmt) + colored.group(3)
1331 val = val_new.encode("utf-8").decode("unicode_escape")
1332 intfmt = ""
1333 return format(val, intfmt)
1334 elif valtype is bytes:
1335 try:
1336 return str(val, "ascii")
1337 except (TypeError, UnicodeDecodeError):
1338 return str(val)
1339 elif valtype is float:
1340 is_a_colored_number = has_invisible and isinstance(val, (str, bytes))
1341 if is_a_colored_number:
1342 raw_val = _strip_ansi(val)
1343 try:
1344 formatted_val = format(float(raw_val), floatfmt)
1345 except (ValueError, TypeError):
1346 return f"{val}"
1347 return val.replace(raw_val, formatted_val)
1348 else:
1349 if isinstance(val, str) and "," in val:
1350 val = val.replace(",", "") # handle thousands-separators
1351 if isinstance(val, Decimal):
1352 return format(val, floatfmt)
1353 try:
1354 return format(float(val), floatfmt)
1355 except (ValueError, TypeError):
1356 return f"{val}"
1357 else:
1358 return f"{val}"
1361def _align_header(header, alignment, width, visible_width, is_multiline=False, width_fn=None):
1362 "Pad string header to width chars given known visible_width of the header."
1363 if is_multiline:
1364 header_lines = re.split(_multiline_codes, header)
1365 padded_lines = [_align_header(h, alignment, width, width_fn(h)) for h in header_lines]
1366 return "\n".join(padded_lines)
1367 # else: not multiline
1368 ninvisible = len(header) - visible_width
1369 width += ninvisible
1370 if alignment == "left":
1371 return _padright(width, header)
1372 elif alignment == "center":
1373 return _padboth(width, header)
1374 elif not alignment:
1375 return f"{header}"
1376 else:
1377 return _padleft(width, header)
1380def _remove_separating_lines(rows):
1381 if isinstance(rows, list):
1382 separating_lines = []
1383 sans_rows = []
1384 for index, row in enumerate(rows):
1385 if _is_separating_line(row):
1386 separating_lines.append(index)
1387 else:
1388 sans_rows.append(row)
1389 return sans_rows, separating_lines
1390 else:
1391 return rows, None
1394def _reinsert_separating_lines(rows, separating_lines):
1395 if separating_lines:
1396 for index in separating_lines:
1397 rows.insert(index, SEPARATING_LINE)
1400def _prepend_row_index(rows, index):
1401 """Add a left-most index column."""
1402 if index is None or index is False:
1403 return rows
1404 if isinstance(index, Sized) and len(index) != len(rows):
1405 raise ValueError(
1406 "index must be as long as the number of data rows: "
1407 f"len(index)={len(index)} len(rows)={len(rows)}"
1408 )
1409 sans_rows, separating_lines = _remove_separating_lines(rows)
1410 new_rows = []
1411 index_iter = iter(index)
1412 for row in sans_rows:
1413 index_v = next(index_iter)
1414 new_rows.append([index_v] + list(row))
1415 rows = new_rows
1416 _reinsert_separating_lines(rows, separating_lines)
1417 return rows
1420def _bool(val):
1421 "A wrapper around standard bool() which doesn't throw on NumPy arrays"
1422 try:
1423 return bool(val)
1424 except ValueError: # val is likely to be a numpy array with many elements
1425 return False
1428def _normalize_tabular_data(tabular_data, headers, showindex="default"):
1429 """Transform a supported data type to a list of lists, and a list of headers,
1430 with headers padding.
1432 Supported tabular data types:
1434 * list-of-lists or another iterable of iterables
1436 * list of named tuples (usually used with headers="keys")
1438 * list of dicts (usually used with headers="keys")
1440 * list of OrderedDicts (usually used with headers="keys")
1442 * list of dataclasses (usually used with headers="keys")
1444 * 2D NumPy arrays
1446 * NumPy record arrays (usually used with headers="keys")
1448 * dict of iterables (usually used with headers="keys")
1450 * pandas.DataFrame (usually used with headers="keys")
1452 The first row can be used as headers if headers="firstrow",
1453 column indices can be used as headers if headers="keys".
1455 If showindex="default", show row indices of the pandas.DataFrame.
1456 If showindex="always", show row indices for all types of data.
1457 If showindex="never", don't show row indices for all types of data.
1458 If showindex is an iterable, show its values as row indices.
1460 """
1462 try:
1463 bool(headers)
1464 except ValueError: # numpy.ndarray, pandas.core.index.Index, ...
1465 headers = list(headers)
1467 err_msg = (
1468 "\n\nTo build a table python-tabulate requires two-dimensional data "
1469 "like a list of lists or similar."
1470 "\nDid you forget a pair of extra [] or ',' in ()?"
1471 )
1472 index = None
1473 if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"):
1474 # dict-like and pandas.DataFrame?
1475 if callable(tabular_data.values):
1476 # likely a conventional dict
1477 keys = tabular_data.keys()
1478 try:
1479 rows = list(izip_longest(*tabular_data.values())) # columns have to be transposed
1480 except TypeError as e: # not iterable
1481 raise TypeError(err_msg) from e
1483 elif hasattr(tabular_data, "index"):
1484 # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0)
1485 keys = list(tabular_data)
1486 if showindex in ["default", "always", True] and tabular_data.index.name is not None:
1487 if isinstance(tabular_data.index.name, list):
1488 keys[:0] = tabular_data.index.name
1489 else:
1490 keys[:0] = [tabular_data.index.name]
1491 vals = tabular_data.values # values matrix doesn't need to be transposed
1492 # for DataFrames add an index per default
1493 index = list(tabular_data.index)
1494 rows = [list(row) for row in vals]
1495 else:
1496 raise ValueError("tabular data doesn't appear to be a dict or a DataFrame")
1498 if headers == "keys":
1499 headers = list(map(str, keys)) # headers should be strings
1501 else: # it's a usual iterable of iterables, or a NumPy array, or an iterable of dataclasses
1502 try:
1503 rows = list(tabular_data)
1504 except TypeError as e: # not iterable
1505 raise TypeError(err_msg) from e
1507 if headers == "keys" and not rows:
1508 # an empty table (issue #81)
1509 headers = []
1510 elif headers == "keys" and hasattr(tabular_data, "dtype") and tabular_data.dtype.names:
1511 # numpy record array
1512 headers = tabular_data.dtype.names
1513 elif (
1514 headers == "keys"
1515 and len(rows) > 0
1516 and isinstance(rows[0], tuple)
1517 and hasattr(rows[0], "_fields")
1518 ):
1519 # namedtuple
1520 headers = list(map(str, rows[0]._fields))
1521 elif len(rows) > 0 and hasattr(rows[0], "keys") and hasattr(rows[0], "values"):
1522 # dict-like object
1523 uniq_keys = set() # implements hashed lookup
1524 keys = [] # storage for set
1525 if headers == "firstrow":
1526 firstdict = rows[0] if len(rows) > 0 else {}
1527 keys.extend(firstdict.keys())
1528 uniq_keys.update(keys)
1529 rows = rows[1:]
1530 for row in rows:
1531 for k in row.keys():
1532 # Save unique items in input order
1533 if k not in uniq_keys:
1534 keys.append(k)
1535 uniq_keys.add(k)
1536 if headers == "keys":
1537 headers = keys
1538 elif isinstance(headers, dict):
1539 # a dict of headers for a list of dicts
1540 headers = [headers.get(k, k) for k in keys]
1541 headers = list(map(str, headers))
1542 elif headers == "firstrow":
1543 if len(rows) > 0:
1544 headers = [firstdict.get(k, k) for k in keys]
1545 headers = list(map(str, headers))
1546 else:
1547 headers = []
1548 elif headers:
1549 raise ValueError("headers for a list of dicts is not a dict or a keyword")
1550 rows = [[row.get(k) for k in keys] for row in rows]
1552 elif (
1553 headers == "keys"
1554 and hasattr(tabular_data, "description")
1555 and hasattr(tabular_data, "fetchone")
1556 and hasattr(tabular_data, "rowcount")
1557 ):
1558 # Python Database API cursor object (PEP 0249)
1559 # print tabulate(cursor, headers='keys')
1560 headers = [column[0] for column in tabular_data.description]
1562 elif dataclasses is not None and len(rows) > 0 and dataclasses.is_dataclass(rows[0]):
1563 # Python's dataclass
1564 field_names = [field.name for field in dataclasses.fields(rows[0])]
1565 if headers == "keys":
1566 headers = field_names
1567 rows = [
1568 ([getattr(row, f) for f in field_names] if not _is_separating_line(row) else row)
1569 for row in rows
1570 ]
1572 elif headers == "keys" and len(rows) > 0:
1573 # keys are column indices
1574 headers = list(map(str, range(len(rows[0]))))
1576 # take headers from the first row if necessary
1577 if headers == "firstrow" and len(rows) > 0:
1578 if index is not None:
1579 headers = [index[0]] + list(rows[0])
1580 index = index[1:]
1581 else:
1582 headers = rows[0]
1583 headers = list(map(str, headers)) # headers should be strings
1584 rows = rows[1:]
1585 elif headers == "firstrow":
1586 headers = []
1588 headers = list(map(str, headers))
1589 # rows = list(map(list, rows))
1590 rows = [r if _is_separating_line(r) else list(r) for r in rows]
1592 # add or remove an index column
1593 showindex_is_a_str = type(showindex) in [str, bytes]
1594 if showindex_is_a_str and showindex == "default" and index is not None:
1595 rows = _prepend_row_index(rows, index)
1596 elif isinstance(showindex, Sized) and not showindex_is_a_str:
1597 rows = _prepend_row_index(rows, list(showindex))
1598 elif isinstance(showindex, Iterable) and not showindex_is_a_str:
1599 rows = _prepend_row_index(rows, showindex)
1600 elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str):
1601 if index is None:
1602 index = list(range(len(rows)))
1603 rows = _prepend_row_index(rows, index)
1604 elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str):
1605 pass
1607 # pad with empty headers for initial columns if necessary
1608 headers_pad = 0
1609 if headers and len(rows) > 0:
1610 headers_pad = max(0, len(rows[0]) - len(headers))
1611 headers = [""] * headers_pad + headers
1613 return rows, headers, headers_pad
1616def _wrap_text_to_colwidths(
1617 list_of_lists,
1618 colwidths,
1619 numparses=True,
1620 missingval=_DEFAULT_MISSINGVAL,
1621 break_long_words=_BREAK_LONG_WORDS,
1622 break_on_hyphens=_BREAK_ON_HYPHENS,
1623):
1624 if len(list_of_lists):
1625 num_cols = len(list_of_lists[0])
1626 else:
1627 num_cols = 0
1628 numparses = _expand_iterable(numparses, num_cols, True)
1630 result = []
1632 for row in list_of_lists:
1633 new_row = []
1634 for cell, width, numparse in zip(row, colwidths, numparses):
1635 if _isnumber(cell) and numparse:
1636 new_row.append(cell)
1637 continue
1639 if width is not None:
1640 wrapper = _CustomTextWrap(
1641 width=width,
1642 break_long_words=break_long_words,
1643 break_on_hyphens=break_on_hyphens,
1644 )
1645 # Cast based on our internal type handling. Any future custom
1646 # formatting of types (such as datetimes) may need to be more
1647 # explicit than just `str` of the object. Also doesn't work for
1648 # custom floatfmt/intfmt, nor with any missing/blank cells.
1649 casted_cell = (
1650 missingval
1651 if cell is None
1652 else (
1653 str(cell)
1654 if cell == "" or _isnumber(cell)
1655 else str(_type(cell, numparse)(cell))
1656 )
1657 )
1658 wrapped = [
1659 "\n".join(wrapper.wrap(line))
1660 for line in casted_cell.splitlines()
1661 if line.strip() != ""
1662 ]
1663 new_row.append("\n".join(wrapped))
1664 else:
1665 new_row.append(cell)
1666 result.append(new_row)
1668 return result
1671def _to_str(s, encoding="utf8", errors="ignore"):
1672 """
1673 A type safe wrapper for converting a bytestring to str. This is essentially just
1674 a wrapper around .decode() intended for use with things like map(), but with some
1675 specific behavior:
1677 1. if the given parameter is not a bytestring, it is returned unmodified
1678 2. decode() is called for the given parameter and assumes utf8 encoding, but the
1679 default error behavior is changed from 'strict' to 'ignore'
1681 >>> repr(_to_str(b'foo'))
1682 "'foo'"
1684 >>> repr(_to_str('foo'))
1685 "'foo'"
1687 >>> repr(_to_str(42))
1688 "'42'"
1690 """
1691 if isinstance(s, bytes):
1692 return s.decode(encoding=encoding, errors=errors)
1693 return str(s)
1696def tabulate(
1697 tabular_data,
1698 headers=(),
1699 tablefmt="simple",
1700 floatfmt=_DEFAULT_FLOATFMT,
1701 intfmt=_DEFAULT_INTFMT,
1702 numalign=_DEFAULT_ALIGN,
1703 stralign=_DEFAULT_ALIGN,
1704 missingval=_DEFAULT_MISSINGVAL,
1705 showindex="default",
1706 disable_numparse=False,
1707 colglobalalign=None,
1708 colalign=None,
1709 preserve_whitespace=False,
1710 maxcolwidths=None,
1711 headersglobalalign=None,
1712 headersalign=None,
1713 rowalign=None,
1714 maxheadercolwidths=None,
1715 break_long_words=_BREAK_LONG_WORDS,
1716 break_on_hyphens=_BREAK_ON_HYPHENS,
1717):
1718 """Format a fixed width table for pretty printing.
1720 >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]]))
1721 --- ---------
1722 1 2.34
1723 -56 8.999
1724 2 10001
1725 --- ---------
1727 The first required argument (`tabular_data`) can be a
1728 list-of-lists (or another iterable of iterables), a list of named
1729 tuples, a dictionary of iterables, an iterable of dictionaries,
1730 an iterable of dataclasses, a two-dimensional NumPy array,
1731 NumPy record array, or a Pandas' dataframe.
1734 Table headers
1735 -------------
1737 To print nice column headers, supply the second argument (`headers`):
1739 - `headers` can be an explicit list of column headers
1740 - if `headers="firstrow"`, then the first row of data is used
1741 - if `headers="keys"`, then dictionary keys or column indices are used
1743 Otherwise a headerless table is produced.
1745 If the number of headers is less than the number of columns, they
1746 are supposed to be names of the last columns. This is consistent
1747 with the plain-text format of R and Pandas' dataframes.
1749 >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]],
1750 ... headers="firstrow"))
1751 sex age
1752 ----- ----- -----
1753 Alice F 24
1754 Bob M 19
1756 By default, pandas.DataFrame data have an additional column called
1757 row index. To add a similar column to all other types of data,
1758 use `showindex="always"` or `showindex=True`. To suppress row indices
1759 for all types of data, pass `showindex="never" or `showindex=False`.
1760 To add a custom row index column, pass `showindex=some_iterable`.
1762 >>> print(tabulate([["F",24],["M",19]], showindex="always"))
1763 - - --
1764 0 F 24
1765 1 M 19
1766 - - --
1769 Column and Headers alignment
1770 ----------------------------
1772 `tabulate` tries to detect column types automatically, and aligns
1773 the values properly. By default it aligns decimal points of the
1774 numbers (or flushes integer numbers to the right), and flushes
1775 everything else to the left. Possible column alignments
1776 (`numalign`, `stralign`) are: "right", "center", "left", "decimal"
1777 (only for `numalign`), and None (to disable alignment).
1779 `colglobalalign` allows for global alignment of columns, before any
1780 specific override from `colalign`. Possible values are: None
1781 (defaults according to coltype), "right", "center", "decimal",
1782 "left".
1783 `colalign` allows for column-wise override starting from left-most
1784 column. Possible values are: "global" (no override), "right",
1785 "center", "decimal", "left".
1786 `headersglobalalign` allows for global headers alignment, before any
1787 specific override from `headersalign`. Possible values are: None
1788 (follow columns alignment), "right", "center", "left".
1789 `headersalign` allows for header-wise override starting from left-most
1790 given header. Possible values are: "global" (no override), "same"
1791 (follow column alignment), "right", "center", "left".
1793 Note on intended behaviour: If there is no `tabular_data`, any column
1794 alignment argument is ignored. Hence, in this case, header
1795 alignment cannot be inferred from column alignment.
1797 Table formats
1798 -------------
1800 `intfmt` is a format specification used for columns which
1801 contain numeric data without a decimal point. This can also be
1802 a list or tuple of format strings, one per column.
1804 `floatfmt` is a format specification used for columns which
1805 contain numeric data with a decimal point. This can also be
1806 a list or tuple of format strings, one per column.
1808 `None` values are replaced with a `missingval` string (like
1809 `floatfmt`, this can also be a list of values for different
1810 columns):
1812 >>> print(tabulate([["spam", 1, None],
1813 ... ["eggs", 42, 3.14],
1814 ... ["other", None, 2.7]], missingval="?"))
1815 ----- -- ----
1816 spam 1 ?
1817 eggs 42 3.14
1818 other ? 2.7
1819 ----- -- ----
1821 Various plain-text table formats (`tablefmt`) are supported:
1822 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki',
1823 'latex', 'latex_raw', 'latex_booktabs', 'latex_longtable' and tsv.
1824 Variable `tabulate_formats`contains the list of currently supported formats.
1826 "plain" format doesn't use any pseudographics to draw tables,
1827 it separates columns with a double space:
1829 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1830 ... ["strings", "numbers"], "plain"))
1831 strings numbers
1832 spam 41.9999
1833 eggs 451
1835 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain"))
1836 spam 41.9999
1837 eggs 451
1839 "simple" format is like Pandoc simple_tables:
1841 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1842 ... ["strings", "numbers"], "simple"))
1843 strings numbers
1844 --------- ---------
1845 spam 41.9999
1846 eggs 451
1848 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple"))
1849 ---- --------
1850 spam 41.9999
1851 eggs 451
1852 ---- --------
1854 "grid" is similar to tables produced by Emacs table.el package or
1855 Pandoc grid_tables:
1857 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1858 ... ["strings", "numbers"], "grid"))
1859 +-----------+-----------+
1860 | strings | numbers |
1861 +===========+===========+
1862 | spam | 41.9999 |
1863 +-----------+-----------+
1864 | eggs | 451 |
1865 +-----------+-----------+
1867 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid"))
1868 +------+----------+
1869 | spam | 41.9999 |
1870 +------+----------+
1871 | eggs | 451 |
1872 +------+----------+
1874 "simple_grid" draws a grid using single-line box-drawing
1875 characters:
1877 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1878 ... ["strings", "numbers"], "simple_grid"))
1879 ┌───────────┬───────────┐
1880 │ strings │ numbers │
1881 ├───────────┼───────────┤
1882 │ spam │ 41.9999 │
1883 ├───────────┼───────────┤
1884 │ eggs │ 451 │
1885 └───────────┴───────────┘
1887 "rounded_grid" draws a grid using single-line box-drawing
1888 characters with rounded corners:
1890 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1891 ... ["strings", "numbers"], "rounded_grid"))
1892 ╭───────────┬───────────╮
1893 │ strings │ numbers │
1894 ├───────────┼───────────┤
1895 │ spam │ 41.9999 │
1896 ├───────────┼───────────┤
1897 │ eggs │ 451 │
1898 ╰───────────┴───────────╯
1900 "heavy_grid" draws a grid using bold (thick) single-line box-drawing
1901 characters:
1903 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1904 ... ["strings", "numbers"], "heavy_grid"))
1905 ┏━━━━━━━━━━━┳━━━━━━━━━━━┓
1906 ┃ strings ┃ numbers ┃
1907 ┣━━━━━━━━━━━╋━━━━━━━━━━━┫
1908 ┃ spam ┃ 41.9999 ┃
1909 ┣━━━━━━━━━━━╋━━━━━━━━━━━┫
1910 ┃ eggs ┃ 451 ┃
1911 ┗━━━━━━━━━━━┻━━━━━━━━━━━┛
1913 "mixed_grid" draws a grid using a mix of light (thin) and heavy (thick) lines
1914 box-drawing characters:
1916 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1917 ... ["strings", "numbers"], "mixed_grid"))
1918 ┍━━━━━━━━━━━┯━━━━━━━━━━━┑
1919 │ strings │ numbers │
1920 ┝━━━━━━━━━━━┿━━━━━━━━━━━┥
1921 │ spam │ 41.9999 │
1922 ├───────────┼───────────┤
1923 │ eggs │ 451 │
1924 ┕━━━━━━━━━━━┷━━━━━━━━━━━┙
1926 "double_grid" draws a grid using double-line box-drawing
1927 characters:
1929 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1930 ... ["strings", "numbers"], "double_grid"))
1931 ╔═══════════╦═══════════╗
1932 ║ strings ║ numbers ║
1933 ╠═══════════╬═══════════╣
1934 ║ spam ║ 41.9999 ║
1935 ╠═══════════╬═══════════╣
1936 ║ eggs ║ 451 ║
1937 ╚═══════════╩═══════════╝
1939 "fancy_grid" draws a grid using a mix of single and
1940 double-line box-drawing characters:
1942 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1943 ... ["strings", "numbers"], "fancy_grid"))
1944 ╒═══════════╤═══════════╕
1945 │ strings │ numbers │
1946 ╞═══════════╪═══════════╡
1947 │ spam │ 41.9999 │
1948 ├───────────┼───────────┤
1949 │ eggs │ 451 │
1950 ╘═══════════╧═══════════╛
1952 "colon_grid" is similar to "grid" but uses colons only to define
1953 columnwise content alignment, without whitespace padding,
1954 similar to the alignment specification of Pandoc `grid_tables`:
1956 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1957 ... ["strings", "numbers"], "colon_grid"))
1958 +-----------+-----------+
1959 | strings | numbers |
1960 +:==========+:==========+
1961 | spam | 41.9999 |
1962 +-----------+-----------+
1963 | eggs | 451 |
1964 +-----------+-----------+
1966 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1967 ... ["strings", "numbers"], "colon_grid",
1968 ... colalign=["right", "left"]))
1969 +-----------+-----------+
1970 | strings | numbers |
1971 +==========:+:==========+
1972 | spam | 41.9999 |
1973 +-----------+-----------+
1974 | eggs | 451 |
1975 +-----------+-----------+
1977 "outline" is the same as the "grid" format but doesn't draw lines between rows:
1979 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1980 ... ["strings", "numbers"], "outline"))
1981 +-----------+-----------+
1982 | strings | numbers |
1983 +===========+===========+
1984 | spam | 41.9999 |
1985 | eggs | 451 |
1986 +-----------+-----------+
1988 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="outline"))
1989 +------+----------+
1990 | spam | 41.9999 |
1991 | eggs | 451 |
1992 +------+----------+
1994 "simple_outline" is the same as the "simple_grid" format but doesn't draw lines between rows:
1996 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
1997 ... ["strings", "numbers"], "simple_outline"))
1998 ┌───────────┬───────────┐
1999 │ strings │ numbers │
2000 ├───────────┼───────────┤
2001 │ spam │ 41.9999 │
2002 │ eggs │ 451 │
2003 └───────────┴───────────┘
2005 "rounded_outline" is the same as the "rounded_grid" format but doesn't draw lines between rows:
2007 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2008 ... ["strings", "numbers"], "rounded_outline"))
2009 ╭───────────┬───────────╮
2010 │ strings │ numbers │
2011 ├───────────┼───────────┤
2012 │ spam │ 41.9999 │
2013 │ eggs │ 451 │
2014 ╰───────────┴───────────╯
2016 "heavy_outline" is the same as the "heavy_grid" format but doesn't draw lines between rows:
2018 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2019 ... ["strings", "numbers"], "heavy_outline"))
2020 ┏━━━━━━━━━━━┳━━━━━━━━━━━┓
2021 ┃ strings ┃ numbers ┃
2022 ┣━━━━━━━━━━━╋━━━━━━━━━━━┫
2023 ┃ spam ┃ 41.9999 ┃
2024 ┃ eggs ┃ 451 ┃
2025 ┗━━━━━━━━━━━┻━━━━━━━━━━━┛
2027 "mixed_outline" is the same as the "mixed_grid" format but doesn't draw lines between rows:
2029 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2030 ... ["strings", "numbers"], "mixed_outline"))
2031 ┍━━━━━━━━━━━┯━━━━━━━━━━━┑
2032 │ strings │ numbers │
2033 ┝━━━━━━━━━━━┿━━━━━━━━━━━┥
2034 │ spam │ 41.9999 │
2035 │ eggs │ 451 │
2036 ┕━━━━━━━━━━━┷━━━━━━━━━━━┙
2038 "double_outline" is the same as the "double_grid" format but doesn't draw lines between rows:
2040 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2041 ... ["strings", "numbers"], "double_outline"))
2042 ╔═══════════╦═══════════╗
2043 ║ strings ║ numbers ║
2044 ╠═══════════╬═══════════╣
2045 ║ spam ║ 41.9999 ║
2046 ║ eggs ║ 451 ║
2047 ╚═══════════╩═══════════╝
2049 "fancy_outline" is the same as the "fancy_grid" format but doesn't draw lines between rows:
2051 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2052 ... ["strings", "numbers"], "fancy_outline"))
2053 ╒═══════════╤═══════════╕
2054 │ strings │ numbers │
2055 ╞═══════════╪═══════════╡
2056 │ spam │ 41.9999 │
2057 │ eggs │ 451 │
2058 ╘═══════════╧═══════════╛
2060 "pipe" is like tables in PHP Markdown Extra extension or Pandoc
2061 pipe_tables:
2063 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2064 ... ["strings", "numbers"], "pipe"))
2065 | strings | numbers |
2066 |:----------|----------:|
2067 | spam | 41.9999 |
2068 | eggs | 451 |
2070 "presto" is like tables produce by the Presto CLI:
2072 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2073 ... ["strings", "numbers"], "presto"))
2074 strings | numbers
2075 -----------+-----------
2076 spam | 41.9999
2077 eggs | 451
2079 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe"))
2080 |:-----|---------:|
2081 | spam | 41.9999 |
2082 | eggs | 451 |
2084 "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They
2085 are slightly different from "pipe" format by not using colons to
2086 define column alignment, and using a "+" sign to indicate line
2087 intersections:
2089 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2090 ... ["strings", "numbers"], "orgtbl"))
2091 | strings | numbers |
2092 |-----------+-----------|
2093 | spam | 41.9999 |
2094 | eggs | 451 |
2097 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl"))
2098 | spam | 41.9999 |
2099 | eggs | 451 |
2101 "rst" is like a simple table format from reStructuredText; please
2102 note that reStructuredText accepts also "grid" tables:
2104 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
2105 ... ["strings", "numbers"], "rst"))
2106 ========= =========
2107 strings numbers
2108 ========= =========
2109 spam 41.9999
2110 eggs 451
2111 ========= =========
2113 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst"))
2114 ==== ========
2115 spam 41.9999
2116 eggs 451
2117 ==== ========
2119 "mediawiki" produces a table markup used in Wikipedia and on other
2120 MediaWiki-based sites:
2122 >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
2123 ... headers="firstrow", tablefmt="mediawiki"))
2124 {| class="wikitable" style="text-align: left;"
2125 |+ <!-- caption -->
2126 |-
2127 ! strings !! style="text-align: right;"| numbers
2128 |-
2129 | spam || style="text-align: right;"| 41.9999
2130 |-
2131 | eggs || style="text-align: right;"| 451
2132 |}
2134 "html" produces HTML markup as an html.escape'd str
2135 with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML
2136 and a .str property so that the raw HTML remains accessible
2137 the unsafehtml table format can be used if an unescaped HTML format is required:
2139 >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
2140 ... headers="firstrow", tablefmt="html"))
2141 <table>
2142 <thead>
2143 <tr><th style="text-align: left;">strings </th><th style="text-align: right;"> numbers</th></tr>
2144 </thead>
2145 <tbody>
2146 <tr><td style="text-align: left;">spam </td><td style="text-align: right;"> 41.9999</td></tr>
2147 <tr><td style="text-align: left;">eggs </td><td style="text-align: right;"> 451 </td></tr>
2148 </tbody>
2149 </table>
2151 "latex" produces a tabular environment of LaTeX document markup:
2153 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex"))
2154 \\begin{tabular}{lr}
2155 \\hline
2156 spam & 41.9999 \\\\
2157 eggs & 451 \\\\
2158 \\hline
2159 \\end{tabular}
2161 "latex_raw" is similar to "latex", but doesn't escape special characters,
2162 such as backslash and underscore, so LaTeX commands may embedded into
2163 cells' values:
2165 >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], tablefmt="latex_raw"))
2166 \\begin{tabular}{lr}
2167 \\hline
2168 spam$_9$ & 41.9999 \\\\
2169 \\emph{eggs} & 451 \\\\
2170 \\hline
2171 \\end{tabular}
2173 "latex_booktabs" produces a tabular environment of LaTeX document markup
2174 using the booktabs.sty package:
2176 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs"))
2177 \\begin{tabular}{lr}
2178 \\toprule
2179 spam & 41.9999 \\\\
2180 eggs & 451 \\\\
2181 \\bottomrule
2182 \\end{tabular}
2184 "latex_longtable" produces a tabular environment that can stretch along
2185 multiple pages, using the longtable package for LaTeX.
2187 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_longtable"))
2188 \\begin{longtable}{lr}
2189 \\hline
2190 spam & 41.9999 \\\\
2191 eggs & 451 \\\\
2192 \\hline
2193 \\end{longtable}
2196 Number parsing
2197 --------------
2198 By default, anything which can be parsed as a number is a number.
2199 This ensures numbers represented as strings are aligned properly.
2200 This can lead to weird results for particular strings such as
2201 specific git SHAs e.g. "42992e1" will be parsed into the number
2202 429920 and aligned as such.
2204 To completely disable number parsing (and alignment), use
2205 `disable_numparse=True`. For more fine grained control, a list column
2206 indices is used to disable number parsing only on those columns
2207 e.g. `disable_numparse=[0, 2]` would disable number parsing only on the
2208 first and third columns.
2210 Column Widths and Auto Line Wrapping
2211 ------------------------------------
2212 Tabulate will, by default, set the width of each column to the length of the
2213 longest element in that column. However, in situations where fields are expected
2214 to reasonably be too long to look good as a single line, tabulate can help automate
2215 word wrapping long fields for you. Use the parameter `maxcolwidths` to provide a
2216 list of maximal column widths:
2218 >>> print(tabulate( \
2219 [('1', 'John Smith', \
2220 'This is a rather long description that might look better if it is wrapped a bit')], \
2221 headers=("Issue Id", "Author", "Description"), \
2222 maxcolwidths=[None, None, 30], \
2223 tablefmt="grid" \
2224 ))
2225 +------------+------------+-------------------------------+
2226 | Issue Id | Author | Description |
2227 +============+============+===============================+
2228 | 1 | John Smith | This is a rather long |
2229 | | | description that might look |
2230 | | | better if it is wrapped a bit |
2231 +------------+------------+-------------------------------+
2233 Header column width can be specified in a similar way using `maxheadercolwidths`.
2235 """
2237 if tabular_data is None:
2238 tabular_data = []
2240 list_of_lists, headers, headers_pad = _normalize_tabular_data(
2241 tabular_data, headers, showindex=showindex
2242 )
2243 list_of_lists, separating_lines = _remove_separating_lines(list_of_lists)
2245 if maxcolwidths is not None:
2246 if type(maxcolwidths) is tuple: # Check if tuple, convert to list if so
2247 maxcolwidths = list(maxcolwidths)
2248 if len(list_of_lists):
2249 num_cols = len(list_of_lists[0])
2250 else:
2251 num_cols = 0
2252 if isinstance(maxcolwidths, int): # Expand scalar for all columns
2253 maxcolwidths = _expand_iterable(maxcolwidths, num_cols, maxcolwidths)
2254 else: # Ignore col width for any 'trailing' columns
2255 maxcolwidths = _expand_iterable(maxcolwidths, num_cols, None)
2257 numparses = _expand_numparse(disable_numparse, num_cols)
2258 list_of_lists = _wrap_text_to_colwidths(
2259 list_of_lists,
2260 maxcolwidths,
2261 numparses=numparses,
2262 missingval=missingval,
2263 break_long_words=break_long_words,
2264 break_on_hyphens=break_on_hyphens,
2265 )
2267 if maxheadercolwidths is not None:
2268 num_cols = len(list_of_lists[0]) if list_of_lists else len(headers)
2269 if isinstance(maxheadercolwidths, int): # Expand scalar for all columns
2270 maxheadercolwidths = _expand_iterable(maxheadercolwidths, num_cols, maxheadercolwidths)
2271 else: # Ignore col width for any 'trailing' columns
2272 maxheadercolwidths = _expand_iterable(maxheadercolwidths, num_cols, None)
2274 numparses = _expand_numparse(disable_numparse, num_cols)
2275 headers = _wrap_text_to_colwidths(
2276 [headers],
2277 maxheadercolwidths,
2278 numparses=numparses,
2279 missingval=missingval,
2280 break_long_words=break_long_words,
2281 break_on_hyphens=break_on_hyphens,
2282 )[0]
2284 # empty values in the first column of RST tables should be escaped (issue #82)
2285 # "" should be escaped as "\\ " or ".."
2286 if tablefmt == "rst":
2287 list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers)
2289 # PrettyTable formatting does not use any extra padding.
2290 # Numbers are not parsed and are treated the same as strings for alignment.
2291 # Check if pretty is the format being used and override the defaults so it
2292 # does not impact other formats.
2293 min_padding = MIN_PADDING
2294 if tablefmt == "pretty":
2295 min_padding = 0
2296 disable_numparse = True
2297 numalign = "center" if numalign == _DEFAULT_ALIGN else numalign
2298 stralign = "center" if stralign == _DEFAULT_ALIGN else stralign
2299 else:
2300 numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign
2301 stralign = "left" if stralign == _DEFAULT_ALIGN else stralign
2303 # 'colon_grid' uses colons in the line beneath the header to represent a column's
2304 # alignment instead of literally aligning the text differently. Hence,
2305 # left alignment of the data in the text output is enforced.
2306 if tablefmt == "colon_grid":
2307 colglobalalign = "left"
2308 headersglobalalign = "left"
2310 # optimization: look for ANSI control codes once,
2311 # enable smart width functions only if a control code is found
2312 #
2313 # convert the headers and rows into a single, tab-delimited string ensuring
2314 # that any bytestrings are decoded safely (i.e. errors ignored)
2315 plain_text = "\t".join(
2316 chain(
2317 # headers
2318 map(_to_str, headers),
2319 # rows: chain the rows together into a single iterable after mapping
2320 # the bytestring conversino to each cell value
2321 chain.from_iterable(map(_to_str, row) for row in list_of_lists),
2322 )
2323 )
2325 has_invisible = _ansi_codes.search(plain_text) is not None
2327 enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
2328 if (
2329 not isinstance(tablefmt, TableFormat)
2330 and tablefmt in multiline_formats
2331 and _is_multiline(plain_text)
2332 ):
2333 tablefmt = multiline_formats.get(tablefmt, tablefmt)
2334 is_multiline = True
2335 else:
2336 is_multiline = False
2337 width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline)
2339 # format rows and columns, convert numeric values to strings
2340 cols = list(izip_longest(*list_of_lists))
2341 numparses = _expand_numparse(disable_numparse, len(cols))
2342 coltypes = [_column_type(col, numparse=np) for col, np in zip(cols, numparses)]
2343 if isinstance(floatfmt, str): # old version
2344 float_formats = len(cols) * [floatfmt] # just duplicate the string to use in each column
2345 else: # if floatfmt is list, tuple etc we have one per column
2346 float_formats = list(floatfmt)
2347 if len(float_formats) < len(cols):
2348 float_formats.extend((len(cols) - len(float_formats)) * [_DEFAULT_FLOATFMT])
2349 if isinstance(intfmt, str): # old version
2350 int_formats = len(cols) * [intfmt] # just duplicate the string to use in each column
2351 else: # if intfmt is list, tuple etc we have one per column
2352 int_formats = list(intfmt)
2353 if len(int_formats) < len(cols):
2354 int_formats.extend((len(cols) - len(int_formats)) * [_DEFAULT_INTFMT])
2355 if isinstance(missingval, str):
2356 missing_vals = len(cols) * [missingval]
2357 else:
2358 missing_vals = list(missingval)
2359 if len(missing_vals) < len(cols):
2360 missing_vals.extend((len(cols) - len(missing_vals)) * [_DEFAULT_MISSINGVAL])
2361 cols = [
2362 [_format(v, ct, fl_fmt, int_fmt, miss_v, has_invisible) for v in c]
2363 for c, ct, fl_fmt, int_fmt, miss_v in zip(
2364 cols, coltypes, float_formats, int_formats, missing_vals
2365 )
2366 ]
2368 # align columns
2369 # first set global alignment
2370 if colglobalalign is not None: # if global alignment provided
2371 aligns = [colglobalalign] * len(cols)
2372 else: # default
2373 aligns = [numalign if ct in [int, float] else stralign for ct in coltypes]
2374 # then specific alignments
2375 if colalign is not None:
2376 assert isinstance(colalign, Iterable)
2377 if isinstance(colalign, str):
2378 warnings.warn(
2379 f"As a string, `colalign` is interpreted as {list(colalign)}. "
2380 f'Did you mean `colglobalalign = "{colalign}"` or `colalign = ("{colalign}",)`?',
2381 stacklevel=2,
2382 )
2383 for idx, align in enumerate(colalign):
2384 if not idx < len(aligns):
2385 break
2386 elif align != "global":
2387 aligns[idx] = align
2388 minwidths = [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols)
2389 aligns_copy = aligns.copy()
2390 # Reset alignments in copy of alignments list to "left" for 'colon_grid' format,
2391 # which enforces left alignment in the text output of the data.
2392 if tablefmt == "colon_grid":
2393 aligns_copy = ["left"] * len(cols)
2394 cols = [
2395 _align_column(
2396 c,
2397 a,
2398 minw,
2399 has_invisible,
2400 enable_widechars,
2401 is_multiline,
2402 preserve_whitespace,
2403 )
2404 for c, a, minw in zip(cols, aligns_copy, minwidths)
2405 ]
2407 aligns_headers = None
2408 if headers:
2409 # align headers and add headers
2410 t_cols = cols or [[""]] * len(headers)
2411 # first set global alignment
2412 if headersglobalalign is not None: # if global alignment provided
2413 aligns_headers = [headersglobalalign] * len(t_cols)
2414 else: # default
2415 aligns_headers = aligns or [stralign] * len(headers)
2416 # then specific header alignments
2417 if headersalign is not None:
2418 assert isinstance(headersalign, Iterable)
2419 if isinstance(headersalign, str):
2420 warnings.warn(
2421 f"As a string, `headersalign` is interpreted as {list(headersalign)}. "
2422 f'Did you mean `headersglobalalign = "{headersalign}"` '
2423 f'or `headersalign = ("{headersalign}",)`?',
2424 stacklevel=2,
2425 )
2426 for idx, align in enumerate(headersalign):
2427 hidx = headers_pad + idx
2428 if not hidx < len(aligns_headers):
2429 break
2430 elif align == "same" and hidx < len(aligns): # same as column align
2431 aligns_headers[hidx] = aligns[hidx]
2432 elif align != "global":
2433 aligns_headers[hidx] = align
2434 minwidths = [
2435 max(minw, max(width_fn(cl) for cl in c)) for minw, c in zip(minwidths, t_cols)
2436 ]
2437 headers = [
2438 _align_header(h, a, minw, width_fn(h), is_multiline, width_fn)
2439 for h, a, minw in zip(headers, aligns_headers, minwidths)
2440 ]
2441 rows = list(zip(*cols))
2442 else:
2443 minwidths = [max(width_fn(cl) for cl in c) for c in cols]
2444 rows = list(zip(*cols))
2446 if not isinstance(tablefmt, TableFormat):
2447 tablefmt = _table_formats.get(tablefmt, _table_formats["simple"])
2449 ra_default = rowalign if isinstance(rowalign, str) else None
2450 rowaligns = _expand_iterable(rowalign, len(rows), ra_default)
2451 _reinsert_separating_lines(rows, separating_lines)
2453 return _format_table(
2454 tablefmt,
2455 headers,
2456 aligns_headers,
2457 rows,
2458 minwidths,
2459 aligns,
2460 is_multiline,
2461 rowaligns=rowaligns,
2462 )
2465def _expand_numparse(disable_numparse, column_count):
2466 """
2467 Return a list of bools of length `column_count` which indicates whether
2468 number parsing should be used on each column.
2469 If `disable_numparse` is a list of indices, each of those indices are False,
2470 and everything else is True.
2471 If `disable_numparse` is a bool, then the returned list is all the same.
2472 """
2473 if isinstance(disable_numparse, Iterable):
2474 numparses = [True] * column_count
2475 for index in disable_numparse:
2476 numparses[index] = False
2477 return numparses
2478 else:
2479 return [not disable_numparse] * column_count
2482def _expand_iterable(original, num_desired, default):
2483 """
2484 Expands the `original` argument to return a return a list of
2485 length `num_desired`. If `original` is shorter than `num_desired`, it will
2486 be padded with the value in `default`.
2487 If `original` is not a list to begin with (i.e. scalar value) a list of
2488 length `num_desired` completely populated with `default will be returned
2489 """
2490 if isinstance(original, Iterable) and not isinstance(original, str):
2491 return original + [default] * (num_desired - len(original))
2492 else:
2493 return [default] * num_desired
2496def _pad_row(cells, padding):
2497 if cells:
2498 if cells == SEPARATING_LINE:
2499 return SEPARATING_LINE
2500 pad = " " * padding
2501 padded_cells = [pad + cell + pad for cell in cells]
2502 return padded_cells
2503 else:
2504 return cells
2507def _build_simple_row(padded_cells: list[list], rowfmt: DataRow) -> str:
2508 "Format row according to DataRow format without padding."
2509 begin = rowfmt.begin
2510 sep = rowfmt.sep
2511 end = rowfmt.end
2512 escape_map: dict = rowfmt.escape_map
2514 if escape_map:
2516 def escape_char(c):
2517 return escape_map.get(c, c)
2519 escaped_cells = ["".join(map(escape_char, cell)) for cell in padded_cells]
2520 else:
2521 escaped_cells = padded_cells
2523 return (begin + sep.join(escaped_cells) + end).rstrip()
2526def _build_row(
2527 padded_cells: list[list],
2528 colwidths: list[int],
2529 colaligns: list[str],
2530 rowfmt: DataRow | Callable,
2531) -> str:
2532 "Return a string which represents a row of data cells."
2533 if not rowfmt:
2534 return None
2535 if callable(rowfmt):
2536 return rowfmt(padded_cells, colwidths, colaligns)
2537 else:
2538 return _build_simple_row(padded_cells, rowfmt)
2541def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt, rowalign=None):
2542 # NOTE: rowalign is ignored and exists for api compatibility with _append_multiline_row
2543 lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt))
2544 return lines
2547def _align_cell_veritically(text_lines, num_lines, column_width, row_alignment):
2548 delta_lines = num_lines - len(text_lines)
2549 blank = [" " * column_width]
2550 if row_alignment == "bottom":
2551 return blank * delta_lines + text_lines
2552 elif row_alignment == "center":
2553 top_delta = delta_lines // 2
2554 bottom_delta = delta_lines - top_delta
2555 return top_delta * blank + text_lines + bottom_delta * blank
2556 else:
2557 return text_lines + blank * delta_lines
2560def _append_multiline_row(
2561 lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad, rowalign=None
2562):
2563 colwidths = [w - 2 * pad for w in padded_widths]
2564 cells_lines = [c.splitlines() for c in padded_multiline_cells]
2565 nlines = max(map(len, cells_lines)) # number of lines in the row
2566 # vertically pad cells where some lines are missing
2567 # cells_lines = [
2568 # (cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths)
2569 # ]
2571 cells_lines = [
2572 _align_cell_veritically(cl, nlines, w, rowalign) for cl, w in zip(cells_lines, colwidths)
2573 ]
2574 lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)]
2575 for ln in lines_cells:
2576 padded_ln = _pad_row(ln, pad)
2577 _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt)
2578 return lines
2581def _build_line(colwidths, colaligns, linefmt):
2582 "Return a string which represents a horizontal line."
2583 if not linefmt:
2584 return None
2585 if callable(linefmt):
2586 return linefmt(colwidths, colaligns)
2587 else:
2588 begin, fill, sep, end = linefmt
2589 cells = [fill * w for w in colwidths]
2590 rowfmt = DataRow(begin, sep, end)
2591 return _build_simple_row(cells, rowfmt)
2594def _append_line(lines, colwidths, colaligns, linefmt):
2595 lines.append(_build_line(colwidths, colaligns, linefmt))
2596 return lines
2599class JupyterHTMLStr(str):
2600 """Wrap the string with a _repr_html_ method so that Jupyter
2601 displays the HTML table"""
2603 def _repr_html_(self):
2604 return self
2606 @property
2607 def str(self):
2608 """add a .str property so that the raw string is still accessible"""
2609 return self
2612def _format_table(
2613 fmt, headers, headersaligns, rows, colwidths, colaligns, is_multiline, rowaligns
2614):
2615 """Produce a plain-text representation of the table."""
2616 lines = []
2617 hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
2618 pad = fmt.padding
2619 headerrow = fmt.headerrow
2621 padded_widths = [(w + 2 * pad) for w in colwidths]
2622 if is_multiline:
2623 pad_row = lambda row, _: row # noqa: E731 # do it later, in _append_multiline_row
2624 append_row = partial(_append_multiline_row, pad=pad)
2625 else:
2626 pad_row = _pad_row
2627 append_row = _append_basic_row
2629 padded_headers = pad_row(headers, pad)
2631 if fmt.lineabove and "lineabove" not in hidden:
2632 _append_line(lines, padded_widths, colaligns, fmt.lineabove)
2634 if padded_headers:
2635 append_row(lines, padded_headers, padded_widths, headersaligns, headerrow)
2636 if fmt.linebelowheader and "linebelowheader" not in hidden:
2637 _append_line(lines, padded_widths, colaligns, fmt.linebelowheader)
2639 if rows and fmt.linebetweenrows and "linebetweenrows" not in hidden:
2640 # initial rows with a line below
2641 for row, ralign in zip(rows[:-1], rowaligns):
2642 if row != SEPARATING_LINE:
2643 append_row(
2644 lines,
2645 pad_row(row, pad),
2646 padded_widths,
2647 colaligns,
2648 fmt.datarow,
2649 rowalign=ralign,
2650 )
2651 _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows)
2652 # the last row without a line below
2653 append_row(
2654 lines,
2655 pad_row(rows[-1], pad),
2656 padded_widths,
2657 colaligns,
2658 fmt.datarow,
2659 rowalign=rowaligns[-1],
2660 )
2661 else:
2662 separating_line = (
2663 fmt.linebetweenrows
2664 or fmt.linebelowheader
2665 or fmt.linebelow
2666 or fmt.lineabove
2667 or Line("", "", "", "")
2668 )
2669 for row in rows:
2670 # test to see if either the 1st column or the 2nd column (account for showindex) has
2671 # the SEPARATING_LINE flag
2672 if _is_separating_line(row):
2673 _append_line(lines, padded_widths, colaligns, separating_line)
2674 else:
2675 append_row(lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow)
2677 if fmt.linebelow and "linebelow" not in hidden:
2678 _append_line(lines, padded_widths, colaligns, fmt.linebelow)
2680 if headers or rows:
2681 output = "\n".join(lines)
2682 if fmt.lineabove == _html_begin_table_without_header:
2683 return JupyterHTMLStr(output)
2684 else:
2685 return output
2686 else: # a completely empty table
2687 return ""
2690class _CustomTextWrap(textwrap.TextWrapper):
2691 """A custom implementation of CPython's textwrap.TextWrapper. This supports
2692 both wide characters (Korea, Japanese, Chinese) - including mixed string.
2693 For the most part, the `_handle_long_word` and `_wrap_chunks` functions were
2694 copy pasted out of the CPython baseline, and updated with our custom length
2695 and line appending logic.
2696 """
2698 def __init__(self, *args, **kwargs):
2699 self._active_codes = []
2700 self.max_lines = None # For python2 compatibility
2701 textwrap.TextWrapper.__init__(self, *args, **kwargs)
2703 @staticmethod
2704 def _len(item):
2705 """Custom len that gets console column width for wide
2706 and non-wide characters as well as ignores color codes"""
2707 stripped = _strip_ansi(item)
2708 if wcwidth:
2709 return wcwidth.wcswidth(stripped)
2710 else:
2711 return len(stripped)
2713 def _update_lines(self, lines, new_line):
2714 """Adds a new line to the list of lines the text is being wrapped into
2715 This function will also track any ANSI color codes in this string as well
2716 as add any colors from previous lines order to preserve the same formatting
2717 as a single unwrapped string.
2718 """
2719 code_matches = list(_ansi_codes.finditer(new_line))
2720 color_codes = [code.string[code.span()[0] : code.span()[1]] for code in code_matches]
2722 # Add color codes from earlier in the unwrapped line, and then track any new ones we add.
2723 new_line = "".join(self._active_codes) + new_line
2725 for code in color_codes:
2726 if code != _ansi_color_reset_code:
2727 self._active_codes.append(code)
2728 else: # A single reset code resets everything
2729 self._active_codes = []
2731 # Always ensure each line is color terminated if any colors are
2732 # still active, otherwise colors will bleed into other cells on the console
2733 if len(self._active_codes) > 0:
2734 new_line = new_line + _ansi_color_reset_code
2736 lines.append(new_line)
2738 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
2739 """_handle_long_word(chunks : [string],
2740 cur_line : [string],
2741 cur_len : int, width : int)
2742 Handle a chunk of text (most likely a word, not whitespace) that
2743 is too long to fit in any line.
2744 """
2745 # Figure out when indent is larger than the specified width, and make
2746 # sure at least one character is stripped off on every pass
2747 if width < 1:
2748 space_left = 1
2749 else:
2750 space_left = width - cur_len
2752 # If we're allowed to break long words, then do so: put as much
2753 # of the next chunk onto the current line as will fit.
2754 if self.break_long_words and space_left > 0:
2755 # Tabulate Custom: Build the string up piece-by-piece in order to
2756 # take each charcter's width into account
2757 chunk = reversed_chunks[-1]
2758 i = 1
2759 # Only count printable characters, so strip_ansi first, index later.
2760 stripped_chunk = _strip_ansi(chunk)
2761 while i <= len(stripped_chunk) and self._len(stripped_chunk[:i]) <= space_left:
2762 i = i + 1
2763 # Always consume at least one character so _wrap_chunks makes
2764 # progress even when the first character is wider than space_left
2765 # (e.g. a 2-column CJK char in a 1-column-wide slot).
2766 i = max(i, 2)
2767 # Consider escape codes when breaking words up
2768 total_escape_len = 0
2769 last_group = 0
2770 if _ansi_codes.search(chunk) is not None:
2771 for group, _, _, _ in _ansi_codes.findall(chunk):
2772 escape_len = len(group)
2773 if group in chunk[last_group : i + total_escape_len + escape_len - 1]:
2774 total_escape_len += escape_len
2775 found = _ansi_codes.search(chunk[last_group:])
2776 last_group += found.end()
2777 cur_line.append(chunk[: i + total_escape_len - 1])
2778 reversed_chunks[-1] = chunk[i + total_escape_len - 1 :]
2780 # Otherwise, we have to preserve the long word intact. Only add
2781 # it to the current line if there's nothing already there --
2782 # that minimizes how much we violate the width constraint.
2783 elif not cur_line:
2784 cur_line.append(reversed_chunks.pop())
2786 # If we're not allowed to break long words, and there's already
2787 # text on the current line, do nothing. Next time through the
2788 # main loop of _wrap_chunks(), we'll wind up here again, but
2789 # cur_len will be zero, so the next line will be entirely
2790 # devoted to the long word that we can't handle right now.
2792 def _wrap_chunks(self, chunks):
2793 """_wrap_chunks(chunks : [string]) -> [string]
2794 Wrap a sequence of text chunks and return a list of lines of
2795 length 'self.width' or less. (If 'break_long_words' is false,
2796 some lines may be longer than this.) Chunks correspond roughly
2797 to words and the whitespace between them: each chunk is
2798 indivisible (modulo 'break_long_words'), but a line break can
2799 come between any two chunks. Chunks should not have internal
2800 whitespace; ie. a chunk is either all whitespace or a "word".
2801 Whitespace chunks will be removed from the beginning and end of
2802 lines, but apart from that whitespace is preserved.
2803 """
2804 lines = []
2805 if self.width <= 0:
2806 raise ValueError(f"invalid width {self.width!r} (must be > 0)")
2807 if self.max_lines is not None:
2808 if self.max_lines > 1:
2809 indent = self.subsequent_indent
2810 else:
2811 indent = self.initial_indent
2812 if self._len(indent) + self._len(self.placeholder.lstrip()) > self.width:
2813 raise ValueError("placeholder too large for max width")
2815 # Arrange in reverse order so items can be efficiently popped
2816 # from a stack of chucks.
2817 chunks.reverse()
2819 while chunks:
2820 # Start the list of chunks that will make up the current line.
2821 # cur_len is just the length of all the chunks in cur_line.
2822 cur_line = []
2823 cur_len = 0
2825 # Figure out which static string will prefix this line.
2826 if lines:
2827 indent = self.subsequent_indent
2828 else:
2829 indent = self.initial_indent
2831 # Maximum width for this line.
2832 width = self.width - self._len(indent)
2834 # First chunk on line is whitespace -- drop it, unless this
2835 # is the very beginning of the text (ie. no lines started yet).
2836 if self.drop_whitespace and chunks[-1].strip() == "" and lines:
2837 del chunks[-1]
2839 while chunks:
2840 chunk_len = self._len(chunks[-1])
2842 # Can at least squeeze this chunk onto the current line.
2843 if cur_len + chunk_len <= width:
2844 cur_line.append(chunks.pop())
2845 cur_len += chunk_len
2847 # Nope, this line is full.
2848 else:
2849 break
2851 # The current line is full, and the next chunk is too big to
2852 # fit on *any* line (not just this one).
2853 if chunks and self._len(chunks[-1]) > width:
2854 self._handle_long_word(chunks, cur_line, cur_len, width)
2855 cur_len = sum(map(self._len, cur_line))
2857 # If the last chunk on this line is all whitespace, drop it.
2858 if self.drop_whitespace and cur_line and cur_line[-1].strip() == "":
2859 cur_len -= self._len(cur_line[-1])
2860 del cur_line[-1]
2862 if cur_line:
2863 if (
2864 self.max_lines is None
2865 or len(lines) + 1 < self.max_lines
2866 or (
2867 not chunks
2868 or self.drop_whitespace
2869 and len(chunks) == 1
2870 and not chunks[0].strip()
2871 )
2872 and cur_len <= width
2873 ):
2874 # Convert current line back to a string and store it in
2875 # list of all lines (return value).
2876 self._update_lines(lines, indent + "".join(cur_line))
2877 else:
2878 while cur_line:
2879 if cur_line[-1].strip() and cur_len + self._len(self.placeholder) <= width:
2880 cur_line.append(self.placeholder)
2881 self._update_lines(lines, indent + "".join(cur_line))
2882 break
2883 cur_len -= self._len(cur_line[-1])
2884 del cur_line[-1]
2885 else:
2886 if lines:
2887 prev_line = lines[-1].rstrip()
2888 if self._len(prev_line) + self._len(self.placeholder) <= self.width:
2889 lines[-1] = prev_line + self.placeholder
2890 break
2891 self._update_lines(lines, indent + self.placeholder.lstrip())
2892 break
2894 return lines
2897if __name__ == "__main__":
2898 from .cli import _main
2900 _main()