tabulate.py 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237
  1. # -*- coding: utf-8 -*-
  2. """Pretty-print tabular data."""
  3. from __future__ import print_function
  4. from __future__ import unicode_literals
  5. from collections import namedtuple, Iterable
  6. from platform import python_version_tuple
  7. import re
  8. if python_version_tuple()[0] < "3":
  9. from itertools import izip_longest
  10. from functools import partial
  11. _none_type = type(None)
  12. _int_type = int
  13. _long_type = long
  14. _float_type = float
  15. _text_type = unicode
  16. _binary_type = str
  17. def _is_file(f):
  18. return isinstance(f, file)
  19. else:
  20. from itertools import zip_longest as izip_longest
  21. from functools import reduce, partial
  22. _none_type = type(None)
  23. _int_type = int
  24. _long_type = int
  25. _float_type = float
  26. _text_type = str
  27. _binary_type = bytes
  28. import io
  29. def _is_file(f):
  30. return isinstance(f, io.IOBase)
  31. try:
  32. import wcwidth # optional wide-character (CJK) support
  33. except ImportError:
  34. wcwidth = None
  35. __all__ = ["tabulate", "tabulate_formats", "simple_separated_format"]
  36. __version__ = "0.7.6-dev"
  37. # minimum extra space in headers
  38. MIN_PADDING = 2
  39. # if True, enable wide-character (CJK) support
  40. WIDE_CHARS_MODE = wcwidth is not None
  41. Line = namedtuple("Line", ["begin", "hline", "sep", "end"])
  42. DataRow = namedtuple("DataRow", ["begin", "sep", "end"])
  43. # A table structure is suppposed to be:
  44. #
  45. # --- lineabove ---------
  46. # headerrow
  47. # --- linebelowheader ---
  48. # datarow
  49. # --- linebewteenrows ---
  50. # ... (more datarows) ...
  51. # --- linebewteenrows ---
  52. # last datarow
  53. # --- linebelow ---------
  54. #
  55. # TableFormat's line* elements can be
  56. #
  57. # - either None, if the element is not used,
  58. # - or a Line tuple,
  59. # - or a function: [col_widths], [col_alignments] -> string.
  60. #
  61. # TableFormat's *row elements can be
  62. #
  63. # - either None, if the element is not used,
  64. # - or a DataRow tuple,
  65. # - or a function: [cell_values], [col_widths], [col_alignments] -> string.
  66. #
  67. # padding (an integer) is the amount of white space around data values.
  68. #
  69. # with_header_hide:
  70. #
  71. # - either None, to display all table elements unconditionally,
  72. # - or a list of elements not to be displayed if the table has column headers.
  73. #
  74. TableFormat = namedtuple("TableFormat", ["lineabove", "linebelowheader",
  75. "linebetweenrows", "linebelow",
  76. "headerrow", "datarow",
  77. "padding", "with_header_hide"])
  78. def _pipe_segment_with_colons(align, colwidth):
  79. """Return a segment of a horizontal line with optional colons which
  80. indicate column's alignment (as in `pipe` output format)."""
  81. w = colwidth
  82. if align in ["right", "decimal"]:
  83. return ('-' * (w - 1)) + ":"
  84. elif align == "center":
  85. return ":" + ('-' * (w - 2)) + ":"
  86. elif align == "left":
  87. return ":" + ('-' * (w - 1))
  88. else:
  89. return '-' * w
  90. def _pipe_line_with_colons(colwidths, colaligns):
  91. """Return a horizontal line with optional colons to indicate column's
  92. alignment (as in `pipe` output format)."""
  93. segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)]
  94. return "|" + "|".join(segments) + "|"
  95. def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
  96. alignment = { "left": '',
  97. "right": 'align="right"| ',
  98. "center": 'align="center"| ',
  99. "decimal": 'align="right"| ' }
  100. # hard-coded padding _around_ align attribute and value together
  101. # rather than padding parameter which affects only the value
  102. values_with_attrs = [' ' + alignment.get(a, '') + c + ' '
  103. for c, a in zip(cell_values, colaligns)]
  104. colsep = separator*2
  105. return (separator + colsep.join(values_with_attrs)).rstrip()
  106. def _textile_row_with_attrs(cell_values, colwidths, colaligns):
  107. cell_values[0] += ' '
  108. alignment = { "left": "<.", "right": ">.", "center": "=.", "decimal": ">." }
  109. values = (alignment.get(a, '') + v for a, v in zip(colaligns, cell_values))
  110. return '|' + '|'.join(values) + '|'
  111. def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore):
  112. # this table header will be suppressed if there is a header row
  113. return "\n".join(["<table>", "<tbody>"])
  114. def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns):
  115. alignment = { "left": '',
  116. "right": ' style="text-align: right;"',
  117. "center": ' style="text-align: center;"',
  118. "decimal": ' style="text-align: right;"' }
  119. values_with_attrs = ["<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ''), c)
  120. for c, a in zip(cell_values, colaligns)]
  121. rowhtml = "<tr>" + "".join(values_with_attrs).rstrip() + "</tr>"
  122. if celltag == "th": # it's a header row, create a new table header
  123. rowhtml = "\n".join(["<table>",
  124. "<thead>",
  125. rowhtml,
  126. "</thead>",
  127. "<tbody>"])
  128. return rowhtml
  129. def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=''):
  130. alignment = { "left": '',
  131. "right": '<style="text-align: right;">',
  132. "center": '<style="text-align: center;">',
  133. "decimal": '<style="text-align: right;">' }
  134. values_with_attrs = ["{0}{1} {2} ".format(celltag,
  135. alignment.get(a, ''),
  136. header+c+header)
  137. for c, a in zip(cell_values, colaligns)]
  138. return "".join(values_with_attrs)+"||"
  139. def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False):
  140. alignment = { "left": "l", "right": "r", "center": "c", "decimal": "r" }
  141. tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns])
  142. return "\n".join(["\\begin{tabular}{" + tabular_columns_fmt + "}",
  143. "\\toprule" if booktabs else "\hline"])
  144. LATEX_ESCAPE_RULES = {r"&": r"\&", r"%": r"\%", r"$": r"\$", r"#": r"\#",
  145. r"_": r"\_", r"^": r"\^{}", r"{": r"\{", r"}": r"\}",
  146. r"~": r"\textasciitilde{}", "\\": r"\textbackslash{}",
  147. r"<": r"\ensuremath{<}", r">": r"\ensuremath{>}"}
  148. def _latex_row(cell_values, colwidths, colaligns):
  149. def escape_char(c):
  150. return LATEX_ESCAPE_RULES.get(c, c)
  151. escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values]
  152. rowfmt = DataRow("", "&", "\\\\")
  153. return _build_simple_row(escaped_values, rowfmt)
  154. _table_formats = {"simple":
  155. TableFormat(lineabove=Line("", "-", " ", ""),
  156. linebelowheader=Line("", "-", " ", ""),
  157. linebetweenrows=None,
  158. linebelow=Line("", "-", " ", ""),
  159. headerrow=DataRow("", " ", ""),
  160. datarow=DataRow("", " ", ""),
  161. padding=0,
  162. with_header_hide=["lineabove", "linebelow"]),
  163. "plain":
  164. TableFormat(lineabove=None, linebelowheader=None,
  165. linebetweenrows=None, linebelow=None,
  166. headerrow=DataRow("", " ", ""),
  167. datarow=DataRow("", " ", ""),
  168. padding=0, with_header_hide=None),
  169. "grid":
  170. TableFormat(lineabove=Line("+", "-", "+", "+"),
  171. linebelowheader=Line("+", "=", "+", "+"),
  172. linebetweenrows=Line("+", "-", "+", "+"),
  173. linebelow=Line("+", "-", "+", "+"),
  174. headerrow=DataRow("|", "|", "|"),
  175. datarow=DataRow("|", "|", "|"),
  176. padding=1, with_header_hide=None),
  177. "fancy_grid":
  178. TableFormat(lineabove=Line("╒", "═", "╤", "╕"),
  179. linebelowheader=Line("╞", "═", "╪", "╡"),
  180. linebetweenrows=Line("├", "─", "┼", "┤"),
  181. linebelow=Line("╘", "═", "╧", "╛"),
  182. headerrow=DataRow("│", "│", "│"),
  183. datarow=DataRow("│", "│", "│"),
  184. padding=1, with_header_hide=None),
  185. "pipe":
  186. TableFormat(lineabove=_pipe_line_with_colons,
  187. linebelowheader=_pipe_line_with_colons,
  188. linebetweenrows=None,
  189. linebelow=None,
  190. headerrow=DataRow("|", "|", "|"),
  191. datarow=DataRow("|", "|", "|"),
  192. padding=1,
  193. with_header_hide=["lineabove"]),
  194. "orgtbl":
  195. TableFormat(lineabove=None,
  196. linebelowheader=Line("|", "-", "+", "|"),
  197. linebetweenrows=None,
  198. linebelow=None,
  199. headerrow=DataRow("|", "|", "|"),
  200. datarow=DataRow("|", "|", "|"),
  201. padding=1, with_header_hide=None),
  202. "jira":
  203. TableFormat(lineabove=None,
  204. linebelowheader=None,
  205. linebetweenrows=None,
  206. linebelow=None,
  207. headerrow=DataRow("||", "||", "||"),
  208. datarow=DataRow("|", "|", "|"),
  209. padding=1, with_header_hide=None),
  210. "psql":
  211. TableFormat(lineabove=Line("+", "-", "+", "+"),
  212. linebelowheader=Line("|", "-", "+", "|"),
  213. linebetweenrows=None,
  214. linebelow=Line("+", "-", "+", "+"),
  215. headerrow=DataRow("|", "|", "|"),
  216. datarow=DataRow("|", "|", "|"),
  217. padding=1, with_header_hide=None),
  218. "rst":
  219. TableFormat(lineabove=Line("", "=", " ", ""),
  220. linebelowheader=Line("", "=", " ", ""),
  221. linebetweenrows=None,
  222. linebelow=Line("", "=", " ", ""),
  223. headerrow=DataRow("", " ", ""),
  224. datarow=DataRow("", " ", ""),
  225. padding=0, with_header_hide=None),
  226. "mediawiki":
  227. TableFormat(lineabove=Line("{| class=\"wikitable\" style=\"text-align: left;\"",
  228. "", "", "\n|+ <!-- caption -->\n|-"),
  229. linebelowheader=Line("|-", "", "", ""),
  230. linebetweenrows=Line("|-", "", "", ""),
  231. linebelow=Line("|}", "", "", ""),
  232. headerrow=partial(_mediawiki_row_with_attrs, "!"),
  233. datarow=partial(_mediawiki_row_with_attrs, "|"),
  234. padding=0, with_header_hide=None),
  235. "moinmoin":
  236. TableFormat(lineabove=None,
  237. linebelowheader=None,
  238. linebetweenrows=None,
  239. linebelow=None,
  240. headerrow=partial(_moin_row_with_attrs,"||",header="'''"),
  241. datarow=partial(_moin_row_with_attrs,"||"),
  242. padding=1, with_header_hide=None),
  243. "html":
  244. TableFormat(lineabove=_html_begin_table_without_header,
  245. linebelowheader="",
  246. linebetweenrows=None,
  247. linebelow=Line("</tbody>\n</table>", "", "", ""),
  248. headerrow=partial(_html_row_with_attrs, "th"),
  249. datarow=partial(_html_row_with_attrs, "td"),
  250. padding=0, with_header_hide=["lineabove"]),
  251. "latex":
  252. TableFormat(lineabove=_latex_line_begin_tabular,
  253. linebelowheader=Line("\\hline", "", "", ""),
  254. linebetweenrows=None,
  255. linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
  256. headerrow=_latex_row,
  257. datarow=_latex_row,
  258. padding=1, with_header_hide=None),
  259. "latex_booktabs":
  260. TableFormat(lineabove=partial(_latex_line_begin_tabular, booktabs=True),
  261. linebelowheader=Line("\\midrule", "", "", ""),
  262. linebetweenrows=None,
  263. linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""),
  264. headerrow=_latex_row,
  265. datarow=_latex_row,
  266. padding=1, with_header_hide=None),
  267. "tsv":
  268. TableFormat(lineabove=None, linebelowheader=None,
  269. linebetweenrows=None, linebelow=None,
  270. headerrow=DataRow("", "\t", ""),
  271. datarow=DataRow("", "\t", ""),
  272. padding=0, with_header_hide=None),
  273. "textile":
  274. TableFormat(lineabove=None, linebelowheader=None,
  275. linebetweenrows=None, linebelow=None,
  276. headerrow=DataRow("|_. ", "|_.", "|"),
  277. datarow=_textile_row_with_attrs,
  278. padding=1, with_header_hide=None)}
  279. tabulate_formats = list(sorted(_table_formats.keys()))
  280. _invisible_codes = re.compile(r"\x1b\[\d*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes
  281. _invisible_codes_bytes = re.compile(b"\x1b\[\d*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes
  282. def simple_separated_format(separator):
  283. """Construct a simple TableFormat with columns separated by a separator.
  284. >>> tsv = simple_separated_format("\\t") ; \
  285. tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23'
  286. True
  287. """
  288. return TableFormat(None, None, None, None,
  289. headerrow=DataRow('', separator, ''),
  290. datarow=DataRow('', separator, ''),
  291. padding=0, with_header_hide=None)
  292. def _isconvertible(conv, string):
  293. try:
  294. n = conv(string)
  295. return True
  296. except (ValueError, TypeError):
  297. return False
  298. def _isnumber(string):
  299. """
  300. >>> _isnumber("123.45")
  301. True
  302. >>> _isnumber("123")
  303. True
  304. >>> _isnumber("spam")
  305. False
  306. """
  307. return _isconvertible(float, string)
  308. def _isint(string, inttype=int):
  309. """
  310. >>> _isint("123")
  311. True
  312. >>> _isint("123.45")
  313. False
  314. """
  315. return type(string) is inttype or\
  316. (isinstance(string, _binary_type) or isinstance(string, _text_type))\
  317. and\
  318. _isconvertible(inttype, string)
  319. def _type(string, has_invisible=True):
  320. """The least generic type (type(None), int, float, str, unicode).
  321. >>> _type(None) is type(None)
  322. True
  323. >>> _type("foo") is type("")
  324. True
  325. >>> _type("1") is type(1)
  326. True
  327. >>> _type('\x1b[31m42\x1b[0m') is type(42)
  328. True
  329. >>> _type('\x1b[31m42\x1b[0m') is type(42)
  330. True
  331. """
  332. if has_invisible and \
  333. (isinstance(string, _text_type) or isinstance(string, _binary_type)):
  334. string = _strip_invisible(string)
  335. if string is None:
  336. return _none_type
  337. elif hasattr(string, "isoformat"): # datetime.datetime, date, and time
  338. return _text_type
  339. elif _isint(string):
  340. return int
  341. elif _isint(string, _long_type):
  342. return int
  343. elif _isnumber(string):
  344. return float
  345. elif isinstance(string, _binary_type):
  346. return _binary_type
  347. else:
  348. return _text_type
  349. def _afterpoint(string):
  350. """Symbols after a decimal point, -1 if the string lacks the decimal point.
  351. >>> _afterpoint("123.45")
  352. 2
  353. >>> _afterpoint("1001")
  354. -1
  355. >>> _afterpoint("eggs")
  356. -1
  357. >>> _afterpoint("123e45")
  358. 2
  359. """
  360. if _isnumber(string):
  361. if _isint(string):
  362. return -1
  363. else:
  364. pos = string.rfind(".")
  365. pos = string.lower().rfind("e") if pos < 0 else pos
  366. if pos >= 0:
  367. return len(string) - pos - 1
  368. else:
  369. return -1 # no point
  370. else:
  371. return -1 # not a number
  372. def _padleft(width, s):
  373. """Flush right.
  374. >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430'
  375. True
  376. """
  377. fmt = "{0:>%ds}" % width
  378. return fmt.format(s)
  379. def _padright(width, s):
  380. """Flush left.
  381. >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 '
  382. True
  383. """
  384. fmt = "{0:<%ds}" % width
  385. return fmt.format(s)
  386. def _padboth(width, s):
  387. """Center string.
  388. >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 '
  389. True
  390. """
  391. fmt = "{0:^%ds}" % width
  392. return fmt.format(s)
  393. def _strip_invisible(s):
  394. "Remove invisible ANSI color codes."
  395. if isinstance(s, _text_type):
  396. return re.sub(_invisible_codes, "", s)
  397. else: # a bytestring
  398. return re.sub(_invisible_codes_bytes, "", s)
  399. def _visible_width(s):
  400. """Visible width of a printed string. ANSI color codes are removed.
  401. >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world")
  402. (5, 5)
  403. """
  404. # optional wide-character support
  405. if wcwidth is not None and WIDE_CHARS_MODE:
  406. len_fn = wcwidth.wcswidth
  407. else:
  408. len_fn = len
  409. if isinstance(s, _text_type) or isinstance(s, _binary_type):
  410. return len_fn(_strip_invisible(s))
  411. else:
  412. return len_fn(_text_type(s))
  413. def _align_column(strings, alignment, minwidth=0, has_invisible=True):
  414. """[string] -> [padded_string]
  415. >>> list(map(str,_align_column(["12.345", "-1234.5", "1.23", "1234.5", "1e+234", "1.0e234"], "decimal")))
  416. [' 12.345 ', '-1234.5 ', ' 1.23 ', ' 1234.5 ', ' 1e+234 ', ' 1.0e234']
  417. >>> list(map(str,_align_column(['123.4', '56.7890'], None)))
  418. ['123.4', '56.7890']
  419. """
  420. if alignment == "right":
  421. strings = [s.strip() for s in strings]
  422. padfn = _padleft
  423. elif alignment == "center":
  424. strings = [s.strip() for s in strings]
  425. padfn = _padboth
  426. elif alignment == "decimal":
  427. if has_invisible:
  428. decimals = [_afterpoint(_strip_invisible(s)) for s in strings]
  429. else:
  430. decimals = [_afterpoint(s) for s in strings]
  431. maxdecimals = max(decimals)
  432. strings = [s + (maxdecimals - decs) * " "
  433. for s, decs in zip(strings, decimals)]
  434. padfn = _padleft
  435. elif not alignment:
  436. return strings
  437. else:
  438. strings = [s.strip() for s in strings]
  439. padfn = _padright
  440. enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
  441. if has_invisible:
  442. width_fn = _visible_width
  443. elif enable_widechars: # optional wide-character support if available
  444. width_fn = wcwidth.wcswidth
  445. else:
  446. width_fn = len
  447. s_lens = list(map(len, strings))
  448. s_widths = list(map(width_fn, strings))
  449. maxwidth = max(max(s_widths), minwidth)
  450. if not enable_widechars and not has_invisible:
  451. padded_strings = [padfn(maxwidth, s) for s in strings]
  452. else:
  453. # enable wide-character width corrections
  454. visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)]
  455. # wcswidth and _visible_width don't count invisible characters;
  456. # padfn doesn't need to apply another correction
  457. padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)]
  458. return padded_strings
  459. def _more_generic(type1, type2):
  460. types = { _none_type: 0, int: 1, float: 2, _binary_type: 3, _text_type: 4 }
  461. invtypes = { 4: _text_type, 3: _binary_type, 2: float, 1: int, 0: _none_type }
  462. moregeneric = max(types.get(type1, 4), types.get(type2, 4))
  463. return invtypes[moregeneric]
  464. def _column_type(strings, has_invisible=True):
  465. """The least generic type all column values are convertible to.
  466. >>> _column_type(["1", "2"]) is _int_type
  467. True
  468. >>> _column_type(["1", "2.3"]) is _float_type
  469. True
  470. >>> _column_type(["1", "2.3", "four"]) is _text_type
  471. True
  472. >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is _text_type
  473. True
  474. >>> _column_type([None, "brux"]) is _text_type
  475. True
  476. >>> _column_type([1, 2, None]) is _int_type
  477. True
  478. >>> import datetime as dt
  479. >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is _text_type
  480. True
  481. """
  482. types = [_type(s, has_invisible) for s in strings ]
  483. return reduce(_more_generic, types, int)
  484. def _format(val, valtype, floatfmt, missingval="", has_invisible=True):
  485. """Format a value accoding to its type.
  486. Unicode is supported:
  487. >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \
  488. tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \
  489. good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \
  490. tabulate(tbl, headers=hrow) == good_result
  491. True
  492. """
  493. if val is None:
  494. return missingval
  495. if valtype in [int, _text_type]:
  496. return "{0}".format(val)
  497. elif valtype is _binary_type:
  498. try:
  499. return _text_type(val, "ascii")
  500. except TypeError:
  501. return _text_type(val)
  502. elif valtype is float:
  503. is_a_colored_number = has_invisible and isinstance(val, (_text_type, _binary_type))
  504. if is_a_colored_number:
  505. raw_val = _strip_invisible(val)
  506. formatted_val = format(float(raw_val), floatfmt)
  507. return val.replace(raw_val, formatted_val)
  508. else:
  509. return format(float(val), floatfmt)
  510. else:
  511. return "{0}".format(val)
  512. def _align_header(header, alignment, width, visible_width):
  513. "Pad string header to width chars given known visible_width of the header."
  514. width += len(header) - visible_width
  515. if alignment == "left":
  516. return _padright(width, header)
  517. elif alignment == "center":
  518. return _padboth(width, header)
  519. elif not alignment:
  520. return "{0}".format(header)
  521. else:
  522. return _padleft(width, header)
  523. def _prepend_row_index(rows, index):
  524. """Add a left-most index column."""
  525. if index is None or index is False:
  526. return rows
  527. if len(index) != len(rows):
  528. print('index=', index)
  529. print('rows=', rows)
  530. raise ValueError('index must be as long as the number of data rows')
  531. rows = [[v]+list(row) for v,row in zip(index, rows)]
  532. return rows
  533. def _bool(val):
  534. "A wrapper around standard bool() which doesn't throw on NumPy arrays"
  535. try:
  536. return bool(val)
  537. except ValueError: # val is likely to be a numpy array with many elements
  538. return False
  539. def _normalize_tabular_data(tabular_data, headers, showindex="default"):
  540. """Transform a supported data type to a list of lists, and a list of headers.
  541. Supported tabular data types:
  542. * list-of-lists or another iterable of iterables
  543. * list of named tuples (usually used with headers="keys")
  544. * list of dicts (usually used with headers="keys")
  545. * list of OrderedDicts (usually used with headers="keys")
  546. * 2D NumPy arrays
  547. * NumPy record arrays (usually used with headers="keys")
  548. * dict of iterables (usually used with headers="keys")
  549. * pandas.DataFrame (usually used with headers="keys")
  550. The first row can be used as headers if headers="firstrow",
  551. column indices can be used as headers if headers="keys".
  552. If showindex="default", show row indices of the pandas.DataFrame.
  553. If showindex="always", show row indices for all types of data.
  554. If showindex="never", don't show row indices for all types of data.
  555. If showindex is an iterable, show its values as row indices.
  556. """
  557. try:
  558. bool(headers)
  559. is_headers2bool_broken = False
  560. except ValueError: # numpy.ndarray, pandas.core.index.Index, ...
  561. is_headers2bool_broken = True
  562. headers = list(headers)
  563. index = None
  564. if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"):
  565. # dict-like and pandas.DataFrame?
  566. if hasattr(tabular_data.values, "__call__"):
  567. # likely a conventional dict
  568. keys = tabular_data.keys()
  569. rows = list(izip_longest(*tabular_data.values())) # columns have to be transposed
  570. elif hasattr(tabular_data, "index"):
  571. # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0)
  572. keys = tabular_data.keys()
  573. vals = tabular_data.values # values matrix doesn't need to be transposed
  574. # for DataFrames add an index per default
  575. index = list(tabular_data.index)
  576. rows = [list(row) for row in vals]
  577. else:
  578. raise ValueError("tabular data doesn't appear to be a dict or a DataFrame")
  579. if headers == "keys":
  580. headers = list(map(_text_type,keys)) # headers should be strings
  581. else: # it's a usual an iterable of iterables, or a NumPy array
  582. rows = list(tabular_data)
  583. if (headers == "keys" and
  584. hasattr(tabular_data, "dtype") and
  585. getattr(tabular_data.dtype, "names")):
  586. # numpy record array
  587. headers = tabular_data.dtype.names
  588. elif (headers == "keys"
  589. and len(rows) > 0
  590. and isinstance(rows[0], tuple)
  591. and hasattr(rows[0], "_fields")):
  592. # namedtuple
  593. headers = list(map(_text_type, rows[0]._fields))
  594. elif (len(rows) > 0
  595. and isinstance(rows[0], dict)):
  596. # dict or OrderedDict
  597. uniq_keys = set() # implements hashed lookup
  598. keys = [] # storage for set
  599. if headers == "firstrow":
  600. firstdict = rows[0] if len(rows) > 0 else {}
  601. keys.extend(firstdict.keys())
  602. uniq_keys.update(keys)
  603. rows = rows[1:]
  604. for row in rows:
  605. for k in row.keys():
  606. #Save unique items in input order
  607. if k not in uniq_keys:
  608. keys.append(k)
  609. uniq_keys.add(k)
  610. if headers == 'keys':
  611. headers = keys
  612. elif isinstance(headers, dict):
  613. # a dict of headers for a list of dicts
  614. headers = [headers.get(k, k) for k in keys]
  615. headers = list(map(_text_type, headers))
  616. elif headers == "firstrow":
  617. if len(rows) > 0:
  618. headers = [firstdict.get(k, k) for k in keys]
  619. headers = list(map(_text_type, headers))
  620. else:
  621. headers = []
  622. elif headers:
  623. raise ValueError('headers for a list of dicts is not a dict or a keyword')
  624. rows = [[row.get(k) for k in keys] for row in rows]
  625. elif headers == "keys" and len(rows) > 0:
  626. # keys are column indices
  627. headers = list(map(_text_type, range(len(rows[0]))))
  628. # take headers from the first row if necessary
  629. if headers == "firstrow" and len(rows) > 0:
  630. if index is not None:
  631. headers = [index[0]] + list(rows[0])
  632. index = index[1:]
  633. else:
  634. headers = rows[0]
  635. headers = list(map(_text_type, headers)) # headers should be strings
  636. rows = rows[1:]
  637. headers = list(map(_text_type,headers))
  638. rows = list(map(list,rows))
  639. # add or remove an index column
  640. showindex_is_a_str = type(showindex) in [_text_type, _binary_type]
  641. if showindex == "default" and index is not None:
  642. rows = _prepend_row_index(rows, index)
  643. elif isinstance(showindex, Iterable) and not showindex_is_a_str:
  644. rows = _prepend_row_index(rows, list(showindex))
  645. elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str):
  646. if index is None:
  647. index = list(range(len(rows)))
  648. rows = _prepend_row_index(rows, index)
  649. elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str):
  650. pass
  651. # pad with empty headers for initial columns if necessary
  652. if headers and len(rows) > 0:
  653. nhs = len(headers)
  654. ncols = len(rows[0])
  655. if nhs < ncols:
  656. headers = [""]*(ncols - nhs) + headers
  657. return rows, headers
  658. def tabulate(tabular_data, headers=(), tablefmt="simple",
  659. floatfmt="g", numalign="decimal", stralign="left",
  660. missingval="", showindex="default"):
  661. """Format a fixed width table for pretty printing.
  662. >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]]))
  663. --- ---------
  664. 1 2.34
  665. -56 8.999
  666. 2 10001
  667. --- ---------
  668. The first required argument (`tabular_data`) can be a
  669. list-of-lists (or another iterable of iterables), a list of named
  670. tuples, a dictionary of iterables, an iterable of dictionaries,
  671. a two-dimensional NumPy array, NumPy record array, or a Pandas'
  672. dataframe.
  673. Table headers
  674. -------------
  675. To print nice column headers, supply the second argument (`headers`):
  676. - `headers` can be an explicit list of column headers
  677. - if `headers="firstrow"`, then the first row of data is used
  678. - if `headers="keys"`, then dictionary keys or column indices are used
  679. Otherwise a headerless table is produced.
  680. If the number of headers is less than the number of columns, they
  681. are supposed to be names of the last columns. This is consistent
  682. with the plain-text format of R and Pandas' dataframes.
  683. >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]],
  684. ... headers="firstrow"))
  685. sex age
  686. ----- ----- -----
  687. Alice F 24
  688. Bob M 19
  689. By default, pandas.DataFrame data have an additional column called
  690. row index. To add a similar column to all other types of data,
  691. use `showindex="always"` or `showindex=True`. To suppress row indices
  692. for all types of data, pass `showindex="never" or `showindex=False`.
  693. To add a custom row index column, pass `showindex=some_iterable`.
  694. >>> print(tabulate([["F",24],["M",19]], showindex="always"))
  695. - - --
  696. 0 F 24
  697. 1 M 19
  698. - - --
  699. Column alignment
  700. ----------------
  701. `tabulate` tries to detect column types automatically, and aligns
  702. the values properly. By default it aligns decimal points of the
  703. numbers (or flushes integer numbers to the right), and flushes
  704. everything else to the left. Possible column alignments
  705. (`numalign`, `stralign`) are: "right", "center", "left", "decimal"
  706. (only for `numalign`), and None (to disable alignment).
  707. Table formats
  708. -------------
  709. `floatfmt` is a format specification used for columns which
  710. contain numeric data with a decimal point.
  711. `None` values are replaced with a `missingval` string:
  712. >>> print(tabulate([["spam", 1, None],
  713. ... ["eggs", 42, 3.14],
  714. ... ["other", None, 2.7]], missingval="?"))
  715. ----- -- ----
  716. spam 1 ?
  717. eggs 42 3.14
  718. other ? 2.7
  719. ----- -- ----
  720. Various plain-text table formats (`tablefmt`) are supported:
  721. 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki',
  722. 'latex', and 'latex_booktabs'. Variable `tabulate_formats` contains the list of
  723. currently supported formats.
  724. "plain" format doesn't use any pseudographics to draw tables,
  725. it separates columns with a double space:
  726. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  727. ... ["strings", "numbers"], "plain"))
  728. strings numbers
  729. spam 41.9999
  730. eggs 451
  731. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain"))
  732. spam 41.9999
  733. eggs 451
  734. "simple" format is like Pandoc simple_tables:
  735. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  736. ... ["strings", "numbers"], "simple"))
  737. strings numbers
  738. --------- ---------
  739. spam 41.9999
  740. eggs 451
  741. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple"))
  742. ---- --------
  743. spam 41.9999
  744. eggs 451
  745. ---- --------
  746. "grid" is similar to tables produced by Emacs table.el package or
  747. Pandoc grid_tables:
  748. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  749. ... ["strings", "numbers"], "grid"))
  750. +-----------+-----------+
  751. | strings | numbers |
  752. +===========+===========+
  753. | spam | 41.9999 |
  754. +-----------+-----------+
  755. | eggs | 451 |
  756. +-----------+-----------+
  757. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid"))
  758. +------+----------+
  759. | spam | 41.9999 |
  760. +------+----------+
  761. | eggs | 451 |
  762. +------+----------+
  763. "fancy_grid" draws a grid using box-drawing characters:
  764. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  765. ... ["strings", "numbers"], "fancy_grid"))
  766. ╒═══════════╤═══════════╕
  767. │ strings │ numbers │
  768. ╞═══════════╪═══════════╡
  769. │ spam │ 41.9999 │
  770. ├───────────┼───────────┤
  771. │ eggs │ 451 │
  772. ╘═══════════╧═══════════╛
  773. "pipe" is like tables in PHP Markdown Extra extension or Pandoc
  774. pipe_tables:
  775. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  776. ... ["strings", "numbers"], "pipe"))
  777. | strings | numbers |
  778. |:----------|----------:|
  779. | spam | 41.9999 |
  780. | eggs | 451 |
  781. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe"))
  782. |:-----|---------:|
  783. | spam | 41.9999 |
  784. | eggs | 451 |
  785. "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They
  786. are slightly different from "pipe" format by not using colons to
  787. define column alignment, and using a "+" sign to indicate line
  788. intersections:
  789. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  790. ... ["strings", "numbers"], "orgtbl"))
  791. | strings | numbers |
  792. |-----------+-----------|
  793. | spam | 41.9999 |
  794. | eggs | 451 |
  795. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl"))
  796. | spam | 41.9999 |
  797. | eggs | 451 |
  798. "rst" is like a simple table format from reStructuredText; please
  799. note that reStructuredText accepts also "grid" tables:
  800. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
  801. ... ["strings", "numbers"], "rst"))
  802. ========= =========
  803. strings numbers
  804. ========= =========
  805. spam 41.9999
  806. eggs 451
  807. ========= =========
  808. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst"))
  809. ==== ========
  810. spam 41.9999
  811. eggs 451
  812. ==== ========
  813. "mediawiki" produces a table markup used in Wikipedia and on other
  814. MediaWiki-based sites:
  815. >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
  816. ... headers="firstrow", tablefmt="mediawiki"))
  817. {| class="wikitable" style="text-align: left;"
  818. |+ <!-- caption -->
  819. |-
  820. ! strings !! align="right"| numbers
  821. |-
  822. | spam || align="right"| 41.9999
  823. |-
  824. | eggs || align="right"| 451
  825. |}
  826. "html" produces HTML markup:
  827. >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
  828. ... headers="firstrow", tablefmt="html"))
  829. <table>
  830. <thead>
  831. <tr><th>strings </th><th style="text-align: right;"> numbers</th></tr>
  832. </thead>
  833. <tbody>
  834. <tr><td>spam </td><td style="text-align: right;"> 41.9999</td></tr>
  835. <tr><td>eggs </td><td style="text-align: right;"> 451 </td></tr>
  836. </tbody>
  837. </table>
  838. "latex" produces a tabular environment of LaTeX document markup:
  839. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex"))
  840. \\begin{tabular}{lr}
  841. \\hline
  842. spam & 41.9999 \\\\
  843. eggs & 451 \\\\
  844. \\hline
  845. \\end{tabular}
  846. "latex_booktabs" produces a tabular environment of LaTeX document markup
  847. using the booktabs.sty package:
  848. >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs"))
  849. \\begin{tabular}{lr}
  850. \\toprule
  851. spam & 41.9999 \\\\
  852. eggs & 451 \\\\
  853. \\bottomrule
  854. \end{tabular}
  855. """
  856. if tabular_data is None:
  857. tabular_data = []
  858. list_of_lists, headers = _normalize_tabular_data(
  859. tabular_data, headers, showindex=showindex)
  860. # optimization: look for ANSI control codes once,
  861. # enable smart width functions only if a control code is found
  862. plain_text = '\n'.join(['\t'.join(map(_text_type, headers))] + \
  863. ['\t'.join(map(_text_type, row)) for row in list_of_lists])
  864. has_invisible = re.search(_invisible_codes, plain_text)
  865. enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
  866. if has_invisible:
  867. width_fn = _visible_width
  868. elif enable_widechars: # optional wide-character support if available
  869. width_fn = wcwidth.wcswidth
  870. else:
  871. width_fn = len
  872. # format rows and columns, convert numeric values to strings
  873. cols = list(zip(*list_of_lists))
  874. coltypes = list(map(_column_type, cols))
  875. cols = [[_format(v, ct, floatfmt, missingval, has_invisible) for v in c]
  876. for c,ct in zip(cols, coltypes)]
  877. # align columns
  878. aligns = [numalign if ct in [int,float] else stralign for ct in coltypes]
  879. minwidths = [width_fn(h) + MIN_PADDING for h in headers] if headers else [0]*len(cols)
  880. cols = [_align_column(c, a, minw, has_invisible)
  881. for c, a, minw in zip(cols, aligns, minwidths)]
  882. if headers:
  883. # align headers and add headers
  884. t_cols = cols or [['']] * len(headers)
  885. t_aligns = aligns or [stralign] * len(headers)
  886. minwidths = [max(minw, width_fn(c[0])) for minw, c in zip(minwidths, t_cols)]
  887. headers = [_align_header(h, a, minw, width_fn(h))
  888. for h, a, minw in zip(headers, t_aligns, minwidths)]
  889. rows = list(zip(*cols))
  890. else:
  891. minwidths = [width_fn(c[0]) for c in cols]
  892. rows = list(zip(*cols))
  893. if not isinstance(tablefmt, TableFormat):
  894. tablefmt = _table_formats.get(tablefmt, _table_formats["simple"])
  895. return _format_table(tablefmt, headers, rows, minwidths, aligns)
  896. def _build_simple_row(padded_cells, rowfmt):
  897. "Format row according to DataRow format without padding."
  898. begin, sep, end = rowfmt
  899. return (begin + sep.join(padded_cells) + end).rstrip()
  900. def _build_row(padded_cells, colwidths, colaligns, rowfmt):
  901. "Return a string which represents a row of data cells."
  902. if not rowfmt:
  903. return None
  904. if hasattr(rowfmt, "__call__"):
  905. return rowfmt(padded_cells, colwidths, colaligns)
  906. else:
  907. return _build_simple_row(padded_cells, rowfmt)
  908. def _build_line(colwidths, colaligns, linefmt):
  909. "Return a string which represents a horizontal line."
  910. if not linefmt:
  911. return None
  912. if hasattr(linefmt, "__call__"):
  913. return linefmt(colwidths, colaligns)
  914. else:
  915. begin, fill, sep, end = linefmt
  916. cells = [fill*w for w in colwidths]
  917. return _build_simple_row(cells, (begin, sep, end))
  918. def _pad_row(cells, padding):
  919. if cells:
  920. pad = " "*padding
  921. padded_cells = [pad + cell + pad for cell in cells]
  922. return padded_cells
  923. else:
  924. return cells
  925. def _format_table(fmt, headers, rows, colwidths, colaligns):
  926. """Produce a plain-text representation of the table."""
  927. lines = []
  928. hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
  929. pad = fmt.padding
  930. headerrow = fmt.headerrow
  931. padded_widths = [(w + 2*pad) for w in colwidths]
  932. padded_headers = _pad_row(headers, pad)
  933. padded_rows = [_pad_row(row, pad) for row in rows]
  934. if fmt.lineabove and "lineabove" not in hidden:
  935. lines.append(_build_line(padded_widths, colaligns, fmt.lineabove))
  936. if padded_headers:
  937. lines.append(_build_row(padded_headers, padded_widths, colaligns, headerrow))
  938. if fmt.linebelowheader and "linebelowheader" not in hidden:
  939. lines.append(_build_line(padded_widths, colaligns, fmt.linebelowheader))
  940. if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden:
  941. # initial rows with a line below
  942. for row in padded_rows[:-1]:
  943. lines.append(_build_row(row, padded_widths, colaligns, fmt.datarow))
  944. lines.append(_build_line(padded_widths, colaligns, fmt.linebetweenrows))
  945. # the last row without a line below
  946. lines.append(_build_row(padded_rows[-1], padded_widths, colaligns, fmt.datarow))
  947. else:
  948. for row in padded_rows:
  949. lines.append(_build_row(row, padded_widths, colaligns, fmt.datarow))
  950. if fmt.linebelow and "linebelow" not in hidden:
  951. lines.append(_build_line(padded_widths, colaligns, fmt.linebelow))
  952. return "\n".join(lines)
  953. def _main():
  954. """\
  955. Usage: tabulate [options] [FILE ...]
  956. Pretty-print tabular data.
  957. See also https://bitbucket.org/astanin/python-tabulate
  958. FILE a filename of the file with tabular data;
  959. if "-" or missing, read data from stdin.
  960. Options:
  961. -h, --help show this message
  962. -1, --header use the first row of data as a table header
  963. -o FILE, --output FILE print table to FILE (default: stdout)
  964. -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace)
  965. -F FPFMT, --float FPFMT floating point number format (default: g)
  966. -f FMT, --format FMT set output table format; supported formats:
  967. plain, simple, grid, fancy_grid, pipe, orgtbl,
  968. rst, mediawiki, html, latex, latex_booktabs, tsv
  969. (default: simple)
  970. """
  971. import getopt
  972. import sys
  973. import textwrap
  974. usage = textwrap.dedent(_main.__doc__)
  975. try:
  976. opts, args = getopt.getopt(sys.argv[1:],
  977. "h1o:s:F:f:",
  978. ["help", "header", "output", "sep=", "float=", "format="])
  979. except getopt.GetoptError as e:
  980. print(e)
  981. print(usage)
  982. sys.exit(2)
  983. headers = []
  984. floatfmt = "g"
  985. tablefmt = "simple"
  986. sep = r"\s+"
  987. outfile = "-"
  988. for opt, value in opts:
  989. if opt in ["-1", "--header"]:
  990. headers = "firstrow"
  991. elif opt in ["-o", "--output"]:
  992. outfile = value
  993. elif opt in ["-F", "--float"]:
  994. floatfmt = value
  995. elif opt in ["-f", "--format"]:
  996. if value not in tabulate_formats:
  997. print("%s is not a supported table format" % value)
  998. print(usage)
  999. sys.exit(3)
  1000. tablefmt = value
  1001. elif opt in ["-s", "--sep"]:
  1002. sep = value
  1003. elif opt in ["-h", "--help"]:
  1004. print(usage)
  1005. sys.exit(0)
  1006. files = [sys.stdin] if not args else args
  1007. with (sys.stdout if outfile == "-" else open(outfile, "w")) as out:
  1008. for f in files:
  1009. if f == "-":
  1010. f = sys.stdin
  1011. if _is_file(f):
  1012. _pprint_file(f, headers=headers, tablefmt=tablefmt,
  1013. sep=sep, floatfmt=floatfmt, file=out)
  1014. else:
  1015. with open(f) as fobj:
  1016. _pprint_file(fobj, headers=headers, tablefmt=tablefmt,
  1017. sep=sep, floatfmt=floatfmt, file=out)
  1018. def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, file):
  1019. rows = fobject.readlines()
  1020. table = [re.split(sep, r.rstrip()) for r in rows if r.strip()]
  1021. print(tabulate(table, headers, tablefmt, floatfmt=floatfmt), file=file)
  1022. if __name__ == "__main__":
  1023. _main()