Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/tabulate.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
| author | shellac |
|---|---|
| date | Mon, 22 Mar 2021 18:12:50 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:4f3585e2f14b |
|---|---|
| 1 # -*- coding: utf-8 -*- | |
| 2 | |
| 3 """Pretty-print tabular data.""" | |
| 4 | |
| 5 from __future__ import print_function | |
| 6 from __future__ import unicode_literals | |
| 7 from collections import namedtuple | |
| 8 import sys | |
| 9 import re | |
| 10 import math | |
| 11 | |
| 12 | |
| 13 if sys.version_info >= (3, 3): | |
| 14 from collections.abc import Iterable | |
| 15 else: | |
| 16 from collections import Iterable | |
| 17 | |
| 18 if sys.version_info[0] < 3: | |
| 19 from itertools import izip_longest | |
| 20 from functools import partial | |
| 21 | |
| 22 _none_type = type(None) | |
| 23 _bool_type = bool | |
| 24 _int_type = int | |
| 25 _long_type = long # noqa | |
| 26 _float_type = float | |
| 27 _text_type = unicode # noqa | |
| 28 _binary_type = str | |
| 29 | |
| 30 def _is_file(f): | |
| 31 return hasattr(f, "read") | |
| 32 | |
| 33 | |
| 34 else: | |
| 35 from itertools import zip_longest as izip_longest | |
| 36 from functools import reduce, partial | |
| 37 | |
| 38 _none_type = type(None) | |
| 39 _bool_type = bool | |
| 40 _int_type = int | |
| 41 _long_type = int | |
| 42 _float_type = float | |
| 43 _text_type = str | |
| 44 _binary_type = bytes | |
| 45 basestring = str | |
| 46 | |
| 47 import io | |
| 48 | |
| 49 def _is_file(f): | |
| 50 return isinstance(f, io.IOBase) | |
| 51 | |
| 52 | |
| 53 try: | |
| 54 import wcwidth # optional wide-character (CJK) support | |
| 55 except ImportError: | |
| 56 wcwidth = None | |
| 57 | |
| 58 try: | |
| 59 from html import escape as htmlescape | |
| 60 except ImportError: | |
| 61 from cgi import escape as htmlescape | |
| 62 | |
| 63 | |
| 64 __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] | |
| 65 __version__ = "0.8.9" | |
| 66 | |
| 67 | |
| 68 # minimum extra space in headers | |
| 69 MIN_PADDING = 2 | |
| 70 | |
| 71 # Whether or not to preserve leading/trailing whitespace in data. | |
| 72 PRESERVE_WHITESPACE = False | |
| 73 | |
| 74 _DEFAULT_FLOATFMT = "g" | |
| 75 _DEFAULT_MISSINGVAL = "" | |
| 76 # default align will be overwritten by "left", "center" or "decimal" | |
| 77 # depending on the formatter | |
| 78 _DEFAULT_ALIGN = "default" | |
| 79 | |
| 80 | |
| 81 # if True, enable wide-character (CJK) support | |
| 82 WIDE_CHARS_MODE = wcwidth is not None | |
| 83 | |
| 84 | |
| 85 Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) | |
| 86 | |
| 87 | |
| 88 DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) | |
| 89 | |
| 90 | |
| 91 # A table structure is suppposed to be: | |
| 92 # | |
| 93 # --- lineabove --------- | |
| 94 # headerrow | |
| 95 # --- linebelowheader --- | |
| 96 # datarow | |
| 97 # --- linebetweenrows --- | |
| 98 # ... (more datarows) ... | |
| 99 # --- linebetweenrows --- | |
| 100 # last datarow | |
| 101 # --- linebelow --------- | |
| 102 # | |
| 103 # TableFormat's line* elements can be | |
| 104 # | |
| 105 # - either None, if the element is not used, | |
| 106 # - or a Line tuple, | |
| 107 # - or a function: [col_widths], [col_alignments] -> string. | |
| 108 # | |
| 109 # TableFormat's *row elements can be | |
| 110 # | |
| 111 # - either None, if the element is not used, | |
| 112 # - or a DataRow tuple, | |
| 113 # - or a function: [cell_values], [col_widths], [col_alignments] -> string. | |
| 114 # | |
| 115 # padding (an integer) is the amount of white space around data values. | |
| 116 # | |
| 117 # with_header_hide: | |
| 118 # | |
| 119 # - either None, to display all table elements unconditionally, | |
| 120 # - or a list of elements not to be displayed if the table has column headers. | |
| 121 # | |
| 122 TableFormat = namedtuple( | |
| 123 "TableFormat", | |
| 124 [ | |
| 125 "lineabove", | |
| 126 "linebelowheader", | |
| 127 "linebetweenrows", | |
| 128 "linebelow", | |
| 129 "headerrow", | |
| 130 "datarow", | |
| 131 "padding", | |
| 132 "with_header_hide", | |
| 133 ], | |
| 134 ) | |
| 135 | |
| 136 | |
| 137 def _pipe_segment_with_colons(align, colwidth): | |
| 138 """Return a segment of a horizontal line with optional colons which | |
| 139 indicate column's alignment (as in `pipe` output format).""" | |
| 140 w = colwidth | |
| 141 if align in ["right", "decimal"]: | |
| 142 return ("-" * (w - 1)) + ":" | |
| 143 elif align == "center": | |
| 144 return ":" + ("-" * (w - 2)) + ":" | |
| 145 elif align == "left": | |
| 146 return ":" + ("-" * (w - 1)) | |
| 147 else: | |
| 148 return "-" * w | |
| 149 | |
| 150 | |
| 151 def _pipe_line_with_colons(colwidths, colaligns): | |
| 152 """Return a horizontal line with optional colons to indicate column's | |
| 153 alignment (as in `pipe` output format).""" | |
| 154 if not colaligns: # e.g. printing an empty data frame (github issue #15) | |
| 155 colaligns = [""] * len(colwidths) | |
| 156 segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)] | |
| 157 return "|" + "|".join(segments) + "|" | |
| 158 | |
| 159 | |
| 160 def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): | |
| 161 alignment = { | |
| 162 "left": "", | |
| 163 "right": 'align="right"| ', | |
| 164 "center": 'align="center"| ', | |
| 165 "decimal": 'align="right"| ', | |
| 166 } | |
| 167 # hard-coded padding _around_ align attribute and value together | |
| 168 # rather than padding parameter which affects only the value | |
| 169 values_with_attrs = [ | |
| 170 " " + alignment.get(a, "") + c + " " for c, a in zip(cell_values, colaligns) | |
| 171 ] | |
| 172 colsep = separator * 2 | |
| 173 return (separator + colsep.join(values_with_attrs)).rstrip() | |
| 174 | |
| 175 | |
| 176 def _textile_row_with_attrs(cell_values, colwidths, colaligns): | |
| 177 cell_values[0] += " " | |
| 178 alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} | |
| 179 values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values)) | |
| 180 return "|" + "|".join(values) + "|" | |
| 181 | |
| 182 | |
| 183 def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): | |
| 184 # this table header will be suppressed if there is a header row | |
| 185 return "<table>\n<tbody>" | |
| 186 | |
| 187 | |
| 188 def _html_row_with_attrs(celltag, unsafe, cell_values, colwidths, colaligns): | |
| 189 alignment = { | |
| 190 "left": "", | |
| 191 "right": ' style="text-align: right;"', | |
| 192 "center": ' style="text-align: center;"', | |
| 193 "decimal": ' style="text-align: right;"', | |
| 194 } | |
| 195 if unsafe: | |
| 196 values_with_attrs = [ | |
| 197 "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), c) | |
| 198 for c, a in zip(cell_values, colaligns) | |
| 199 ] | |
| 200 else: | |
| 201 values_with_attrs = [ | |
| 202 "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), htmlescape(c)) | |
| 203 for c, a in zip(cell_values, colaligns) | |
| 204 ] | |
| 205 rowhtml = "<tr>{}</tr>".format("".join(values_with_attrs).rstrip()) | |
| 206 if celltag == "th": # it's a header row, create a new table header | |
| 207 rowhtml = "<table>\n<thead>\n{}\n</thead>\n<tbody>".format(rowhtml) | |
| 208 return rowhtml | |
| 209 | |
| 210 | |
| 211 def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=""): | |
| 212 alignment = { | |
| 213 "left": "", | |
| 214 "right": '<style="text-align: right;">', | |
| 215 "center": '<style="text-align: center;">', | |
| 216 "decimal": '<style="text-align: right;">', | |
| 217 } | |
| 218 values_with_attrs = [ | |
| 219 "{0}{1} {2} ".format(celltag, alignment.get(a, ""), header + c + header) | |
| 220 for c, a in zip(cell_values, colaligns) | |
| 221 ] | |
| 222 return "".join(values_with_attrs) + "||" | |
| 223 | |
| 224 | |
| 225 def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=False): | |
| 226 alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} | |
| 227 tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) | |
| 228 return "\n".join( | |
| 229 [ | |
| 230 ("\\begin{tabular}{" if not longtable else "\\begin{longtable}{") | |
| 231 + tabular_columns_fmt | |
| 232 + "}", | |
| 233 "\\toprule" if booktabs else "\\hline", | |
| 234 ] | |
| 235 ) | |
| 236 | |
| 237 | |
| 238 LATEX_ESCAPE_RULES = { | |
| 239 r"&": r"\&", | |
| 240 r"%": r"\%", | |
| 241 r"$": r"\$", | |
| 242 r"#": r"\#", | |
| 243 r"_": r"\_", | |
| 244 r"^": r"\^{}", | |
| 245 r"{": r"\{", | |
| 246 r"}": r"\}", | |
| 247 r"~": r"\textasciitilde{}", | |
| 248 "\\": r"\textbackslash{}", | |
| 249 r"<": r"\ensuremath{<}", | |
| 250 r">": r"\ensuremath{>}", | |
| 251 } | |
| 252 | |
| 253 | |
| 254 def _latex_row(cell_values, colwidths, colaligns, escrules=LATEX_ESCAPE_RULES): | |
| 255 def escape_char(c): | |
| 256 return escrules.get(c, c) | |
| 257 | |
| 258 escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values] | |
| 259 rowfmt = DataRow("", "&", "\\\\") | |
| 260 return _build_simple_row(escaped_values, rowfmt) | |
| 261 | |
| 262 | |
| 263 def _rst_escape_first_column(rows, headers): | |
| 264 def escape_empty(val): | |
| 265 if isinstance(val, (_text_type, _binary_type)) and not val.strip(): | |
| 266 return ".." | |
| 267 else: | |
| 268 return val | |
| 269 | |
| 270 new_headers = list(headers) | |
| 271 new_rows = [] | |
| 272 if headers: | |
| 273 new_headers[0] = escape_empty(headers[0]) | |
| 274 for row in rows: | |
| 275 new_row = list(row) | |
| 276 if new_row: | |
| 277 new_row[0] = escape_empty(row[0]) | |
| 278 new_rows.append(new_row) | |
| 279 return new_rows, new_headers | |
| 280 | |
| 281 | |
| 282 _table_formats = { | |
| 283 "simple": TableFormat( | |
| 284 lineabove=Line("", "-", " ", ""), | |
| 285 linebelowheader=Line("", "-", " ", ""), | |
| 286 linebetweenrows=None, | |
| 287 linebelow=Line("", "-", " ", ""), | |
| 288 headerrow=DataRow("", " ", ""), | |
| 289 datarow=DataRow("", " ", ""), | |
| 290 padding=0, | |
| 291 with_header_hide=["lineabove", "linebelow"], | |
| 292 ), | |
| 293 "plain": TableFormat( | |
| 294 lineabove=None, | |
| 295 linebelowheader=None, | |
| 296 linebetweenrows=None, | |
| 297 linebelow=None, | |
| 298 headerrow=DataRow("", " ", ""), | |
| 299 datarow=DataRow("", " ", ""), | |
| 300 padding=0, | |
| 301 with_header_hide=None, | |
| 302 ), | |
| 303 "grid": TableFormat( | |
| 304 lineabove=Line("+", "-", "+", "+"), | |
| 305 linebelowheader=Line("+", "=", "+", "+"), | |
| 306 linebetweenrows=Line("+", "-", "+", "+"), | |
| 307 linebelow=Line("+", "-", "+", "+"), | |
| 308 headerrow=DataRow("|", "|", "|"), | |
| 309 datarow=DataRow("|", "|", "|"), | |
| 310 padding=1, | |
| 311 with_header_hide=None, | |
| 312 ), | |
| 313 "fancy_grid": TableFormat( | |
| 314 lineabove=Line("╒", "═", "╤", "╕"), | |
| 315 linebelowheader=Line("╞", "═", "╪", "╡"), | |
| 316 linebetweenrows=Line("├", "─", "┼", "┤"), | |
| 317 linebelow=Line("╘", "═", "╧", "╛"), | |
| 318 headerrow=DataRow("│", "│", "│"), | |
| 319 datarow=DataRow("│", "│", "│"), | |
| 320 padding=1, | |
| 321 with_header_hide=None, | |
| 322 ), | |
| 323 "fancy_outline": TableFormat( | |
| 324 lineabove=Line("╒", "═", "╤", "╕"), | |
| 325 linebelowheader=Line("╞", "═", "╪", "╡"), | |
| 326 linebetweenrows=None, | |
| 327 linebelow=Line("╘", "═", "╧", "╛"), | |
| 328 headerrow=DataRow("│", "│", "│"), | |
| 329 datarow=DataRow("│", "│", "│"), | |
| 330 padding=1, | |
| 331 with_header_hide=None, | |
| 332 ), | |
| 333 "github": TableFormat( | |
| 334 lineabove=Line("|", "-", "|", "|"), | |
| 335 linebelowheader=Line("|", "-", "|", "|"), | |
| 336 linebetweenrows=None, | |
| 337 linebelow=None, | |
| 338 headerrow=DataRow("|", "|", "|"), | |
| 339 datarow=DataRow("|", "|", "|"), | |
| 340 padding=1, | |
| 341 with_header_hide=["lineabove"], | |
| 342 ), | |
| 343 "pipe": TableFormat( | |
| 344 lineabove=_pipe_line_with_colons, | |
| 345 linebelowheader=_pipe_line_with_colons, | |
| 346 linebetweenrows=None, | |
| 347 linebelow=None, | |
| 348 headerrow=DataRow("|", "|", "|"), | |
| 349 datarow=DataRow("|", "|", "|"), | |
| 350 padding=1, | |
| 351 with_header_hide=["lineabove"], | |
| 352 ), | |
| 353 "orgtbl": TableFormat( | |
| 354 lineabove=None, | |
| 355 linebelowheader=Line("|", "-", "+", "|"), | |
| 356 linebetweenrows=None, | |
| 357 linebelow=None, | |
| 358 headerrow=DataRow("|", "|", "|"), | |
| 359 datarow=DataRow("|", "|", "|"), | |
| 360 padding=1, | |
| 361 with_header_hide=None, | |
| 362 ), | |
| 363 "jira": TableFormat( | |
| 364 lineabove=None, | |
| 365 linebelowheader=None, | |
| 366 linebetweenrows=None, | |
| 367 linebelow=None, | |
| 368 headerrow=DataRow("||", "||", "||"), | |
| 369 datarow=DataRow("|", "|", "|"), | |
| 370 padding=1, | |
| 371 with_header_hide=None, | |
| 372 ), | |
| 373 "presto": TableFormat( | |
| 374 lineabove=None, | |
| 375 linebelowheader=Line("", "-", "+", ""), | |
| 376 linebetweenrows=None, | |
| 377 linebelow=None, | |
| 378 headerrow=DataRow("", "|", ""), | |
| 379 datarow=DataRow("", "|", ""), | |
| 380 padding=1, | |
| 381 with_header_hide=None, | |
| 382 ), | |
| 383 "pretty": TableFormat( | |
| 384 lineabove=Line("+", "-", "+", "+"), | |
| 385 linebelowheader=Line("+", "-", "+", "+"), | |
| 386 linebetweenrows=None, | |
| 387 linebelow=Line("+", "-", "+", "+"), | |
| 388 headerrow=DataRow("|", "|", "|"), | |
| 389 datarow=DataRow("|", "|", "|"), | |
| 390 padding=1, | |
| 391 with_header_hide=None, | |
| 392 ), | |
| 393 "psql": TableFormat( | |
| 394 lineabove=Line("+", "-", "+", "+"), | |
| 395 linebelowheader=Line("|", "-", "+", "|"), | |
| 396 linebetweenrows=None, | |
| 397 linebelow=Line("+", "-", "+", "+"), | |
| 398 headerrow=DataRow("|", "|", "|"), | |
| 399 datarow=DataRow("|", "|", "|"), | |
| 400 padding=1, | |
| 401 with_header_hide=None, | |
| 402 ), | |
| 403 "rst": TableFormat( | |
| 404 lineabove=Line("", "=", " ", ""), | |
| 405 linebelowheader=Line("", "=", " ", ""), | |
| 406 linebetweenrows=None, | |
| 407 linebelow=Line("", "=", " ", ""), | |
| 408 headerrow=DataRow("", " ", ""), | |
| 409 datarow=DataRow("", " ", ""), | |
| 410 padding=0, | |
| 411 with_header_hide=None, | |
| 412 ), | |
| 413 "mediawiki": TableFormat( | |
| 414 lineabove=Line( | |
| 415 '{| class="wikitable" style="text-align: left;"', | |
| 416 "", | |
| 417 "", | |
| 418 "\n|+ <!-- caption -->\n|-", | |
| 419 ), | |
| 420 linebelowheader=Line("|-", "", "", ""), | |
| 421 linebetweenrows=Line("|-", "", "", ""), | |
| 422 linebelow=Line("|}", "", "", ""), | |
| 423 headerrow=partial(_mediawiki_row_with_attrs, "!"), | |
| 424 datarow=partial(_mediawiki_row_with_attrs, "|"), | |
| 425 padding=0, | |
| 426 with_header_hide=None, | |
| 427 ), | |
| 428 "moinmoin": TableFormat( | |
| 429 lineabove=None, | |
| 430 linebelowheader=None, | |
| 431 linebetweenrows=None, | |
| 432 linebelow=None, | |
| 433 headerrow=partial(_moin_row_with_attrs, "||", header="'''"), | |
| 434 datarow=partial(_moin_row_with_attrs, "||"), | |
| 435 padding=1, | |
| 436 with_header_hide=None, | |
| 437 ), | |
| 438 "youtrack": TableFormat( | |
| 439 lineabove=None, | |
| 440 linebelowheader=None, | |
| 441 linebetweenrows=None, | |
| 442 linebelow=None, | |
| 443 headerrow=DataRow("|| ", " || ", " || "), | |
| 444 datarow=DataRow("| ", " | ", " |"), | |
| 445 padding=1, | |
| 446 with_header_hide=None, | |
| 447 ), | |
| 448 "html": TableFormat( | |
| 449 lineabove=_html_begin_table_without_header, | |
| 450 linebelowheader="", | |
| 451 linebetweenrows=None, | |
| 452 linebelow=Line("</tbody>\n</table>", "", "", ""), | |
| 453 headerrow=partial(_html_row_with_attrs, "th", False), | |
| 454 datarow=partial(_html_row_with_attrs, "td", False), | |
| 455 padding=0, | |
| 456 with_header_hide=["lineabove"], | |
| 457 ), | |
| 458 "unsafehtml": TableFormat( | |
| 459 lineabove=_html_begin_table_without_header, | |
| 460 linebelowheader="", | |
| 461 linebetweenrows=None, | |
| 462 linebelow=Line("</tbody>\n</table>", "", "", ""), | |
| 463 headerrow=partial(_html_row_with_attrs, "th", True), | |
| 464 datarow=partial(_html_row_with_attrs, "td", True), | |
| 465 padding=0, | |
| 466 with_header_hide=["lineabove"], | |
| 467 ), | |
| 468 "latex": TableFormat( | |
| 469 lineabove=_latex_line_begin_tabular, | |
| 470 linebelowheader=Line("\\hline", "", "", ""), | |
| 471 linebetweenrows=None, | |
| 472 linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), | |
| 473 headerrow=_latex_row, | |
| 474 datarow=_latex_row, | |
| 475 padding=1, | |
| 476 with_header_hide=None, | |
| 477 ), | |
| 478 "latex_raw": TableFormat( | |
| 479 lineabove=_latex_line_begin_tabular, | |
| 480 linebelowheader=Line("\\hline", "", "", ""), | |
| 481 linebetweenrows=None, | |
| 482 linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), | |
| 483 headerrow=partial(_latex_row, escrules={}), | |
| 484 datarow=partial(_latex_row, escrules={}), | |
| 485 padding=1, | |
| 486 with_header_hide=None, | |
| 487 ), | |
| 488 "latex_booktabs": TableFormat( | |
| 489 lineabove=partial(_latex_line_begin_tabular, booktabs=True), | |
| 490 linebelowheader=Line("\\midrule", "", "", ""), | |
| 491 linebetweenrows=None, | |
| 492 linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""), | |
| 493 headerrow=_latex_row, | |
| 494 datarow=_latex_row, | |
| 495 padding=1, | |
| 496 with_header_hide=None, | |
| 497 ), | |
| 498 "latex_longtable": TableFormat( | |
| 499 lineabove=partial(_latex_line_begin_tabular, longtable=True), | |
| 500 linebelowheader=Line("\\hline\n\\endhead", "", "", ""), | |
| 501 linebetweenrows=None, | |
| 502 linebelow=Line("\\hline\n\\end{longtable}", "", "", ""), | |
| 503 headerrow=_latex_row, | |
| 504 datarow=_latex_row, | |
| 505 padding=1, | |
| 506 with_header_hide=None, | |
| 507 ), | |
| 508 "tsv": TableFormat( | |
| 509 lineabove=None, | |
| 510 linebelowheader=None, | |
| 511 linebetweenrows=None, | |
| 512 linebelow=None, | |
| 513 headerrow=DataRow("", "\t", ""), | |
| 514 datarow=DataRow("", "\t", ""), | |
| 515 padding=0, | |
| 516 with_header_hide=None, | |
| 517 ), | |
| 518 "textile": TableFormat( | |
| 519 lineabove=None, | |
| 520 linebelowheader=None, | |
| 521 linebetweenrows=None, | |
| 522 linebelow=None, | |
| 523 headerrow=DataRow("|_. ", "|_.", "|"), | |
| 524 datarow=_textile_row_with_attrs, | |
| 525 padding=1, | |
| 526 with_header_hide=None, | |
| 527 ), | |
| 528 } | |
| 529 | |
| 530 | |
| 531 tabulate_formats = list(sorted(_table_formats.keys())) | |
| 532 | |
| 533 # The table formats for which multiline cells will be folded into subsequent | |
| 534 # table rows. The key is the original format specified at the API. The value is | |
| 535 # the format that will be used to represent the original format. | |
| 536 multiline_formats = { | |
| 537 "plain": "plain", | |
| 538 "simple": "simple", | |
| 539 "grid": "grid", | |
| 540 "fancy_grid": "fancy_grid", | |
| 541 "pipe": "pipe", | |
| 542 "orgtbl": "orgtbl", | |
| 543 "jira": "jira", | |
| 544 "presto": "presto", | |
| 545 "pretty": "pretty", | |
| 546 "psql": "psql", | |
| 547 "rst": "rst", | |
| 548 } | |
| 549 | |
| 550 # TODO: Add multiline support for the remaining table formats: | |
| 551 # - mediawiki: Replace \n with <br> | |
| 552 # - moinmoin: TBD | |
| 553 # - youtrack: TBD | |
| 554 # - html: Replace \n with <br> | |
| 555 # - latex*: Use "makecell" package: In header, replace X\nY with | |
| 556 # \thead{X\\Y} and in data row, replace X\nY with \makecell{X\\Y} | |
| 557 # - tsv: TBD | |
| 558 # - textile: Replace \n with <br/> (must be well-formed XML) | |
| 559 | |
| 560 _multiline_codes = re.compile(r"\r|\n|\r\n") | |
| 561 _multiline_codes_bytes = re.compile(b"\r|\n|\r\n") | |
| 562 _invisible_codes = re.compile( | |
| 563 r"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m|\x1b\]8;;(.*?)\x1b\\" | |
| 564 ) # ANSI color codes | |
| 565 _invisible_codes_bytes = re.compile( | |
| 566 b"\x1b\\[\\d+\\[;\\d]*m|\x1b\\[\\d*;\\d*;\\d*m|\\x1b\\]8;;(.*?)\\x1b\\\\" | |
| 567 ) # ANSI color codes | |
| 568 _invisible_codes_link = re.compile( | |
| 569 r"\x1B]8;[a-zA-Z0-9:]*;[^\x1B]+\x1B\\([^\x1b]+)\x1B]8;;\x1B\\" | |
| 570 ) # Terminal hyperlinks | |
| 571 | |
| 572 | |
| 573 def simple_separated_format(separator): | |
| 574 """Construct a simple TableFormat with columns separated by a separator. | |
| 575 | |
| 576 >>> tsv = simple_separated_format("\\t") ; \ | |
| 577 tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23' | |
| 578 True | |
| 579 | |
| 580 """ | |
| 581 return TableFormat( | |
| 582 None, | |
| 583 None, | |
| 584 None, | |
| 585 None, | |
| 586 headerrow=DataRow("", separator, ""), | |
| 587 datarow=DataRow("", separator, ""), | |
| 588 padding=0, | |
| 589 with_header_hide=None, | |
| 590 ) | |
| 591 | |
| 592 | |
| 593 def _isconvertible(conv, string): | |
| 594 try: | |
| 595 conv(string) | |
| 596 return True | |
| 597 except (ValueError, TypeError): | |
| 598 return False | |
| 599 | |
| 600 | |
| 601 def _isnumber(string): | |
| 602 """ | |
| 603 >>> _isnumber("123.45") | |
| 604 True | |
| 605 >>> _isnumber("123") | |
| 606 True | |
| 607 >>> _isnumber("spam") | |
| 608 False | |
| 609 >>> _isnumber("123e45678") | |
| 610 False | |
| 611 >>> _isnumber("inf") | |
| 612 True | |
| 613 """ | |
| 614 if not _isconvertible(float, string): | |
| 615 return False | |
| 616 elif isinstance(string, (_text_type, _binary_type)) and ( | |
| 617 math.isinf(float(string)) or math.isnan(float(string)) | |
| 618 ): | |
| 619 return string.lower() in ["inf", "-inf", "nan"] | |
| 620 return True | |
| 621 | |
| 622 | |
| 623 def _isint(string, inttype=int): | |
| 624 """ | |
| 625 >>> _isint("123") | |
| 626 True | |
| 627 >>> _isint("123.45") | |
| 628 False | |
| 629 """ | |
| 630 return ( | |
| 631 type(string) is inttype | |
| 632 or (isinstance(string, _binary_type) or isinstance(string, _text_type)) | |
| 633 and _isconvertible(inttype, string) | |
| 634 ) | |
| 635 | |
| 636 | |
| 637 def _isbool(string): | |
| 638 """ | |
| 639 >>> _isbool(True) | |
| 640 True | |
| 641 >>> _isbool("False") | |
| 642 True | |
| 643 >>> _isbool(1) | |
| 644 False | |
| 645 """ | |
| 646 return type(string) is _bool_type or ( | |
| 647 isinstance(string, (_binary_type, _text_type)) and string in ("True", "False") | |
| 648 ) | |
| 649 | |
| 650 | |
| 651 def _type(string, has_invisible=True, numparse=True): | |
| 652 """The least generic type (type(None), int, float, str, unicode). | |
| 653 | |
| 654 >>> _type(None) is type(None) | |
| 655 True | |
| 656 >>> _type("foo") is type("") | |
| 657 True | |
| 658 >>> _type("1") is type(1) | |
| 659 True | |
| 660 >>> _type('\x1b[31m42\x1b[0m') is type(42) | |
| 661 True | |
| 662 >>> _type('\x1b[31m42\x1b[0m') is type(42) | |
| 663 True | |
| 664 | |
| 665 """ | |
| 666 | |
| 667 if has_invisible and ( | |
| 668 isinstance(string, _text_type) or isinstance(string, _binary_type) | |
| 669 ): | |
| 670 string = _strip_invisible(string) | |
| 671 | |
| 672 if string is None: | |
| 673 return _none_type | |
| 674 elif hasattr(string, "isoformat"): # datetime.datetime, date, and time | |
| 675 return _text_type | |
| 676 elif _isbool(string): | |
| 677 return _bool_type | |
| 678 elif _isint(string) and numparse: | |
| 679 return int | |
| 680 elif _isint(string, _long_type) and numparse: | |
| 681 return int | |
| 682 elif _isnumber(string) and numparse: | |
| 683 return float | |
| 684 elif isinstance(string, _binary_type): | |
| 685 return _binary_type | |
| 686 else: | |
| 687 return _text_type | |
| 688 | |
| 689 | |
| 690 def _afterpoint(string): | |
| 691 """Symbols after a decimal point, -1 if the string lacks the decimal point. | |
| 692 | |
| 693 >>> _afterpoint("123.45") | |
| 694 2 | |
| 695 >>> _afterpoint("1001") | |
| 696 -1 | |
| 697 >>> _afterpoint("eggs") | |
| 698 -1 | |
| 699 >>> _afterpoint("123e45") | |
| 700 2 | |
| 701 | |
| 702 """ | |
| 703 if _isnumber(string): | |
| 704 if _isint(string): | |
| 705 return -1 | |
| 706 else: | |
| 707 pos = string.rfind(".") | |
| 708 pos = string.lower().rfind("e") if pos < 0 else pos | |
| 709 if pos >= 0: | |
| 710 return len(string) - pos - 1 | |
| 711 else: | |
| 712 return -1 # no point | |
| 713 else: | |
| 714 return -1 # not a number | |
| 715 | |
| 716 | |
| 717 def _padleft(width, s): | |
| 718 """Flush right. | |
| 719 | |
| 720 >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430' | |
| 721 True | |
| 722 | |
| 723 """ | |
| 724 fmt = "{0:>%ds}" % width | |
| 725 return fmt.format(s) | |
| 726 | |
| 727 | |
| 728 def _padright(width, s): | |
| 729 """Flush left. | |
| 730 | |
| 731 >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 ' | |
| 732 True | |
| 733 | |
| 734 """ | |
| 735 fmt = "{0:<%ds}" % width | |
| 736 return fmt.format(s) | |
| 737 | |
| 738 | |
| 739 def _padboth(width, s): | |
| 740 """Center string. | |
| 741 | |
| 742 >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 ' | |
| 743 True | |
| 744 | |
| 745 """ | |
| 746 fmt = "{0:^%ds}" % width | |
| 747 return fmt.format(s) | |
| 748 | |
| 749 | |
| 750 def _padnone(ignore_width, s): | |
| 751 return s | |
| 752 | |
| 753 | |
| 754 def _strip_invisible(s): | |
| 755 r"""Remove invisible ANSI color codes. | |
| 756 | |
| 757 >>> str(_strip_invisible('\x1B]8;;https://example.com\x1B\\This is a link\x1B]8;;\x1B\\')) | |
| 758 'This is a link' | |
| 759 | |
| 760 """ | |
| 761 if isinstance(s, _text_type): | |
| 762 links_removed = re.sub(_invisible_codes_link, "\\1", s) | |
| 763 return re.sub(_invisible_codes, "", links_removed) | |
| 764 else: # a bytestring | |
| 765 return re.sub(_invisible_codes_bytes, "", s) | |
| 766 | |
| 767 | |
| 768 def _visible_width(s): | |
| 769 """Visible width of a printed string. ANSI color codes are removed. | |
| 770 | |
| 771 >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world") | |
| 772 (5, 5) | |
| 773 | |
| 774 """ | |
| 775 # optional wide-character support | |
| 776 if wcwidth is not None and WIDE_CHARS_MODE: | |
| 777 len_fn = wcwidth.wcswidth | |
| 778 else: | |
| 779 len_fn = len | |
| 780 if isinstance(s, _text_type) or isinstance(s, _binary_type): | |
| 781 return len_fn(_strip_invisible(s)) | |
| 782 else: | |
| 783 return len_fn(_text_type(s)) | |
| 784 | |
| 785 | |
| 786 def _is_multiline(s): | |
| 787 if isinstance(s, _text_type): | |
| 788 return bool(re.search(_multiline_codes, s)) | |
| 789 else: # a bytestring | |
| 790 return bool(re.search(_multiline_codes_bytes, s)) | |
| 791 | |
| 792 | |
| 793 def _multiline_width(multiline_s, line_width_fn=len): | |
| 794 """Visible width of a potentially multiline content.""" | |
| 795 return max(map(line_width_fn, re.split("[\r\n]", multiline_s))) | |
| 796 | |
| 797 | |
| 798 def _choose_width_fn(has_invisible, enable_widechars, is_multiline): | |
| 799 """Return a function to calculate visible cell width.""" | |
| 800 if has_invisible: | |
| 801 line_width_fn = _visible_width | |
| 802 elif enable_widechars: # optional wide-character support if available | |
| 803 line_width_fn = wcwidth.wcswidth | |
| 804 else: | |
| 805 line_width_fn = len | |
| 806 if is_multiline: | |
| 807 width_fn = lambda s: _multiline_width(s, line_width_fn) # noqa | |
| 808 else: | |
| 809 width_fn = line_width_fn | |
| 810 return width_fn | |
| 811 | |
| 812 | |
| 813 def _align_column_choose_padfn(strings, alignment, has_invisible): | |
| 814 if alignment == "right": | |
| 815 if not PRESERVE_WHITESPACE: | |
| 816 strings = [s.strip() for s in strings] | |
| 817 padfn = _padleft | |
| 818 elif alignment == "center": | |
| 819 if not PRESERVE_WHITESPACE: | |
| 820 strings = [s.strip() for s in strings] | |
| 821 padfn = _padboth | |
| 822 elif alignment == "decimal": | |
| 823 if has_invisible: | |
| 824 decimals = [_afterpoint(_strip_invisible(s)) for s in strings] | |
| 825 else: | |
| 826 decimals = [_afterpoint(s) for s in strings] | |
| 827 maxdecimals = max(decimals) | |
| 828 strings = [s + (maxdecimals - decs) * " " for s, decs in zip(strings, decimals)] | |
| 829 padfn = _padleft | |
| 830 elif not alignment: | |
| 831 padfn = _padnone | |
| 832 else: | |
| 833 if not PRESERVE_WHITESPACE: | |
| 834 strings = [s.strip() for s in strings] | |
| 835 padfn = _padright | |
| 836 return strings, padfn | |
| 837 | |
| 838 | |
| 839 def _align_column_choose_width_fn(has_invisible, enable_widechars, is_multiline): | |
| 840 if has_invisible: | |
| 841 line_width_fn = _visible_width | |
| 842 elif enable_widechars: # optional wide-character support if available | |
| 843 line_width_fn = wcwidth.wcswidth | |
| 844 else: | |
| 845 line_width_fn = len | |
| 846 if is_multiline: | |
| 847 width_fn = lambda s: _align_column_multiline_width(s, line_width_fn) # noqa | |
| 848 else: | |
| 849 width_fn = line_width_fn | |
| 850 return width_fn | |
| 851 | |
| 852 | |
| 853 def _align_column_multiline_width(multiline_s, line_width_fn=len): | |
| 854 """Visible width of a potentially multiline content.""" | |
| 855 return list(map(line_width_fn, re.split("[\r\n]", multiline_s))) | |
| 856 | |
| 857 | |
| 858 def _flat_list(nested_list): | |
| 859 ret = [] | |
| 860 for item in nested_list: | |
| 861 if isinstance(item, list): | |
| 862 for subitem in item: | |
| 863 ret.append(subitem) | |
| 864 else: | |
| 865 ret.append(item) | |
| 866 return ret | |
| 867 | |
| 868 | |
| 869 def _align_column( | |
| 870 strings, | |
| 871 alignment, | |
| 872 minwidth=0, | |
| 873 has_invisible=True, | |
| 874 enable_widechars=False, | |
| 875 is_multiline=False, | |
| 876 ): | |
| 877 """[string] -> [padded_string]""" | |
| 878 strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible) | |
| 879 width_fn = _align_column_choose_width_fn( | |
| 880 has_invisible, enable_widechars, is_multiline | |
| 881 ) | |
| 882 | |
| 883 s_widths = list(map(width_fn, strings)) | |
| 884 maxwidth = max(max(_flat_list(s_widths)), minwidth) | |
| 885 # TODO: refactor column alignment in single-line and multiline modes | |
| 886 if is_multiline: | |
| 887 if not enable_widechars and not has_invisible: | |
| 888 padded_strings = [ | |
| 889 "\n".join([padfn(maxwidth, s) for s in ms.splitlines()]) | |
| 890 for ms in strings | |
| 891 ] | |
| 892 else: | |
| 893 # enable wide-character width corrections | |
| 894 s_lens = [[len(s) for s in re.split("[\r\n]", ms)] for ms in strings] | |
| 895 visible_widths = [ | |
| 896 [maxwidth - (w - l) for w, l in zip(mw, ml)] | |
| 897 for mw, ml in zip(s_widths, s_lens) | |
| 898 ] | |
| 899 # wcswidth and _visible_width don't count invisible characters; | |
| 900 # padfn doesn't need to apply another correction | |
| 901 padded_strings = [ | |
| 902 "\n".join([padfn(w, s) for s, w in zip((ms.splitlines() or ms), mw)]) | |
| 903 for ms, mw in zip(strings, visible_widths) | |
| 904 ] | |
| 905 else: # single-line cell values | |
| 906 if not enable_widechars and not has_invisible: | |
| 907 padded_strings = [padfn(maxwidth, s) for s in strings] | |
| 908 else: | |
| 909 # enable wide-character width corrections | |
| 910 s_lens = list(map(len, strings)) | |
| 911 visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] | |
| 912 # wcswidth and _visible_width don't count invisible characters; | |
| 913 # padfn doesn't need to apply another correction | |
| 914 padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)] | |
| 915 return padded_strings | |
| 916 | |
| 917 | |
| 918 def _more_generic(type1, type2): | |
| 919 types = { | |
| 920 _none_type: 0, | |
| 921 _bool_type: 1, | |
| 922 int: 2, | |
| 923 float: 3, | |
| 924 _binary_type: 4, | |
| 925 _text_type: 5, | |
| 926 } | |
| 927 invtypes = { | |
| 928 5: _text_type, | |
| 929 4: _binary_type, | |
| 930 3: float, | |
| 931 2: int, | |
| 932 1: _bool_type, | |
| 933 0: _none_type, | |
| 934 } | |
| 935 moregeneric = max(types.get(type1, 5), types.get(type2, 5)) | |
| 936 return invtypes[moregeneric] | |
| 937 | |
| 938 | |
| 939 def _column_type(strings, has_invisible=True, numparse=True): | |
| 940 """The least generic type all column values are convertible to. | |
| 941 | |
| 942 >>> _column_type([True, False]) is _bool_type | |
| 943 True | |
| 944 >>> _column_type(["1", "2"]) is _int_type | |
| 945 True | |
| 946 >>> _column_type(["1", "2.3"]) is _float_type | |
| 947 True | |
| 948 >>> _column_type(["1", "2.3", "four"]) is _text_type | |
| 949 True | |
| 950 >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is _text_type | |
| 951 True | |
| 952 >>> _column_type([None, "brux"]) is _text_type | |
| 953 True | |
| 954 >>> _column_type([1, 2, None]) is _int_type | |
| 955 True | |
| 956 >>> import datetime as dt | |
| 957 >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is _text_type | |
| 958 True | |
| 959 | |
| 960 """ | |
| 961 types = [_type(s, has_invisible, numparse) for s in strings] | |
| 962 return reduce(_more_generic, types, _bool_type) | |
| 963 | |
| 964 | |
| 965 def _format(val, valtype, floatfmt, missingval="", has_invisible=True): | |
| 966 """Format a value according to its type. | |
| 967 | |
| 968 Unicode is supported: | |
| 969 | |
| 970 >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \ | |
| 971 tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \ | |
| 972 good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \ | |
| 973 tabulate(tbl, headers=hrow) == good_result | |
| 974 True | |
| 975 | |
| 976 """ # noqa | |
| 977 if val is None: | |
| 978 return missingval | |
| 979 | |
| 980 if valtype in [int, _text_type]: | |
| 981 return "{0}".format(val) | |
| 982 elif valtype is _binary_type: | |
| 983 try: | |
| 984 return _text_type(val, "ascii") | |
| 985 except TypeError: | |
| 986 return _text_type(val) | |
| 987 elif valtype is float: | |
| 988 is_a_colored_number = has_invisible and isinstance( | |
| 989 val, (_text_type, _binary_type) | |
| 990 ) | |
| 991 if is_a_colored_number: | |
| 992 raw_val = _strip_invisible(val) | |
| 993 formatted_val = format(float(raw_val), floatfmt) | |
| 994 return val.replace(raw_val, formatted_val) | |
| 995 else: | |
| 996 return format(float(val), floatfmt) | |
| 997 else: | |
| 998 return "{0}".format(val) | |
| 999 | |
| 1000 | |
| 1001 def _align_header( | |
| 1002 header, alignment, width, visible_width, is_multiline=False, width_fn=None | |
| 1003 ): | |
| 1004 "Pad string header to width chars given known visible_width of the header." | |
| 1005 if is_multiline: | |
| 1006 header_lines = re.split(_multiline_codes, header) | |
| 1007 padded_lines = [ | |
| 1008 _align_header(h, alignment, width, width_fn(h)) for h in header_lines | |
| 1009 ] | |
| 1010 return "\n".join(padded_lines) | |
| 1011 # else: not multiline | |
| 1012 ninvisible = len(header) - visible_width | |
| 1013 width += ninvisible | |
| 1014 if alignment == "left": | |
| 1015 return _padright(width, header) | |
| 1016 elif alignment == "center": | |
| 1017 return _padboth(width, header) | |
| 1018 elif not alignment: | |
| 1019 return "{0}".format(header) | |
| 1020 else: | |
| 1021 return _padleft(width, header) | |
| 1022 | |
| 1023 | |
| 1024 def _prepend_row_index(rows, index): | |
| 1025 """Add a left-most index column.""" | |
| 1026 if index is None or index is False: | |
| 1027 return rows | |
| 1028 if len(index) != len(rows): | |
| 1029 print("index=", index) | |
| 1030 print("rows=", rows) | |
| 1031 raise ValueError("index must be as long as the number of data rows") | |
| 1032 rows = [[v] + list(row) for v, row in zip(index, rows)] | |
| 1033 return rows | |
| 1034 | |
| 1035 | |
| 1036 def _bool(val): | |
| 1037 "A wrapper around standard bool() which doesn't throw on NumPy arrays" | |
| 1038 try: | |
| 1039 return bool(val) | |
| 1040 except ValueError: # val is likely to be a numpy array with many elements | |
| 1041 return False | |
| 1042 | |
| 1043 | |
| 1044 def _normalize_tabular_data(tabular_data, headers, showindex="default"): | |
| 1045 """Transform a supported data type to a list of lists, and a list of headers. | |
| 1046 | |
| 1047 Supported tabular data types: | |
| 1048 | |
| 1049 * list-of-lists or another iterable of iterables | |
| 1050 | |
| 1051 * list of named tuples (usually used with headers="keys") | |
| 1052 | |
| 1053 * list of dicts (usually used with headers="keys") | |
| 1054 | |
| 1055 * list of OrderedDicts (usually used with headers="keys") | |
| 1056 | |
| 1057 * 2D NumPy arrays | |
| 1058 | |
| 1059 * NumPy record arrays (usually used with headers="keys") | |
| 1060 | |
| 1061 * dict of iterables (usually used with headers="keys") | |
| 1062 | |
| 1063 * pandas.DataFrame (usually used with headers="keys") | |
| 1064 | |
| 1065 The first row can be used as headers if headers="firstrow", | |
| 1066 column indices can be used as headers if headers="keys". | |
| 1067 | |
| 1068 If showindex="default", show row indices of the pandas.DataFrame. | |
| 1069 If showindex="always", show row indices for all types of data. | |
| 1070 If showindex="never", don't show row indices for all types of data. | |
| 1071 If showindex is an iterable, show its values as row indices. | |
| 1072 | |
| 1073 """ | |
| 1074 | |
| 1075 try: | |
| 1076 bool(headers) | |
| 1077 is_headers2bool_broken = False # noqa | |
| 1078 except ValueError: # numpy.ndarray, pandas.core.index.Index, ... | |
| 1079 is_headers2bool_broken = True # noqa | |
| 1080 headers = list(headers) | |
| 1081 | |
| 1082 index = None | |
| 1083 if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"): | |
| 1084 # dict-like and pandas.DataFrame? | |
| 1085 if hasattr(tabular_data.values, "__call__"): | |
| 1086 # likely a conventional dict | |
| 1087 keys = tabular_data.keys() | |
| 1088 rows = list( | |
| 1089 izip_longest(*tabular_data.values()) | |
| 1090 ) # columns have to be transposed | |
| 1091 elif hasattr(tabular_data, "index"): | |
| 1092 # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0) | |
| 1093 keys = list(tabular_data) | |
| 1094 if ( | |
| 1095 showindex in ["default", "always", True] | |
| 1096 and tabular_data.index.name is not None | |
| 1097 ): | |
| 1098 if isinstance(tabular_data.index.name, list): | |
| 1099 keys[:0] = tabular_data.index.name | |
| 1100 else: | |
| 1101 keys[:0] = [tabular_data.index.name] | |
| 1102 vals = tabular_data.values # values matrix doesn't need to be transposed | |
| 1103 # for DataFrames add an index per default | |
| 1104 index = list(tabular_data.index) | |
| 1105 rows = [list(row) for row in vals] | |
| 1106 else: | |
| 1107 raise ValueError("tabular data doesn't appear to be a dict or a DataFrame") | |
| 1108 | |
| 1109 if headers == "keys": | |
| 1110 headers = list(map(_text_type, keys)) # headers should be strings | |
| 1111 | |
| 1112 else: # it's a usual an iterable of iterables, or a NumPy array | |
| 1113 rows = list(tabular_data) | |
| 1114 | |
| 1115 if headers == "keys" and not rows: | |
| 1116 # an empty table (issue #81) | |
| 1117 headers = [] | |
| 1118 elif ( | |
| 1119 headers == "keys" | |
| 1120 and hasattr(tabular_data, "dtype") | |
| 1121 and getattr(tabular_data.dtype, "names") | |
| 1122 ): | |
| 1123 # numpy record array | |
| 1124 headers = tabular_data.dtype.names | |
| 1125 elif ( | |
| 1126 headers == "keys" | |
| 1127 and len(rows) > 0 | |
| 1128 and isinstance(rows[0], tuple) | |
| 1129 and hasattr(rows[0], "_fields") | |
| 1130 ): | |
| 1131 # namedtuple | |
| 1132 headers = list(map(_text_type, rows[0]._fields)) | |
| 1133 elif len(rows) > 0 and hasattr(rows[0], "keys") and hasattr(rows[0], "values"): | |
| 1134 # dict-like object | |
| 1135 uniq_keys = set() # implements hashed lookup | |
| 1136 keys = [] # storage for set | |
| 1137 if headers == "firstrow": | |
| 1138 firstdict = rows[0] if len(rows) > 0 else {} | |
| 1139 keys.extend(firstdict.keys()) | |
| 1140 uniq_keys.update(keys) | |
| 1141 rows = rows[1:] | |
| 1142 for row in rows: | |
| 1143 for k in row.keys(): | |
| 1144 # Save unique items in input order | |
| 1145 if k not in uniq_keys: | |
| 1146 keys.append(k) | |
| 1147 uniq_keys.add(k) | |
| 1148 if headers == "keys": | |
| 1149 headers = keys | |
| 1150 elif isinstance(headers, dict): | |
| 1151 # a dict of headers for a list of dicts | |
| 1152 headers = [headers.get(k, k) for k in keys] | |
| 1153 headers = list(map(_text_type, headers)) | |
| 1154 elif headers == "firstrow": | |
| 1155 if len(rows) > 0: | |
| 1156 headers = [firstdict.get(k, k) for k in keys] | |
| 1157 headers = list(map(_text_type, headers)) | |
| 1158 else: | |
| 1159 headers = [] | |
| 1160 elif headers: | |
| 1161 raise ValueError( | |
| 1162 "headers for a list of dicts is not a dict or a keyword" | |
| 1163 ) | |
| 1164 rows = [[row.get(k) for k in keys] for row in rows] | |
| 1165 | |
| 1166 elif ( | |
| 1167 headers == "keys" | |
| 1168 and hasattr(tabular_data, "description") | |
| 1169 and hasattr(tabular_data, "fetchone") | |
| 1170 and hasattr(tabular_data, "rowcount") | |
| 1171 ): | |
| 1172 # Python Database API cursor object (PEP 0249) | |
| 1173 # print tabulate(cursor, headers='keys') | |
| 1174 headers = [column[0] for column in tabular_data.description] | |
| 1175 | |
| 1176 elif headers == "keys" and len(rows) > 0: | |
| 1177 # keys are column indices | |
| 1178 headers = list(map(_text_type, range(len(rows[0])))) | |
| 1179 | |
| 1180 # take headers from the first row if necessary | |
| 1181 if headers == "firstrow" and len(rows) > 0: | |
| 1182 if index is not None: | |
| 1183 headers = [index[0]] + list(rows[0]) | |
| 1184 index = index[1:] | |
| 1185 else: | |
| 1186 headers = rows[0] | |
| 1187 headers = list(map(_text_type, headers)) # headers should be strings | |
| 1188 rows = rows[1:] | |
| 1189 | |
| 1190 headers = list(map(_text_type, headers)) | |
| 1191 rows = list(map(list, rows)) | |
| 1192 | |
| 1193 # add or remove an index column | |
| 1194 showindex_is_a_str = type(showindex) in [_text_type, _binary_type] | |
| 1195 if showindex == "default" and index is not None: | |
| 1196 rows = _prepend_row_index(rows, index) | |
| 1197 elif isinstance(showindex, Iterable) and not showindex_is_a_str: | |
| 1198 rows = _prepend_row_index(rows, list(showindex)) | |
| 1199 elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str): | |
| 1200 if index is None: | |
| 1201 index = list(range(len(rows))) | |
| 1202 rows = _prepend_row_index(rows, index) | |
| 1203 elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str): | |
| 1204 pass | |
| 1205 | |
| 1206 # pad with empty headers for initial columns if necessary | |
| 1207 if headers and len(rows) > 0: | |
| 1208 nhs = len(headers) | |
| 1209 ncols = len(rows[0]) | |
| 1210 if nhs < ncols: | |
| 1211 headers = [""] * (ncols - nhs) + headers | |
| 1212 | |
| 1213 return rows, headers | |
| 1214 | |
| 1215 | |
| 1216 def tabulate( | |
| 1217 tabular_data, | |
| 1218 headers=(), | |
| 1219 tablefmt="simple", | |
| 1220 floatfmt=_DEFAULT_FLOATFMT, | |
| 1221 numalign=_DEFAULT_ALIGN, | |
| 1222 stralign=_DEFAULT_ALIGN, | |
| 1223 missingval=_DEFAULT_MISSINGVAL, | |
| 1224 showindex="default", | |
| 1225 disable_numparse=False, | |
| 1226 colalign=None, | |
| 1227 ): | |
| 1228 """Format a fixed width table for pretty printing. | |
| 1229 | |
| 1230 >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]])) | |
| 1231 --- --------- | |
| 1232 1 2.34 | |
| 1233 -56 8.999 | |
| 1234 2 10001 | |
| 1235 --- --------- | |
| 1236 | |
| 1237 The first required argument (`tabular_data`) can be a | |
| 1238 list-of-lists (or another iterable of iterables), a list of named | |
| 1239 tuples, a dictionary of iterables, an iterable of dictionaries, | |
| 1240 a two-dimensional NumPy array, NumPy record array, or a Pandas' | |
| 1241 dataframe. | |
| 1242 | |
| 1243 | |
| 1244 Table headers | |
| 1245 ------------- | |
| 1246 | |
| 1247 To print nice column headers, supply the second argument (`headers`): | |
| 1248 | |
| 1249 - `headers` can be an explicit list of column headers | |
| 1250 - if `headers="firstrow"`, then the first row of data is used | |
| 1251 - if `headers="keys"`, then dictionary keys or column indices are used | |
| 1252 | |
| 1253 Otherwise a headerless table is produced. | |
| 1254 | |
| 1255 If the number of headers is less than the number of columns, they | |
| 1256 are supposed to be names of the last columns. This is consistent | |
| 1257 with the plain-text format of R and Pandas' dataframes. | |
| 1258 | |
| 1259 >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]], | |
| 1260 ... headers="firstrow")) | |
| 1261 sex age | |
| 1262 ----- ----- ----- | |
| 1263 Alice F 24 | |
| 1264 Bob M 19 | |
| 1265 | |
| 1266 By default, pandas.DataFrame data have an additional column called | |
| 1267 row index. To add a similar column to all other types of data, | |
| 1268 use `showindex="always"` or `showindex=True`. To suppress row indices | |
| 1269 for all types of data, pass `showindex="never" or `showindex=False`. | |
| 1270 To add a custom row index column, pass `showindex=some_iterable`. | |
| 1271 | |
| 1272 >>> print(tabulate([["F",24],["M",19]], showindex="always")) | |
| 1273 - - -- | |
| 1274 0 F 24 | |
| 1275 1 M 19 | |
| 1276 - - -- | |
| 1277 | |
| 1278 | |
| 1279 Column alignment | |
| 1280 ---------------- | |
| 1281 | |
| 1282 `tabulate` tries to detect column types automatically, and aligns | |
| 1283 the values properly. By default it aligns decimal points of the | |
| 1284 numbers (or flushes integer numbers to the right), and flushes | |
| 1285 everything else to the left. Possible column alignments | |
| 1286 (`numalign`, `stralign`) are: "right", "center", "left", "decimal" | |
| 1287 (only for `numalign`), and None (to disable alignment). | |
| 1288 | |
| 1289 | |
| 1290 Table formats | |
| 1291 ------------- | |
| 1292 | |
| 1293 `floatfmt` is a format specification used for columns which | |
| 1294 contain numeric data with a decimal point. This can also be | |
| 1295 a list or tuple of format strings, one per column. | |
| 1296 | |
| 1297 `None` values are replaced with a `missingval` string (like | |
| 1298 `floatfmt`, this can also be a list of values for different | |
| 1299 columns): | |
| 1300 | |
| 1301 >>> print(tabulate([["spam", 1, None], | |
| 1302 ... ["eggs", 42, 3.14], | |
| 1303 ... ["other", None, 2.7]], missingval="?")) | |
| 1304 ----- -- ---- | |
| 1305 spam 1 ? | |
| 1306 eggs 42 3.14 | |
| 1307 other ? 2.7 | |
| 1308 ----- -- ---- | |
| 1309 | |
| 1310 Various plain-text table formats (`tablefmt`) are supported: | |
| 1311 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki', | |
| 1312 'latex', 'latex_raw', 'latex_booktabs', 'latex_longtable' and tsv. | |
| 1313 Variable `tabulate_formats`contains the list of currently supported formats. | |
| 1314 | |
| 1315 "plain" format doesn't use any pseudographics to draw tables, | |
| 1316 it separates columns with a double space: | |
| 1317 | |
| 1318 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1319 ... ["strings", "numbers"], "plain")) | |
| 1320 strings numbers | |
| 1321 spam 41.9999 | |
| 1322 eggs 451 | |
| 1323 | |
| 1324 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain")) | |
| 1325 spam 41.9999 | |
| 1326 eggs 451 | |
| 1327 | |
| 1328 "simple" format is like Pandoc simple_tables: | |
| 1329 | |
| 1330 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1331 ... ["strings", "numbers"], "simple")) | |
| 1332 strings numbers | |
| 1333 --------- --------- | |
| 1334 spam 41.9999 | |
| 1335 eggs 451 | |
| 1336 | |
| 1337 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple")) | |
| 1338 ---- -------- | |
| 1339 spam 41.9999 | |
| 1340 eggs 451 | |
| 1341 ---- -------- | |
| 1342 | |
| 1343 "grid" is similar to tables produced by Emacs table.el package or | |
| 1344 Pandoc grid_tables: | |
| 1345 | |
| 1346 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1347 ... ["strings", "numbers"], "grid")) | |
| 1348 +-----------+-----------+ | |
| 1349 | strings | numbers | | |
| 1350 +===========+===========+ | |
| 1351 | spam | 41.9999 | | |
| 1352 +-----------+-----------+ | |
| 1353 | eggs | 451 | | |
| 1354 +-----------+-----------+ | |
| 1355 | |
| 1356 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid")) | |
| 1357 +------+----------+ | |
| 1358 | spam | 41.9999 | | |
| 1359 +------+----------+ | |
| 1360 | eggs | 451 | | |
| 1361 +------+----------+ | |
| 1362 | |
| 1363 "fancy_grid" draws a grid using box-drawing characters: | |
| 1364 | |
| 1365 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1366 ... ["strings", "numbers"], "fancy_grid")) | |
| 1367 ╒═══════════╤═══════════╕ | |
| 1368 │ strings │ numbers │ | |
| 1369 ╞═══════════╪═══════════╡ | |
| 1370 │ spam │ 41.9999 │ | |
| 1371 ├───────────┼───────────┤ | |
| 1372 │ eggs │ 451 │ | |
| 1373 ╘═══════════╧═══════════╛ | |
| 1374 | |
| 1375 "pipe" is like tables in PHP Markdown Extra extension or Pandoc | |
| 1376 pipe_tables: | |
| 1377 | |
| 1378 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1379 ... ["strings", "numbers"], "pipe")) | |
| 1380 | strings | numbers | | |
| 1381 |:----------|----------:| | |
| 1382 | spam | 41.9999 | | |
| 1383 | eggs | 451 | | |
| 1384 | |
| 1385 "presto" is like tables produce by the Presto CLI: | |
| 1386 | |
| 1387 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1388 ... ["strings", "numbers"], "presto")) | |
| 1389 strings | numbers | |
| 1390 -----------+----------- | |
| 1391 spam | 41.9999 | |
| 1392 eggs | 451 | |
| 1393 | |
| 1394 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe")) | |
| 1395 |:-----|---------:| | |
| 1396 | spam | 41.9999 | | |
| 1397 | eggs | 451 | | |
| 1398 | |
| 1399 "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They | |
| 1400 are slightly different from "pipe" format by not using colons to | |
| 1401 define column alignment, and using a "+" sign to indicate line | |
| 1402 intersections: | |
| 1403 | |
| 1404 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1405 ... ["strings", "numbers"], "orgtbl")) | |
| 1406 | strings | numbers | | |
| 1407 |-----------+-----------| | |
| 1408 | spam | 41.9999 | | |
| 1409 | eggs | 451 | | |
| 1410 | |
| 1411 | |
| 1412 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl")) | |
| 1413 | spam | 41.9999 | | |
| 1414 | eggs | 451 | | |
| 1415 | |
| 1416 "rst" is like a simple table format from reStructuredText; please | |
| 1417 note that reStructuredText accepts also "grid" tables: | |
| 1418 | |
| 1419 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], | |
| 1420 ... ["strings", "numbers"], "rst")) | |
| 1421 ========= ========= | |
| 1422 strings numbers | |
| 1423 ========= ========= | |
| 1424 spam 41.9999 | |
| 1425 eggs 451 | |
| 1426 ========= ========= | |
| 1427 | |
| 1428 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst")) | |
| 1429 ==== ======== | |
| 1430 spam 41.9999 | |
| 1431 eggs 451 | |
| 1432 ==== ======== | |
| 1433 | |
| 1434 "mediawiki" produces a table markup used in Wikipedia and on other | |
| 1435 MediaWiki-based sites: | |
| 1436 | |
| 1437 >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], | |
| 1438 ... headers="firstrow", tablefmt="mediawiki")) | |
| 1439 {| class="wikitable" style="text-align: left;" | |
| 1440 |+ <!-- caption --> | |
| 1441 |- | |
| 1442 ! strings !! align="right"| numbers | |
| 1443 |- | |
| 1444 | spam || align="right"| 41.9999 | |
| 1445 |- | |
| 1446 | eggs || align="right"| 451 | |
| 1447 |} | |
| 1448 | |
| 1449 "html" produces HTML markup as an html.escape'd str | |
| 1450 with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML | |
| 1451 and a .str property so that the raw HTML remains accessible | |
| 1452 the unsafehtml table format can be used if an unescaped HTML format is required: | |
| 1453 | |
| 1454 >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], | |
| 1455 ... headers="firstrow", tablefmt="html")) | |
| 1456 <table> | |
| 1457 <thead> | |
| 1458 <tr><th>strings </th><th style="text-align: right;"> numbers</th></tr> | |
| 1459 </thead> | |
| 1460 <tbody> | |
| 1461 <tr><td>spam </td><td style="text-align: right;"> 41.9999</td></tr> | |
| 1462 <tr><td>eggs </td><td style="text-align: right;"> 451 </td></tr> | |
| 1463 </tbody> | |
| 1464 </table> | |
| 1465 | |
| 1466 "latex" produces a tabular environment of LaTeX document markup: | |
| 1467 | |
| 1468 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex")) | |
| 1469 \\begin{tabular}{lr} | |
| 1470 \\hline | |
| 1471 spam & 41.9999 \\\\ | |
| 1472 eggs & 451 \\\\ | |
| 1473 \\hline | |
| 1474 \\end{tabular} | |
| 1475 | |
| 1476 "latex_raw" is similar to "latex", but doesn't escape special characters, | |
| 1477 such as backslash and underscore, so LaTeX commands may embedded into | |
| 1478 cells' values: | |
| 1479 | |
| 1480 >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], tablefmt="latex_raw")) | |
| 1481 \\begin{tabular}{lr} | |
| 1482 \\hline | |
| 1483 spam$_9$ & 41.9999 \\\\ | |
| 1484 \\emph{eggs} & 451 \\\\ | |
| 1485 \\hline | |
| 1486 \\end{tabular} | |
| 1487 | |
| 1488 "latex_booktabs" produces a tabular environment of LaTeX document markup | |
| 1489 using the booktabs.sty package: | |
| 1490 | |
| 1491 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs")) | |
| 1492 \\begin{tabular}{lr} | |
| 1493 \\toprule | |
| 1494 spam & 41.9999 \\\\ | |
| 1495 eggs & 451 \\\\ | |
| 1496 \\bottomrule | |
| 1497 \\end{tabular} | |
| 1498 | |
| 1499 "latex_longtable" produces a tabular environment that can stretch along | |
| 1500 multiple pages, using the longtable package for LaTeX. | |
| 1501 | |
| 1502 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_longtable")) | |
| 1503 \\begin{longtable}{lr} | |
| 1504 \\hline | |
| 1505 spam & 41.9999 \\\\ | |
| 1506 eggs & 451 \\\\ | |
| 1507 \\hline | |
| 1508 \\end{longtable} | |
| 1509 | |
| 1510 | |
| 1511 Number parsing | |
| 1512 -------------- | |
| 1513 By default, anything which can be parsed as a number is a number. | |
| 1514 This ensures numbers represented as strings are aligned properly. | |
| 1515 This can lead to weird results for particular strings such as | |
| 1516 specific git SHAs e.g. "42992e1" will be parsed into the number | |
| 1517 429920 and aligned as such. | |
| 1518 | |
| 1519 To completely disable number parsing (and alignment), use | |
| 1520 `disable_numparse=True`. For more fine grained control, a list column | |
| 1521 indices is used to disable number parsing only on those columns | |
| 1522 e.g. `disable_numparse=[0, 2]` would disable number parsing only on the | |
| 1523 first and third columns. | |
| 1524 """ | |
| 1525 | |
| 1526 if tabular_data is None: | |
| 1527 tabular_data = [] | |
| 1528 list_of_lists, headers = _normalize_tabular_data( | |
| 1529 tabular_data, headers, showindex=showindex | |
| 1530 ) | |
| 1531 | |
| 1532 # empty values in the first column of RST tables should be escaped (issue #82) | |
| 1533 # "" should be escaped as "\\ " or ".." | |
| 1534 if tablefmt == "rst": | |
| 1535 list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers) | |
| 1536 | |
| 1537 # PrettyTable formatting does not use any extra padding. | |
| 1538 # Numbers are not parsed and are treated the same as strings for alignment. | |
| 1539 # Check if pretty is the format being used and override the defaults so it | |
| 1540 # does not impact other formats. | |
| 1541 min_padding = MIN_PADDING | |
| 1542 if tablefmt == "pretty": | |
| 1543 min_padding = 0 | |
| 1544 disable_numparse = True | |
| 1545 numalign = "center" if numalign == _DEFAULT_ALIGN else numalign | |
| 1546 stralign = "center" if stralign == _DEFAULT_ALIGN else stralign | |
| 1547 else: | |
| 1548 numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign | |
| 1549 stralign = "left" if stralign == _DEFAULT_ALIGN else stralign | |
| 1550 | |
| 1551 # optimization: look for ANSI control codes once, | |
| 1552 # enable smart width functions only if a control code is found | |
| 1553 plain_text = "\t".join( | |
| 1554 ["\t".join(map(_text_type, headers))] | |
| 1555 + ["\t".join(map(_text_type, row)) for row in list_of_lists] | |
| 1556 ) | |
| 1557 | |
| 1558 has_invisible = re.search(_invisible_codes, plain_text) | |
| 1559 if not has_invisible: | |
| 1560 has_invisible = re.search(_invisible_codes_link, plain_text) | |
| 1561 enable_widechars = wcwidth is not None and WIDE_CHARS_MODE | |
| 1562 if ( | |
| 1563 not isinstance(tablefmt, TableFormat) | |
| 1564 and tablefmt in multiline_formats | |
| 1565 and _is_multiline(plain_text) | |
| 1566 ): | |
| 1567 tablefmt = multiline_formats.get(tablefmt, tablefmt) | |
| 1568 is_multiline = True | |
| 1569 else: | |
| 1570 is_multiline = False | |
| 1571 width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) | |
| 1572 | |
| 1573 # format rows and columns, convert numeric values to strings | |
| 1574 cols = list(izip_longest(*list_of_lists)) | |
| 1575 numparses = _expand_numparse(disable_numparse, len(cols)) | |
| 1576 coltypes = [_column_type(col, numparse=np) for col, np in zip(cols, numparses)] | |
| 1577 if isinstance(floatfmt, basestring): # old version | |
| 1578 float_formats = len(cols) * [ | |
| 1579 floatfmt | |
| 1580 ] # just duplicate the string to use in each column | |
| 1581 else: # if floatfmt is list, tuple etc we have one per column | |
| 1582 float_formats = list(floatfmt) | |
| 1583 if len(float_formats) < len(cols): | |
| 1584 float_formats.extend((len(cols) - len(float_formats)) * [_DEFAULT_FLOATFMT]) | |
| 1585 if isinstance(missingval, basestring): | |
| 1586 missing_vals = len(cols) * [missingval] | |
| 1587 else: | |
| 1588 missing_vals = list(missingval) | |
| 1589 if len(missing_vals) < len(cols): | |
| 1590 missing_vals.extend((len(cols) - len(missing_vals)) * [_DEFAULT_MISSINGVAL]) | |
| 1591 cols = [ | |
| 1592 [_format(v, ct, fl_fmt, miss_v, has_invisible) for v in c] | |
| 1593 for c, ct, fl_fmt, miss_v in zip(cols, coltypes, float_formats, missing_vals) | |
| 1594 ] | |
| 1595 | |
| 1596 # align columns | |
| 1597 aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] | |
| 1598 if colalign is not None: | |
| 1599 assert isinstance(colalign, Iterable) | |
| 1600 for idx, align in enumerate(colalign): | |
| 1601 aligns[idx] = align | |
| 1602 minwidths = ( | |
| 1603 [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) | |
| 1604 ) | |
| 1605 cols = [ | |
| 1606 _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) | |
| 1607 for c, a, minw in zip(cols, aligns, minwidths) | |
| 1608 ] | |
| 1609 | |
| 1610 if headers: | |
| 1611 # align headers and add headers | |
| 1612 t_cols = cols or [[""]] * len(headers) | |
| 1613 t_aligns = aligns or [stralign] * len(headers) | |
| 1614 minwidths = [ | |
| 1615 max(minw, max(width_fn(cl) for cl in c)) | |
| 1616 for minw, c in zip(minwidths, t_cols) | |
| 1617 ] | |
| 1618 headers = [ | |
| 1619 _align_header(h, a, minw, width_fn(h), is_multiline, width_fn) | |
| 1620 for h, a, minw in zip(headers, t_aligns, minwidths) | |
| 1621 ] | |
| 1622 rows = list(zip(*cols)) | |
| 1623 else: | |
| 1624 minwidths = [max(width_fn(cl) for cl in c) for c in cols] | |
| 1625 rows = list(zip(*cols)) | |
| 1626 | |
| 1627 if not isinstance(tablefmt, TableFormat): | |
| 1628 tablefmt = _table_formats.get(tablefmt, _table_formats["simple"]) | |
| 1629 | |
| 1630 return _format_table(tablefmt, headers, rows, minwidths, aligns, is_multiline) | |
| 1631 | |
| 1632 | |
| 1633 def _expand_numparse(disable_numparse, column_count): | |
| 1634 """ | |
| 1635 Return a list of bools of length `column_count` which indicates whether | |
| 1636 number parsing should be used on each column. | |
| 1637 If `disable_numparse` is a list of indices, each of those indices are False, | |
| 1638 and everything else is True. | |
| 1639 If `disable_numparse` is a bool, then the returned list is all the same. | |
| 1640 """ | |
| 1641 if isinstance(disable_numparse, Iterable): | |
| 1642 numparses = [True] * column_count | |
| 1643 for index in disable_numparse: | |
| 1644 numparses[index] = False | |
| 1645 return numparses | |
| 1646 else: | |
| 1647 return [not disable_numparse] * column_count | |
| 1648 | |
| 1649 | |
| 1650 def _pad_row(cells, padding): | |
| 1651 if cells: | |
| 1652 pad = " " * padding | |
| 1653 padded_cells = [pad + cell + pad for cell in cells] | |
| 1654 return padded_cells | |
| 1655 else: | |
| 1656 return cells | |
| 1657 | |
| 1658 | |
| 1659 def _build_simple_row(padded_cells, rowfmt): | |
| 1660 "Format row according to DataRow format without padding." | |
| 1661 begin, sep, end = rowfmt | |
| 1662 return (begin + sep.join(padded_cells) + end).rstrip() | |
| 1663 | |
| 1664 | |
| 1665 def _build_row(padded_cells, colwidths, colaligns, rowfmt): | |
| 1666 "Return a string which represents a row of data cells." | |
| 1667 if not rowfmt: | |
| 1668 return None | |
| 1669 if hasattr(rowfmt, "__call__"): | |
| 1670 return rowfmt(padded_cells, colwidths, colaligns) | |
| 1671 else: | |
| 1672 return _build_simple_row(padded_cells, rowfmt) | |
| 1673 | |
| 1674 | |
| 1675 def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt): | |
| 1676 lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt)) | |
| 1677 return lines | |
| 1678 | |
| 1679 | |
| 1680 def _append_multiline_row( | |
| 1681 lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad | |
| 1682 ): | |
| 1683 colwidths = [w - 2 * pad for w in padded_widths] | |
| 1684 cells_lines = [c.splitlines() for c in padded_multiline_cells] | |
| 1685 nlines = max(map(len, cells_lines)) # number of lines in the row | |
| 1686 # vertically pad cells where some lines are missing | |
| 1687 cells_lines = [ | |
| 1688 (cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths) | |
| 1689 ] | |
| 1690 lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)] | |
| 1691 for ln in lines_cells: | |
| 1692 padded_ln = _pad_row(ln, pad) | |
| 1693 _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt) | |
| 1694 return lines | |
| 1695 | |
| 1696 | |
| 1697 def _build_line(colwidths, colaligns, linefmt): | |
| 1698 "Return a string which represents a horizontal line." | |
| 1699 if not linefmt: | |
| 1700 return None | |
| 1701 if hasattr(linefmt, "__call__"): | |
| 1702 return linefmt(colwidths, colaligns) | |
| 1703 else: | |
| 1704 begin, fill, sep, end = linefmt | |
| 1705 cells = [fill * w for w in colwidths] | |
| 1706 return _build_simple_row(cells, (begin, sep, end)) | |
| 1707 | |
| 1708 | |
| 1709 def _append_line(lines, colwidths, colaligns, linefmt): | |
| 1710 lines.append(_build_line(colwidths, colaligns, linefmt)) | |
| 1711 return lines | |
| 1712 | |
| 1713 | |
| 1714 class JupyterHTMLStr(str): | |
| 1715 """Wrap the string with a _repr_html_ method so that Jupyter | |
| 1716 displays the HTML table""" | |
| 1717 | |
| 1718 def _repr_html_(self): | |
| 1719 return self | |
| 1720 | |
| 1721 @property | |
| 1722 def str(self): | |
| 1723 """add a .str property so that the raw string is still accessible""" | |
| 1724 return self | |
| 1725 | |
| 1726 | |
| 1727 def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline): | |
| 1728 """Produce a plain-text representation of the table.""" | |
| 1729 lines = [] | |
| 1730 hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] | |
| 1731 pad = fmt.padding | |
| 1732 headerrow = fmt.headerrow | |
| 1733 | |
| 1734 padded_widths = [(w + 2 * pad) for w in colwidths] | |
| 1735 if is_multiline: | |
| 1736 pad_row = lambda row, _: row # noqa do it later, in _append_multiline_row | |
| 1737 append_row = partial(_append_multiline_row, pad=pad) | |
| 1738 else: | |
| 1739 pad_row = _pad_row | |
| 1740 append_row = _append_basic_row | |
| 1741 | |
| 1742 padded_headers = pad_row(headers, pad) | |
| 1743 padded_rows = [pad_row(row, pad) for row in rows] | |
| 1744 | |
| 1745 if fmt.lineabove and "lineabove" not in hidden: | |
| 1746 _append_line(lines, padded_widths, colaligns, fmt.lineabove) | |
| 1747 | |
| 1748 if padded_headers: | |
| 1749 append_row(lines, padded_headers, padded_widths, colaligns, headerrow) | |
| 1750 if fmt.linebelowheader and "linebelowheader" not in hidden: | |
| 1751 _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) | |
| 1752 | |
| 1753 if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: | |
| 1754 # initial rows with a line below | |
| 1755 for row in padded_rows[:-1]: | |
| 1756 append_row(lines, row, padded_widths, colaligns, fmt.datarow) | |
| 1757 _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) | |
| 1758 # the last row without a line below | |
| 1759 append_row(lines, padded_rows[-1], padded_widths, colaligns, fmt.datarow) | |
| 1760 else: | |
| 1761 for row in padded_rows: | |
| 1762 append_row(lines, row, padded_widths, colaligns, fmt.datarow) | |
| 1763 | |
| 1764 if fmt.linebelow and "linebelow" not in hidden: | |
| 1765 _append_line(lines, padded_widths, colaligns, fmt.linebelow) | |
| 1766 | |
| 1767 if headers or rows: | |
| 1768 output = "\n".join(lines) | |
| 1769 if fmt.lineabove == _html_begin_table_without_header: | |
| 1770 return JupyterHTMLStr(output) | |
| 1771 else: | |
| 1772 return output | |
| 1773 else: # a completely empty table | |
| 1774 return "" | |
| 1775 | |
| 1776 | |
| 1777 def _main(): | |
| 1778 """\ | |
| 1779 Usage: tabulate [options] [FILE ...] | |
| 1780 | |
| 1781 Pretty-print tabular data. | |
| 1782 See also https://github.com/astanin/python-tabulate | |
| 1783 | |
| 1784 FILE a filename of the file with tabular data; | |
| 1785 if "-" or missing, read data from stdin. | |
| 1786 | |
| 1787 Options: | |
| 1788 | |
| 1789 -h, --help show this message | |
| 1790 -1, --header use the first row of data as a table header | |
| 1791 -o FILE, --output FILE print table to FILE (default: stdout) | |
| 1792 -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace) | |
| 1793 -F FPFMT, --float FPFMT floating point number format (default: g) | |
| 1794 -f FMT, --format FMT set output table format; supported formats: | |
| 1795 plain, simple, grid, fancy_grid, pipe, orgtbl, | |
| 1796 rst, mediawiki, html, latex, latex_raw, | |
| 1797 latex_booktabs, latex_longtable, tsv | |
| 1798 (default: simple) | |
| 1799 """ | |
| 1800 import getopt | |
| 1801 import sys | |
| 1802 import textwrap | |
| 1803 | |
| 1804 usage = textwrap.dedent(_main.__doc__) | |
| 1805 try: | |
| 1806 opts, args = getopt.getopt( | |
| 1807 sys.argv[1:], | |
| 1808 "h1o:s:F:A:f:", | |
| 1809 ["help", "header", "output", "sep=", "float=", "align=", "format="], | |
| 1810 ) | |
| 1811 except getopt.GetoptError as e: | |
| 1812 print(e) | |
| 1813 print(usage) | |
| 1814 sys.exit(2) | |
| 1815 headers = [] | |
| 1816 floatfmt = _DEFAULT_FLOATFMT | |
| 1817 colalign = None | |
| 1818 tablefmt = "simple" | |
| 1819 sep = r"\s+" | |
| 1820 outfile = "-" | |
| 1821 for opt, value in opts: | |
| 1822 if opt in ["-1", "--header"]: | |
| 1823 headers = "firstrow" | |
| 1824 elif opt in ["-o", "--output"]: | |
| 1825 outfile = value | |
| 1826 elif opt in ["-F", "--float"]: | |
| 1827 floatfmt = value | |
| 1828 elif opt in ["-C", "--colalign"]: | |
| 1829 colalign = value.split() | |
| 1830 elif opt in ["-f", "--format"]: | |
| 1831 if value not in tabulate_formats: | |
| 1832 print("%s is not a supported table format" % value) | |
| 1833 print(usage) | |
| 1834 sys.exit(3) | |
| 1835 tablefmt = value | |
| 1836 elif opt in ["-s", "--sep"]: | |
| 1837 sep = value | |
| 1838 elif opt in ["-h", "--help"]: | |
| 1839 print(usage) | |
| 1840 sys.exit(0) | |
| 1841 files = [sys.stdin] if not args else args | |
| 1842 with (sys.stdout if outfile == "-" else open(outfile, "w")) as out: | |
| 1843 for f in files: | |
| 1844 if f == "-": | |
| 1845 f = sys.stdin | |
| 1846 if _is_file(f): | |
| 1847 _pprint_file( | |
| 1848 f, | |
| 1849 headers=headers, | |
| 1850 tablefmt=tablefmt, | |
| 1851 sep=sep, | |
| 1852 floatfmt=floatfmt, | |
| 1853 file=out, | |
| 1854 colalign=colalign, | |
| 1855 ) | |
| 1856 else: | |
| 1857 with open(f) as fobj: | |
| 1858 _pprint_file( | |
| 1859 fobj, | |
| 1860 headers=headers, | |
| 1861 tablefmt=tablefmt, | |
| 1862 sep=sep, | |
| 1863 floatfmt=floatfmt, | |
| 1864 file=out, | |
| 1865 colalign=colalign, | |
| 1866 ) | |
| 1867 | |
| 1868 | |
| 1869 def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, file, colalign): | |
| 1870 rows = fobject.readlines() | |
| 1871 table = [re.split(sep, r.rstrip()) for r in rows if r.strip()] | |
| 1872 print( | |
| 1873 tabulate(table, headers, tablefmt, floatfmt=floatfmt, colalign=colalign), | |
| 1874 file=file, | |
| 1875 ) | |
| 1876 | |
| 1877 | |
| 1878 if __name__ == "__main__": | |
| 1879 _main() |
