diff --git a/tabulate/bin/tabulate b/tabulate/bin/tabulate new file mode 100755 index 00000000..83a13c94 --- /dev/null +++ b/tabulate/bin/tabulate @@ -0,0 +1,8 @@ +#!/usr/local/opt/python@3.13/bin/python3.13 +# -*- coding: utf-8 -*- +import re +import sys +from tabulate import _main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(_main()) diff --git a/tabulate/go.mod b/tabulate/go.mod new file mode 100644 index 00000000..a02f7bf5 --- /dev/null +++ b/tabulate/go.mod @@ -0,0 +1,5 @@ +module github.com/PengPengPeng717/llpkg/tabulate + +go 1.24.5 + +require github.com/goplus/lib v0.3.0 diff --git a/tabulate/go.sum b/tabulate/go.sum new file mode 100644 index 00000000..54e0f00c --- /dev/null +++ b/tabulate/go.sum @@ -0,0 +1,2 @@ +github.com/goplus/lib v0.3.0 h1:y0ZGb5Q/RikW1oMMB4Di7XIZIpuzh/7mlrR8HNbxXCA= +github.com/goplus/lib v0.3.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0= diff --git a/tabulate/llpkg.cfg b/tabulate/llpkg.cfg new file mode 100644 index 00000000..132ce40b --- /dev/null +++ b/tabulate/llpkg.cfg @@ -0,0 +1,17 @@ +{ + "type": "python", + "upstream": { + "installer": { + "name": "pip" + }, + "package": { + "name": "tabulate", + "version": "0.9.0" + } + }, + "llpyg": { + "output_dir": "./test", + "mod_name": "github.com/PengPengPeng717/llpkg/tabulate", + "mod_depth": 1 + } +} \ No newline at end of file diff --git a/tabulate/llpyg.cfg b/tabulate/llpyg.cfg new file mode 100644 index 00000000..1d427184 --- /dev/null +++ b/tabulate/llpyg.cfg @@ -0,0 +1,7 @@ +{ + "name": "tabulate", + "libName": "tabulate", + "modules": [ + "tabulate" + ] +} diff --git a/tabulate/tabulate-0.9.0.dist-info/INSTALLER b/tabulate/tabulate-0.9.0.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tabulate/tabulate-0.9.0.dist-info/LICENSE b/tabulate/tabulate-0.9.0.dist-info/LICENSE new file mode 100644 index 00000000..81241eca --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2011-2020 Sergey Astanin and contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tabulate/tabulate-0.9.0.dist-info/METADATA b/tabulate/tabulate-0.9.0.dist-info/METADATA new file mode 100644 index 00000000..3909a000 --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/METADATA @@ -0,0 +1,1149 @@ +Metadata-Version: 2.1 +Name: tabulate +Version: 0.9.0 +Summary: Pretty-print tabular data +Author-email: Sergey Astanin +License: MIT +Project-URL: Homepage, https://github.com/astanin/python-tabulate +Classifier: Development Status :: 4 - Beta +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Topic :: Software Development :: Libraries +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: widechars +Requires-Dist: wcwidth ; extra == 'widechars' + +python-tabulate +=============== + +Pretty-print tabular data in Python, a library and a command-line +utility. + +The main use cases of the library are: + +- printing small tables without hassle: just one function call, + formatting is guided by the data itself +- authoring tabular data for lightweight plain-text markup: multiple + output formats suitable for further editing or transformation +- readable presentation of mixed textual and numeric data: smart + column alignment, configurable number formatting, alignment by a + decimal point + +Installation +------------ + +To install the Python library and the command line utility, run: + +```shell +pip install tabulate +``` + +The command line utility will be installed as `tabulate` to `bin` on +Linux (e.g. `/usr/bin`); or as `tabulate.exe` to `Scripts` in your +Python installation on Windows (e.g. `C:\Python39\Scripts\tabulate.exe`). + +You may consider installing the library only for the current user: + +```shell +pip install tabulate --user +``` + +In this case the command line utility will be installed to +`~/.local/bin/tabulate` on Linux and to +`%APPDATA%\Python\Scripts\tabulate.exe` on Windows. + +To install just the library on Unix-like operating systems: + +```shell +TABULATE_INSTALL=lib-only pip install tabulate +``` + +On Windows: + +```shell +set TABULATE_INSTALL=lib-only +pip install tabulate +``` + +Build status +------------ + +[![Build status](https://circleci.com/gh/astanin/python-tabulate.svg?style=svg)](https://circleci.com/gh/astanin/python-tabulate/tree/master) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master) + +Library usage +------------- + +The module provides just one function, `tabulate`, which takes a list of +lists or another tabular data type as the first argument, and outputs a +nicely formatted plain-text table: + +```pycon +>>> from tabulate import tabulate + +>>> table = [["Sun",696000,1989100000],["Earth",6371,5973.6], +... ["Moon",1737,73.5],["Mars",3390,641.85]] +>>> print(tabulate(table)) +----- ------ ------------- +Sun 696000 1.9891e+09 +Earth 6371 5973.6 +Moon 1737 73.5 +Mars 3390 641.85 +----- ------ ------------- +``` + +The following tabular data types are supported: + +- list of lists or another iterable of iterables +- list or another iterable of dicts (keys as columns) +- dict of iterables (keys as columns) +- list of dataclasses (Python 3.7+ only, field names as columns) +- two-dimensional NumPy array +- NumPy record arrays (names as columns) +- pandas.DataFrame + +Tabulate is a Python3 library. + +### Headers + +The second optional argument named `headers` defines a list of column +headers to be used: + +```pycon +>>> print(tabulate(table, headers=["Planet","R (km)", "mass (x 10^29 kg)"])) +Planet R (km) mass (x 10^29 kg) +-------- -------- ------------------- +Sun 696000 1.9891e+09 +Earth 6371 5973.6 +Moon 1737 73.5 +Mars 3390 641.85 +``` + +If `headers="firstrow"`, then the first row of data is used: + +```pycon +>>> print(tabulate([["Name","Age"],["Alice",24],["Bob",19]], +... headers="firstrow")) +Name Age +------ ----- +Alice 24 +Bob 19 +``` + +If `headers="keys"`, then the keys of a dictionary/dataframe, or column +indices are used. It also works for NumPy record arrays and lists of +dictionaries or named tuples: + +```pycon +>>> print(tabulate({"Name": ["Alice", "Bob"], +... "Age": [24, 19]}, headers="keys")) + Age Name +----- ------ + 24 Alice + 19 Bob +``` + +### Row Indices + +By default, only pandas.DataFrame tables have an additional column +called row index. To add a similar column to any other type of table, +pass `showindex="always"` or `showindex=True` argument to `tabulate()`. +To suppress row indices for all types of data, pass `showindex="never"` +or `showindex=False`. To add a custom row index column, pass +`showindex=rowIDs`, where `rowIDs` is some iterable: + +```pycon +>>> print(tabulate([["F",24],["M",19]], showindex="always")) +- - -- +0 F 24 +1 M 19 +- - -- +``` + +### Table format + +There is more than one way to format a table in plain text. The third +optional argument named `tablefmt` defines how the table is formatted. + +Supported table formats are: + +- "plain" +- "simple" +- "github" +- "grid" +- "simple\_grid" +- "rounded\_grid" +- "heavy\_grid" +- "mixed\_grid" +- "double\_grid" +- "fancy\_grid" +- "outline" +- "simple\_outline" +- "rounded\_outline" +- "heavy\_outline" +- "mixed\_outline" +- "double\_outline" +- "fancy\_outline" +- "pipe" +- "orgtbl" +- "asciidoc" +- "jira" +- "presto" +- "pretty" +- "psql" +- "rst" +- "mediawiki" +- "moinmoin" +- "youtrack" +- "html" +- "unsafehtml" +- "latex" +- "latex\_raw" +- "latex\_booktabs" +- "latex\_longtable" +- "textile" +- "tsv" + +`plain` tables do not use any pseudo-graphics to draw lines: + +```pycon +>>> table = [["spam",42],["eggs",451],["bacon",0]] +>>> headers = ["item", "qty"] +>>> print(tabulate(table, headers, tablefmt="plain")) +item qty +spam 42 +eggs 451 +bacon 0 +``` + +`simple` is the default format (the default may change in future +versions). It corresponds to `simple_tables` in [Pandoc Markdown +extensions](http://johnmacfarlane.net/pandoc/README.html#tables): + +```pycon +>>> print(tabulate(table, headers, tablefmt="simple")) +item qty +------ ----- +spam 42 +eggs 451 +bacon 0 +``` + +`github` follows the conventions of GitHub flavored Markdown. It +corresponds to the `pipe` format without alignment colons: + +```pycon +>>> print(tabulate(table, headers, tablefmt="github")) +| item | qty | +|--------|-------| +| spam | 42 | +| eggs | 451 | +| bacon | 0 | +``` + +`grid` is like tables formatted by Emacs' +[table.el](http://table.sourceforge.net/) package. It corresponds to +`grid_tables` in Pandoc Markdown extensions: + +```pycon +>>> print(tabulate(table, headers, tablefmt="grid")) ++--------+-------+ +| item | qty | ++========+=======+ +| spam | 42 | ++--------+-------+ +| eggs | 451 | ++--------+-------+ +| bacon | 0 | ++--------+-------+ +``` + +`simple_grid` draws a grid using single-line box-drawing characters: + + >>> print(tabulate(table, headers, tablefmt="simple_grid")) + ┌────────┬───────┐ + │ item │ qty │ + ├────────┼───────┤ + │ spam │ 42 │ + ├────────┼───────┤ + │ eggs │ 451 │ + ├────────┼───────┤ + │ bacon │ 0 │ + └────────┴───────┘ + +`rounded_grid` draws a grid using single-line box-drawing characters with rounded corners: + + >>> print(tabulate(table, headers, tablefmt="rounded_grid")) + ╭────────┬───────╮ + │ item │ qty │ + ├────────┼───────┤ + │ spam │ 42 │ + ├────────┼───────┤ + │ eggs │ 451 │ + ├────────┼───────┤ + │ bacon │ 0 │ + ╰────────┴───────╯ + +`heavy_grid` draws a grid using bold (thick) single-line box-drawing characters: + + >>> print(tabulate(table, headers, tablefmt="heavy_grid")) + ┏━━━━━━━━┳━━━━━━━┓ + ┃ item ┃ qty ┃ + ┣━━━━━━━━╋━━━━━━━┫ + ┃ spam ┃ 42 ┃ + ┣━━━━━━━━╋━━━━━━━┫ + ┃ eggs ┃ 451 ┃ + ┣━━━━━━━━╋━━━━━━━┫ + ┃ bacon ┃ 0 ┃ + ┗━━━━━━━━┻━━━━━━━┛ + +`mixed_grid` draws a grid using a mix of light (thin) and heavy (thick) lines box-drawing characters: + + >>> print(tabulate(table, headers, tablefmt="mixed_grid")) + ┍━━━━━━━━┯━━━━━━━┑ + │ item │ qty │ + ┝━━━━━━━━┿━━━━━━━┥ + │ spam │ 42 │ + ├────────┼───────┤ + │ eggs │ 451 │ + ├────────┼───────┤ + │ bacon │ 0 │ + ┕━━━━━━━━┷━━━━━━━┙ + +`double_grid` draws a grid using double-line box-drawing characters: + + >>> print(tabulate(table, headers, tablefmt="double_grid")) + ╔════════╦═══════╗ + ║ item ║ qty ║ + ╠════════╬═══════╣ + ║ spam ║ 42 ║ + ╠════════╬═══════╣ + ║ eggs ║ 451 ║ + ╠════════╬═══════╣ + ║ bacon ║ 0 ║ + ╚════════╩═══════╝ + +`fancy_grid` draws a grid using a mix of single and + double-line box-drawing characters: + +```pycon +>>> print(tabulate(table, headers, tablefmt="fancy_grid")) +╒════════╤═══════╕ +│ item │ qty │ +╞════════╪═══════╡ +│ spam │ 42 │ +├────────┼───────┤ +│ eggs │ 451 │ +├────────┼───────┤ +│ bacon │ 0 │ +╘════════╧═══════╛ +``` + +`outline` is the same as the `grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="outline")) + +--------+-------+ + | item | qty | + +========+=======+ + | spam | 42 | + | eggs | 451 | + | bacon | 0 | + +--------+-------+ + +`simple_outline` is the same as the `simple_grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="simple_outline")) + ┌────────┬───────┐ + │ item │ qty │ + ├────────┼───────┤ + │ spam │ 42 │ + │ eggs │ 451 │ + │ bacon │ 0 │ + └────────┴───────┘ + +`rounded_outline` is the same as the `rounded_grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="rounded_outline")) + ╭────────┬───────╮ + │ item │ qty │ + ├────────┼───────┤ + │ spam │ 42 │ + │ eggs │ 451 │ + │ bacon │ 0 │ + ╰────────┴───────╯ + +`heavy_outline` is the same as the `heavy_grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="heavy_outline")) + ┏━━━━━━━━┳━━━━━━━┓ + ┃ item ┃ qty ┃ + ┣━━━━━━━━╋━━━━━━━┫ + ┃ spam ┃ 42 ┃ + ┃ eggs ┃ 451 ┃ + ┃ bacon ┃ 0 ┃ + ┗━━━━━━━━┻━━━━━━━┛ + +`mixed_outline` is the same as the `mixed_grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="mixed_outline")) + ┍━━━━━━━━┯━━━━━━━┑ + │ item │ qty │ + ┝━━━━━━━━┿━━━━━━━┥ + │ spam │ 42 │ + │ eggs │ 451 │ + │ bacon │ 0 │ + ┕━━━━━━━━┷━━━━━━━┙ + +`double_outline` is the same as the `double_grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="double_outline")) + ╔════════╦═══════╗ + ║ item ║ qty ║ + ╠════════╬═══════╣ + ║ spam ║ 42 ║ + ║ eggs ║ 451 ║ + ║ bacon ║ 0 ║ + ╚════════╩═══════╝ + +`fancy_outline` is the same as the `fancy_grid` format but doesn't draw lines between rows: + + >>> print(tabulate(table, headers, tablefmt="fancy_outline")) + ╒════════╤═══════╕ + │ item │ qty │ + ╞════════╪═══════╡ + │ spam │ 42 │ + │ eggs │ 451 │ + │ bacon │ 0 │ + ╘════════╧═══════╛ + +`presto` is like tables formatted by Presto cli: + +```pycon +>>> print(tabulate(table, headers, tablefmt="presto")) + item | qty +--------+------- + spam | 42 + eggs | 451 + bacon | 0 +``` + +`pretty` attempts to be close to the format emitted by the PrettyTables +library: + +```pycon +>>> print(tabulate(table, headers, tablefmt="pretty")) ++-------+-----+ +| item | qty | ++-------+-----+ +| spam | 42 | +| eggs | 451 | +| bacon | 0 | ++-------+-----+ +``` + +`psql` is like tables formatted by Postgres' psql cli: + +```pycon +>>> print(tabulate(table, headers, tablefmt="psql")) ++--------+-------+ +| item | qty | +|--------+-------| +| spam | 42 | +| eggs | 451 | +| bacon | 0 | ++--------+-------+ +``` + +`pipe` follows the conventions of [PHP Markdown +Extra](http://michelf.ca/projects/php-markdown/extra/#table) extension. +It corresponds to `pipe_tables` in Pandoc. This format uses colons to +indicate column alignment: + +```pycon +>>> print(tabulate(table, headers, tablefmt="pipe")) +| item | qty | +|:-------|------:| +| spam | 42 | +| eggs | 451 | +| bacon | 0 | +``` + +`asciidoc` formats data like a simple table of the +[AsciiDoctor](https://docs.asciidoctor.org/asciidoc/latest/syntax-quick-reference/#tables) +format: + +```pycon +>>> print(tabulate(table, headers, tablefmt="asciidoc")) +[cols="8<,7>",options="header"] +|==== +| item | qty +| spam | 42 +| eggs | 451 +| bacon | 0 +|==== +``` + +`orgtbl` follows the conventions of Emacs +[org-mode](http://orgmode.org/manual/Tables.html), and is editable also +in the minor orgtbl-mode. Hence its name: + +```pycon +>>> print(tabulate(table, headers, tablefmt="orgtbl")) +| item | qty | +|--------+-------| +| spam | 42 | +| eggs | 451 | +| bacon | 0 | +``` + +`jira` follows the conventions of Atlassian Jira markup language: + +```pycon +>>> print(tabulate(table, headers, tablefmt="jira")) +|| item || qty || +| spam | 42 | +| eggs | 451 | +| bacon | 0 | +``` + +`rst` formats data like a simple table of the +[reStructuredText](http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables) +format: + +```pycon +>>> print(tabulate(table, headers, tablefmt="rst")) +====== ===== +item qty +====== ===== +spam 42 +eggs 451 +bacon 0 +====== ===== +``` + +`mediawiki` format produces a table markup used in +[Wikipedia](http://www.mediawiki.org/wiki/Help:Tables) and on other +MediaWiki-based sites: + + ```pycon +>>> print(tabulate(table, headers, tablefmt="mediawiki")) +{| class="wikitable" style="text-align: left;" +|+ +|- +! item !! align="right"| qty +|- +| spam || align="right"| 42 +|- +| eggs || align="right"| 451 +|- +| bacon || align="right"| 0 +|} +``` + +`moinmoin` format produces a table markup used in +[MoinMoin](https://moinmo.in/) wikis: + +```pycon +>>> print(tabulate(table, headers, tablefmt="moinmoin")) +|| ''' item ''' || ''' quantity ''' || +|| spam || 41.999 || +|| eggs || 451 || +|| bacon || || +``` + +`youtrack` format produces a table markup used in Youtrack tickets: + +```pycon +>>> print(tabulate(table, headers, tablefmt="youtrack")) +|| item || quantity || +| spam | 41.999 | +| eggs | 451 | +| bacon | | +``` + +`textile` format produces a table markup used in +[Textile](http://redcloth.org/hobix.com/textile/) format: + +```pycon +>>> print(tabulate(table, headers, tablefmt="textile")) +|_. item |_. qty | +|<. spam |>. 42 | +|<. eggs |>. 451 | +|<. bacon |>. 0 | +``` + +`html` produces standard HTML markup as an html.escape'd str +with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML +and a .str property so that the raw HTML remains accessible. +`unsafehtml` table format can be used if an unescaped HTML is required: + +```pycon +>>> print(tabulate(table, headers, tablefmt="html")) + + + + + + + +
item qty
spam 42
eggs 451
bacon 0
+``` + +`latex` format creates a `tabular` environment for LaTeX markup, +replacing special characters like `_` or `\` to their LaTeX +correspondents: + +```pycon +>>> print(tabulate(table, headers, tablefmt="latex")) +\begin{tabular}{lr} +\hline + item & qty \\ +\hline + spam & 42 \\ + eggs & 451 \\ + bacon & 0 \\ +\hline +\end{tabular} +``` + +`latex_raw` behaves like `latex` but does not escape LaTeX commands and +special characters. + +`latex_booktabs` creates a `tabular` environment for LaTeX markup using +spacing and style from the `booktabs` package. + +`latex_longtable` creates a table that can stretch along multiple pages, +using the `longtable` package. + +### Column alignment + +`tabulate` is smart about column alignment. It detects columns which +contain only numbers, and aligns them by a decimal point (or flushes +them to the right if they appear to be integers). Text columns are +flushed to the left. + +You can override the default alignment with `numalign` and `stralign` +named arguments. Possible column alignments are: `right`, `center`, +`left`, `decimal` (only for numbers), and `None` (to disable alignment). + +Aligning by a decimal point works best when you need to compare numbers +at a glance: + +```pycon +>>> print(tabulate([[1.2345],[123.45],[12.345],[12345],[1234.5]])) +---------- + 1.2345 + 123.45 + 12.345 +12345 + 1234.5 +---------- +``` + +Compare this with a more common right alignment: + +```pycon +>>> print(tabulate([[1.2345],[123.45],[12.345],[12345],[1234.5]], numalign="right")) +------ +1.2345 +123.45 +12.345 + 12345 +1234.5 +------ +``` + +For `tabulate`, anything which can be parsed as a number is a number. +Even numbers represented as strings are aligned properly. This feature +comes in handy when reading a mixed table of text and numbers from a +file: + +```pycon +>>> import csv ; from StringIO import StringIO +>>> table = list(csv.reader(StringIO("spam, 42\neggs, 451\n"))) +>>> table +[['spam', ' 42'], ['eggs', ' 451']] +>>> print(tabulate(table)) +---- ---- +spam 42 +eggs 451 +---- ---- +``` + +To disable this feature use `disable_numparse=True`. + +```pycon +>>> print(tabulate.tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True)) +---- ---- +Ver1 18.0 +Ver2 19.2 +---- ---- +``` + +### Custom column alignment + +`tabulate` allows a custom column alignment to override the above. The +`colalign` argument can be a list or a tuple of `stralign` named +arguments. Possible column alignments are: `right`, `center`, `left`, +`decimal` (only for numbers), and `None` (to disable alignment). +Omitting an alignment uses the default. For example: + +```pycon +>>> print(tabulate([["one", "two"], ["three", "four"]], colalign=("right",)) +----- ---- + one two +three four +----- ---- +``` + +### Number formatting + +`tabulate` allows to define custom number formatting applied to all +columns of decimal numbers. Use `floatfmt` named argument: + +```pycon +>>> print(tabulate([["pi",3.141593],["e",2.718282]], floatfmt=".4f")) +-- ------ +pi 3.1416 +e 2.7183 +-- ------ +``` + +`floatfmt` argument can be a list or a tuple of format strings, one per +column, in which case every column may have different number formatting: + +```pycon +>>> print(tabulate([[0.12345, 0.12345, 0.12345]], floatfmt=(".1f", ".3f"))) +--- ----- ------- +0.1 0.123 0.12345 +--- ----- ------- +``` + +`intfmt` works similarly for integers + + >>> print(tabulate([["a",1000],["b",90000]], intfmt=",")) + - ------ + a 1,000 + b 90,000 + - ------ + +### Text formatting + +By default, `tabulate` removes leading and trailing whitespace from text +columns. To disable whitespace removal, set the global module-level flag +`PRESERVE_WHITESPACE`: + +```python +import tabulate +tabulate.PRESERVE_WHITESPACE = True +``` + +### Wide (fullwidth CJK) symbols + +To properly align tables which contain wide characters (typically +fullwidth glyphs from Chinese, Japanese or Korean languages), the user +should install `wcwidth` library. To install it together with +`tabulate`: + +```shell +pip install tabulate[widechars] +``` + +Wide character support is enabled automatically if `wcwidth` library is +already installed. To disable wide characters support without +uninstalling `wcwidth`, set the global module-level flag +`WIDE_CHARS_MODE`: + +```python +import tabulate +tabulate.WIDE_CHARS_MODE = False +``` + +### Multiline cells + +Most table formats support multiline cell text (text containing newline +characters). The newline characters are honored as line break +characters. + +Multiline cells are supported for data rows and for header rows. + +Further automatic line breaks are not inserted. Of course, some output +formats such as latex or html handle automatic formatting of the cell +content on their own, but for those that don't, the newline characters +in the input cell text are the only means to break a line in cell text. + +Note that some output formats (e.g. simple, or plain) do not represent +row delimiters, so that the representation of multiline cells in such +formats may be ambiguous to the reader. + +The following examples of formatted output use the following table with +a multiline cell, and headers with a multiline cell: + +```pycon +>>> table = [["eggs",451],["more\nspam",42]] +>>> headers = ["item\nname", "qty"] +``` + +`plain` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="plain")) +item qty +name +eggs 451 +more 42 +spam +``` + +`simple` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="simple")) +item qty +name +------ ----- +eggs 451 +more 42 +spam +``` + +`grid` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="grid")) ++--------+-------+ +| item | qty | +| name | | ++========+=======+ +| eggs | 451 | ++--------+-------+ +| more | 42 | +| spam | | ++--------+-------+ +``` + +`fancy_grid` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="fancy_grid")) +╒════════╤═══════╕ +│ item │ qty │ +│ name │ │ +╞════════╪═══════╡ +│ eggs │ 451 │ +├────────┼───────┤ +│ more │ 42 │ +│ spam │ │ +╘════════╧═══════╛ +``` + +`pipe` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="pipe")) +| item | qty | +| name | | +|:-------|------:| +| eggs | 451 | +| more | 42 | +| spam | | +``` + +`orgtbl` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="orgtbl")) +| item | qty | +| name | | +|--------+-------| +| eggs | 451 | +| more | 42 | +| spam | | +``` + +`jira` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="jira")) +| item | qty | +| name | | +|:-------|------:| +| eggs | 451 | +| more | 42 | +| spam | | +``` + +`presto` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="presto")) + item | qty + name | +--------+------- + eggs | 451 + more | 42 + spam | +``` + +`pretty` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="pretty")) ++------+-----+ +| item | qty | +| name | | ++------+-----+ +| eggs | 451 | +| more | 42 | +| spam | | ++------+-----+ +``` + +`psql` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="psql")) ++--------+-------+ +| item | qty | +| name | | +|--------+-------| +| eggs | 451 | +| more | 42 | +| spam | | ++--------+-------+ +``` + +`rst` tables: + +```pycon +>>> print(tabulate(table, headers, tablefmt="rst")) +====== ===== +item qty +name +====== ===== +eggs 451 +more 42 +spam +====== ===== +``` + +Multiline cells are not well-supported for the other table formats. + +### Automating Multilines +While tabulate supports data passed in with multilines entries explicitly provided, +it also provides some support to help manage this work internally. + +The `maxcolwidths` argument is a list where each entry specifies the max width for +it's respective column. Any cell that will exceed this will automatically wrap the content. +To assign the same max width for all columns, a singular int scaler can be used. + +Use `None` for any columns where an explicit maximum does not need to be provided, +and thus no automate multiline wrapping will take place. + +The wrapping uses the python standard [textwrap.wrap](https://docs.python.org/3/library/textwrap.html#textwrap.wrap) +function with default parameters - aside from width. + +This example demonstrates usage of automatic multiline wrapping, though typically +the lines being wrapped would probably be significantly longer than this. + +```pycon +>>> print(tabulate([["John Smith", "Middle Manager"]], headers=["Name", "Title"], tablefmt="grid", maxcolwidths=[None, 8])) ++------------+---------+ +| Name | Title | ++============+=========+ +| John Smith | Middle | +| | Manager | ++------------+---------+ +``` + +### Adding Separating lines +One might want to add one or more separating lines to highlight different sections in a table. + +The separating lines will be of the same type as the one defined by the specified formatter as either the +linebetweenrows, linebelowheader, linebelow, lineabove or just a simple empty line when none is defined for the formatter + + + >>> from tabulate import tabulate, SEPARATING_LINE + + table = [["Earth",6371], + ["Mars",3390], + SEPARATING_LINE, + ["Moon",1737]] + print(tabulate(table, tablefmt="simple")) + ----- ---- + Earth 6371 + Mars 3390 + ----- ---- + Moon 1737 + ----- ---- + +### ANSI support +ANSI escape codes are non-printable byte sequences usually used for terminal operations like setting +color output or modifying cursor positions. Because multi-byte ANSI sequences are inherently non-printable, +they can still introduce unwanted extra length to strings. For example: + + >>> len('\033[31mthis text is red\033[0m') # printable length is 16 + 25 + +To deal with this, string lengths are calculated after first removing all ANSI escape sequences. This ensures +that the actual printable length is used for column widths, rather than the byte length. In the final, printable +table, however, ANSI escape sequences are not removed so the original styling is preserved. + +Some terminals support a special grouping of ANSI escape sequences that are intended to display hyperlinks +much in the same way they are shown in browsers. These are handled just as mentioned before: non-printable +ANSI escape sequences are removed prior to string length calculation. The only diifference with escaped +hyperlinks is that column width will be based on the length of the URL _text_ rather than the URL +itself (terminals would show this text). For example: + + >>> len('\x1b]8;;https://example.com\x1b\\example\x1b]8;;\x1b\\') # display length is 7, showing 'example' + 45 + + +Usage of the command line utility +--------------------------------- + + Usage: tabulate [options] [FILE ...] + + FILE a filename of the file with tabular data; + if "-" or missing, read data from stdin. + + Options: + + -h, --help show this message + -1, --header use the first row of data as a table header + -o FILE, --output FILE print table to FILE (default: stdout) + -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace) + -F FPFMT, --float FPFMT floating point number format (default: g) + -I INTFMT, --int INTFMT integer point number format (default: "") + -f FMT, --format FMT set output table format; supported formats: + plain, simple, github, grid, fancy_grid, pipe, + orgtbl, rst, mediawiki, html, latex, latex_raw, + latex_booktabs, latex_longtable, tsv + (default: simple) + +Performance considerations +-------------------------- + +Such features as decimal point alignment and trying to parse everything +as a number imply that `tabulate`: + +- has to "guess" how to print a particular tabular data type +- needs to keep the entire table in-memory +- has to "transpose" the table twice +- does much more work than it may appear + +It may not be suitable for serializing really big tables (but who's +going to do that, anyway?) or printing tables in performance sensitive +applications. `tabulate` is about two orders of magnitude slower than +simply joining lists of values with a tab, comma, or other separator. + +At the same time, `tabulate` is comparable to other table +pretty-printers. Given a 10x10 table (a list of lists) of mixed text and +numeric data, `tabulate` appears to be slower than `asciitable`, and +faster than `PrettyTable` and `texttable` The following mini-benchmark +was run in Python 3.9.13 on Windows 10: + + ================================= ========== =========== + Table formatter time, μs rel. time + ================================= ========== =========== + csv to StringIO 12.5 1.0 + join with tabs and newlines 14.6 1.2 + asciitable (0.8.0) 192.0 15.4 + tabulate (0.9.0) 483.5 38.7 + tabulate (0.9.0, WIDE_CHARS_MODE) 637.6 51.1 + PrettyTable (3.4.1) 1080.6 86.6 + texttable (1.6.4) 1390.3 111.4 + ================================= ========== =========== + + +Version history +--------------- + +The full version history can be found at the [changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG). + +How to contribute +----------------- + +Contributions should include tests and an explanation for the changes +they propose. Documentation (examples, docstrings, README.md) should be +updated accordingly. + +This project uses [pytest](https://docs.pytest.org/) testing +framework and [tox](https://tox.readthedocs.io/) to automate testing in +different environments. Add tests to one of the files in the `test/` +folder. + +To run tests on all supported Python versions, make sure all Python +interpreters, `pytest` and `tox` are installed, then run `tox` in the root +of the project source tree. + +On Linux `tox` expects to find executables like `python3.7`, `python3.8` etc. +On Windows it looks for `C:\Python37\python.exe`, `C:\Python38\python.exe` etc. respectively. + +One way to install all the required versions of the Python interpreter is to use [pyenv](https://github.com/pyenv/pyenv). +All versions can then be easily installed with something like: + + pyenv install 3.7.12 + pyenv install 3.8.12 + ... + +Don't forget to change your `PATH` so that `tox` knows how to find all the installed versions. Something like + + export PATH="${PATH}:${HOME}/.pyenv/shims" + +To test only some Python environments, use `-e` option. For example, to +test only against Python 3.7 and Python 3.10, run: + +```shell +tox -e py37,py310 +``` + +in the root of the project source tree. + +To enable NumPy and Pandas tests, run: + +```shell +tox -e py37-extra,py310-extra +``` + +(this may take a long time the first time, because NumPy and Pandas will +have to be installed in the new virtual environments) + +To fix code formatting: + +```shell +tox -e lint +``` + +See `tox.ini` file to learn how to use to test +individual Python versions. + +Contributors +------------ + +Sergey Astanin, Pau Tallada Crespí, Erwin Marsi, Mik Kocikowski, Bill +Ryder, Zach Dwiel, Frederik Rietdijk, Philipp Bogensberger, Greg +(anonymous), Stefan Tatschner, Emiel van Miltenburg, Brandon Bennett, +Amjith Ramanujam, Jan Schulz, Simon Percivall, Javier Santacruz +López-Cepero, Sam Denton, Alexey Ziyangirov, acaird, Cesar Sanchez, +naught101, John Vandenberg, Zack Dever, Christian Clauss, Benjamin +Maier, Andy MacKinlay, Thomas Roten, Jue Wang, Joe King, Samuel Phan, +Nick Satterly, Daniel Robbins, Dmitry B, Lars Butler, Andreas Maier, +Dick Marinus, Sébastien Celles, Yago González, Andrew Gaul, Wim Glenn, +Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea, +Wes Turner, Andrew Tija, Marco Gorelli, Sean McGinnis, danja100, +endolith, Dominic Davis-Foster, pavlocat, Daniel Aslau, paulc, +Felix Yan, Shane Loretz, Frank Busse, Harsh Singh, Derek Weitzel, +Vladimir Vrzić, 서승우 (chrd5273), Georgy Frolov, Christian Cwienk, +Bart Broere, Vilhelm Prytz, Alexander Gažo, Hugo van Kemenade, +jamescooke, Matt Warner, Jérôme Provensal, Kevin Deldycke, +Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH, +Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan, +Dimitri Papadopoulos. + diff --git a/tabulate/tabulate-0.9.0.dist-info/RECORD b/tabulate/tabulate-0.9.0.dist-info/RECORD new file mode 100644 index 00000000..cd4f8ebc --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/RECORD @@ -0,0 +1,13 @@ +../../bin/tabulate,sha256=uneHJk3NF7L8B9E-g7smH0ES_cbkaBt6oMAj5rJal40,234 +tabulate-0.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +tabulate-0.9.0.dist-info/LICENSE,sha256=zfq1DTfY6tBkaPt2o6uvzQXBZ0nsihjuv6UP1Ix8stI,1080 +tabulate-0.9.0.dist-info/METADATA,sha256=8oAqreJhIJG0WVHyZa8pF0-QwyNvaMyMzetkaUHmKWk,34132 +tabulate-0.9.0.dist-info/RECORD,, +tabulate-0.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tabulate-0.9.0.dist-info/WHEEL,sha256=G16H4A3IeoQmnOrYV4ueZGKSjhipXx8zc8nu9FGlvMA,92 +tabulate-0.9.0.dist-info/entry_points.txt,sha256=8DmChBYma2n4UqC1VkkKbD5Nu4MrdZasURoeTtG0JVo,44 +tabulate-0.9.0.dist-info/top_level.txt,sha256=qfqkQ2az7LTxUeRePtX8ggmh294Kf1ERdI-11aWqFZU,9 +tabulate/__init__.py,sha256=X3rwoo_NcTuDDJc4hnWUX6jElQsFtY-NGHyQCldS1X0,95290 +tabulate/__pycache__/__init__.cpython-313.pyc,, +tabulate/__pycache__/version.cpython-313.pyc,, +tabulate/version.py,sha256=QVVpjnTor93ym-Tb6Y_XtL_6pmQ3MtoNy3Q6I0o3Yqg,181 diff --git a/tabulate/tabulate-0.9.0.dist-info/REQUESTED b/tabulate/tabulate-0.9.0.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/tabulate/tabulate-0.9.0.dist-info/WHEEL b/tabulate/tabulate-0.9.0.dist-info/WHEEL new file mode 100644 index 00000000..becc9a66 --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.37.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tabulate/tabulate-0.9.0.dist-info/entry_points.txt b/tabulate/tabulate-0.9.0.dist-info/entry_points.txt new file mode 100644 index 00000000..1efc880f --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +tabulate = tabulate:_main diff --git a/tabulate/tabulate-0.9.0.dist-info/top_level.txt b/tabulate/tabulate-0.9.0.dist-info/top_level.txt new file mode 100644 index 00000000..a5d51591 --- /dev/null +++ b/tabulate/tabulate-0.9.0.dist-info/top_level.txt @@ -0,0 +1 @@ +tabulate diff --git a/tabulate/tabulate.go b/tabulate/tabulate.go new file mode 100644 index 00000000..d463d585 --- /dev/null +++ b/tabulate/tabulate.go @@ -0,0 +1,537 @@ +package tabulate + +import ( + "github.com/goplus/lib/py" + _ "unsafe" +) + +const LLGoPackage = "py.tabulate" +// Returns a new subclass of tuple with named fields. +// +// >>> Point = namedtuple('Point', ['x', 'y']) +// >>> Point.__doc__ # docstring for the new class +// 'Point(x, y)' +// >>> p = Point(11, y=22) # instantiate with positional args or keywords +// >>> p[0] + p[1] # indexable like a plain tuple +// 33 +// >>> x, y = p # unpack like a regular tuple +// >>> x, y +// (11, 22) +// >>> p.x + p.y # fields also accessible by name +// 33 +// >>> d = p._asdict() # convert to a dictionary +// >>> d['x'] +// 11 +// >>> Point(**d) # convert from a dictionary +// Point(x=11, y=22) +// >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields +// Point(x=100, y=22) +// +// +// +//go:linkname Namedtuple py.namedtuple +func Namedtuple(typename *py.Object, fieldNames *py.Object) *py.Object +// +// Replace special characters "&", "<" and ">" to HTML-safe sequences. +// If the optional flag quote is true (the default), the quotation mark +// characters, both double quote (") and single quote (') characters are also +// translated. +// +// +//go:linkname Htmlescape py.htmlescape +func Htmlescape(s *py.Object, quote *py.Object) *py.Object +// reduce(function, iterable[, initial]) -> value +// +// Apply a function of two arguments cumulatively to the items of an iterable, from left to right. +// +// This effectively reduces the iterable to a single value. If initial is present, +// it is placed before the items of the iterable in the calculation, and serves as +// a default when the iterable is empty. +// +// For example, reduce(lambda x, y: x+y, [1, 2, 3, 4, 5]) +// calculates ((((1 + 2) + 3) + 4) + 5). +// +//go:linkname Reduce py.reduce +func Reduce(function *py.Object, iterable *py.Object, initial *py.Object) *py.Object +// Construct a simple TableFormat with columns separated by a separator. +// +// >>> tsv = simple_separated_format("\t") ; tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \t 1\nspam\t23' +// True +// +// +// +//go:linkname SimpleSeparatedFormat py.simple_separated_format +func SimpleSeparatedFormat(separator *py.Object) *py.Object +// Format a fixed width table for pretty printing. +// +// >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]])) +// --- --------- +// 1 2.34 +// -56 8.999 +// 2 10001 +// --- --------- +// +// The first required argument (`tabular_data`) can be a +// list-of-lists (or another iterable of iterables), a list of named +// tuples, a dictionary of iterables, an iterable of dictionaries, +// an iterable of dataclasses (Python 3.7+), a two-dimensional NumPy array, +// NumPy record array, or a Pandas' dataframe. +// +// +// Table headers +// ------------- +// +// To print nice column headers, supply the second argument (`headers`): +// +// - `headers` can be an explicit list of column headers +// - if `headers="firstrow"`, then the first row of data is used +// - if `headers="keys"`, then dictionary keys or column indices are used +// +// Otherwise a headerless table is produced. +// +// If the number of headers is less than the number of columns, they +// are supposed to be names of the last columns. This is consistent +// with the plain-text format of R and Pandas' dataframes. +// +// >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]], +// ... headers="firstrow")) +// sex age +// ----- ----- ----- +// Alice F 24 +// Bob M 19 +// +// By default, pandas.DataFrame data have an additional column called +// row index. To add a similar column to all other types of data, +// use `showindex="always"` or `showindex=True`. To suppress row indices +// for all types of data, pass `showindex="never" or `showindex=False`. +// To add a custom row index column, pass `showindex=some_iterable`. +// +// >>> print(tabulate([["F",24],["M",19]], showindex="always")) +// - - -- +// 0 F 24 +// 1 M 19 +// - - -- +// +// +// Column alignment +// ---------------- +// +// `tabulate` tries to detect column types automatically, and aligns +// the values properly. By default it aligns decimal points of the +// numbers (or flushes integer numbers to the right), and flushes +// everything else to the left. Possible column alignments +// (`numalign`, `stralign`) are: "right", "center", "left", "decimal" +// (only for `numalign`), and None (to disable alignment). +// +// +// Table formats +// ------------- +// +// `intfmt` is a format specification used for columns which +// contain numeric data without a decimal point. This can also be +// a list or tuple of format strings, one per column. +// +// `floatfmt` is a format specification used for columns which +// contain numeric data with a decimal point. This can also be +// a list or tuple of format strings, one per column. +// +// `None` values are replaced with a `missingval` string (like +// `floatfmt`, this can also be a list of values for different +// columns): +// +// >>> print(tabulate([["spam", 1, None], +// ... ["eggs", 42, 3.14], +// ... ["other", None, 2.7]], missingval="?")) +// ----- -- ---- +// spam 1 ? +// eggs 42 3.14 +// other ? 2.7 +// ----- -- ---- +// +// Various plain-text table formats (`tablefmt`) are supported: +// 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki', +// 'latex', 'latex_raw', 'latex_booktabs', 'latex_longtable' and tsv. +// Variable `tabulate_formats`contains the list of currently supported formats. +// +// "plain" format doesn't use any pseudographics to draw tables, +// it separates columns with a double space: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "plain")) +// strings numbers +// spam 41.9999 +// eggs 451 +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain")) +// spam 41.9999 +// eggs 451 +// +// "simple" format is like Pandoc simple_tables: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "simple")) +// strings numbers +// --------- --------- +// spam 41.9999 +// eggs 451 +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple")) +// ---- -------- +// spam 41.9999 +// eggs 451 +// ---- -------- +// +// "grid" is similar to tables produced by Emacs table.el package or +// Pandoc grid_tables: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "grid")) +// +-----------+-----------+ +// | strings | numbers | +// +===========+===========+ +// | spam | 41.9999 | +// +-----------+-----------+ +// | eggs | 451 | +// +-----------+-----------+ +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid")) +// +------+----------+ +// | spam | 41.9999 | +// +------+----------+ +// | eggs | 451 | +// +------+----------+ +// +// "simple_grid" draws a grid using single-line box-drawing +// characters: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "simple_grid")) +// ┌───────────┬───────────┐ +// │ strings │ numbers │ +// ├───────────┼───────────┤ +// │ spam │ 41.9999 │ +// ├───────────┼───────────┤ +// │ eggs │ 451 │ +// └───────────┴───────────┘ +// +// "rounded_grid" draws a grid using single-line box-drawing +// characters with rounded corners: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "rounded_grid")) +// ╭───────────┬───────────╮ +// │ strings │ numbers │ +// ├───────────┼───────────┤ +// │ spam │ 41.9999 │ +// ├───────────┼───────────┤ +// │ eggs │ 451 │ +// ╰───────────┴───────────╯ +// +// "heavy_grid" draws a grid using bold (thick) single-line box-drawing +// characters: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "heavy_grid")) +// ┏━━━━━━━━━━━┳━━━━━━━━━━━┓ +// ┃ strings ┃ numbers ┃ +// ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ +// ┃ spam ┃ 41.9999 ┃ +// ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ +// ┃ eggs ┃ 451 ┃ +// ┗━━━━━━━━━━━┻━━━━━━━━━━━┛ +// +// "mixed_grid" draws a grid using a mix of light (thin) and heavy (thick) lines +// box-drawing characters: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "mixed_grid")) +// ┍━━━━━━━━━━━┯━━━━━━━━━━━┑ +// │ strings │ numbers │ +// ┝━━━━━━━━━━━┿━━━━━━━━━━━┥ +// │ spam │ 41.9999 │ +// ├───────────┼───────────┤ +// │ eggs │ 451 │ +// ┕━━━━━━━━━━━┷━━━━━━━━━━━┙ +// +// "double_grid" draws a grid using double-line box-drawing +// characters: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "double_grid")) +// ╔═══════════╦═══════════╗ +// ║ strings ║ numbers ║ +// ╠═══════════╬═══════════╣ +// ║ spam ║ 41.9999 ║ +// ╠═══════════╬═══════════╣ +// ║ eggs ║ 451 ║ +// ╚═══════════╩═══════════╝ +// +// "fancy_grid" draws a grid using a mix of single and +// double-line box-drawing characters: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "fancy_grid")) +// ╒═══════════╤═══════════╕ +// │ strings │ numbers │ +// ╞═══════════╪═══════════╡ +// │ spam │ 41.9999 │ +// ├───────────┼───────────┤ +// │ eggs │ 451 │ +// ╘═══════════╧═══════════╛ +// +// "outline" is the same as the "grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "outline")) +// +-----------+-----------+ +// | strings | numbers | +// +===========+===========+ +// | spam | 41.9999 | +// | eggs | 451 | +// +-----------+-----------+ +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="outline")) +// +------+----------+ +// | spam | 41.9999 | +// | eggs | 451 | +// +------+----------+ +// +// "simple_outline" is the same as the "simple_grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "simple_outline")) +// ┌───────────┬───────────┐ +// │ strings │ numbers │ +// ├───────────┼───────────┤ +// │ spam │ 41.9999 │ +// │ eggs │ 451 │ +// └───────────┴───────────┘ +// +// "rounded_outline" is the same as the "rounded_grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "rounded_outline")) +// ╭───────────┬───────────╮ +// │ strings │ numbers │ +// ├───────────┼───────────┤ +// │ spam │ 41.9999 │ +// │ eggs │ 451 │ +// ╰───────────┴───────────╯ +// +// "heavy_outline" is the same as the "heavy_grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "heavy_outline")) +// ┏━━━━━━━━━━━┳━━━━━━━━━━━┓ +// ┃ strings ┃ numbers ┃ +// ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ +// ┃ spam ┃ 41.9999 ┃ +// ┃ eggs ┃ 451 ┃ +// ┗━━━━━━━━━━━┻━━━━━━━━━━━┛ +// +// "mixed_outline" is the same as the "mixed_grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "mixed_outline")) +// ┍━━━━━━━━━━━┯━━━━━━━━━━━┑ +// │ strings │ numbers │ +// ┝━━━━━━━━━━━┿━━━━━━━━━━━┥ +// │ spam │ 41.9999 │ +// │ eggs │ 451 │ +// ┕━━━━━━━━━━━┷━━━━━━━━━━━┙ +// +// "double_outline" is the same as the "double_grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "double_outline")) +// ╔═══════════╦═══════════╗ +// ║ strings ║ numbers ║ +// ╠═══════════╬═══════════╣ +// ║ spam ║ 41.9999 ║ +// ║ eggs ║ 451 ║ +// ╚═══════════╩═══════════╝ +// +// "fancy_outline" is the same as the "fancy_grid" format but doesn't draw lines between rows: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "fancy_outline")) +// ╒═══════════╤═══════════╕ +// │ strings │ numbers │ +// ╞═══════════╪═══════════╡ +// │ spam │ 41.9999 │ +// │ eggs │ 451 │ +// ╘═══════════╧═══════════╛ +// +// "pipe" is like tables in PHP Markdown Extra extension or Pandoc +// pipe_tables: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "pipe")) +// | strings | numbers | +// |:----------|----------:| +// | spam | 41.9999 | +// | eggs | 451 | +// +// "presto" is like tables produce by the Presto CLI: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "presto")) +// strings | numbers +// -----------+----------- +// spam | 41.9999 +// eggs | 451 +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe")) +// |:-----|---------:| +// | spam | 41.9999 | +// | eggs | 451 | +// +// "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They +// are slightly different from "pipe" format by not using colons to +// define column alignment, and using a "+" sign to indicate line +// intersections: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "orgtbl")) +// | strings | numbers | +// |-----------+-----------| +// | spam | 41.9999 | +// | eggs | 451 | +// +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl")) +// | spam | 41.9999 | +// | eggs | 451 | +// +// "rst" is like a simple table format from reStructuredText; please +// note that reStructuredText accepts also "grid" tables: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], +// ... ["strings", "numbers"], "rst")) +// ========= ========= +// strings numbers +// ========= ========= +// spam 41.9999 +// eggs 451 +// ========= ========= +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst")) +// ==== ======== +// spam 41.9999 +// eggs 451 +// ==== ======== +// +// "mediawiki" produces a table markup used in Wikipedia and on other +// MediaWiki-based sites: +// +// >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], +// ... headers="firstrow", tablefmt="mediawiki")) +// {| class="wikitable" style="text-align: left;" +// |+ +// |- +// ! strings !! align="right"| numbers +// |- +// | spam || align="right"| 41.9999 +// |- +// | eggs || align="right"| 451 +// |} +// +// "html" produces HTML markup as an html.escape'd str +// with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML +// and a .str property so that the raw HTML remains accessible +// the unsafehtml table format can be used if an unescaped HTML format is required: +// +// >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], +// ... headers="firstrow", tablefmt="html")) +// +// +// +// +// +// +// +// +//
strings numbers
spam 41.9999
eggs 451
+// +// "latex" produces a tabular environment of LaTeX document markup: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex")) +// \begin{tabular}{lr} +// \hline +// spam & 41.9999 \\ +// eggs & 451 \\ +// \hline +// \end{tabular} +// +// "latex_raw" is similar to "latex", but doesn't escape special characters, +// such as backslash and underscore, so LaTeX commands may embedded into +// cells' values: +// +// >>> print(tabulate([["spam$_9$", 41.9999], ["\\emph{eggs}", "451.0"]], tablefmt="latex_raw")) +// \begin{tabular}{lr} +// \hline +// spam$_9$ & 41.9999 \\ +// \emph{eggs} & 451 \\ +// \hline +// \end{tabular} +// +// "latex_booktabs" produces a tabular environment of LaTeX document markup +// using the booktabs.sty package: +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs")) +// \begin{tabular}{lr} +// \toprule +// spam & 41.9999 \\ +// eggs & 451 \\ +// \bottomrule +// \end{tabular} +// +// "latex_longtable" produces a tabular environment that can stretch along +// multiple pages, using the longtable package for LaTeX. +// +// >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_longtable")) +// \begin{longtable}{lr} +// \hline +// spam & 41.9999 \\ +// eggs & 451 \\ +// \hline +// \end{longtable} +// +// +// Number parsing +// -------------- +// By default, anything which can be parsed as a number is a number. +// This ensures numbers represented as strings are aligned properly. +// This can lead to weird results for particular strings such as +// specific git SHAs e.g. "42992e1" will be parsed into the number +// 429920 and aligned as such. +// +// To completely disable number parsing (and alignment), use +// `disable_numparse=True`. For more fine grained control, a list column +// indices is used to disable number parsing only on those columns +// e.g. `disable_numparse=[0, 2]` would disable number parsing only on the +// first and third columns. +// +// Column Widths and Auto Line Wrapping +// ------------------------------------ +// Tabulate will, by default, set the width of each column to the length of the +// longest element in that column. However, in situations where fields are expected +// to reasonably be too long to look good as a single line, tabulate can help automate +// word wrapping long fields for you. Use the parameter `maxcolwidth` to provide a +// list of maximal column widths +// +// >>> print(tabulate( [('1', 'John Smith', 'This is a rather long description that might look better if it is wrapped a bit')], headers=("Issue Id", "Author", "Description"), maxcolwidths=[None, None, 30], tablefmt="grid" )) +// +------------+------------+-------------------------------+ +// | Issue Id | Author | Description | +// +============+============+===============================+ +// | 1 | John Smith | This is a rather long | +// | | | description that might look | +// | | | better if it is wrapped a bit | +// +------------+------------+-------------------------------+ +// +// Header column width can be specified in a similar way using `maxheadercolwidth` +// +// +// +//go:linkname Tabulate py.tabulate +func Tabulate(tabularData *py.Object, headers *py.Object, tablefmt *py.Object, floatfmt *py.Object, intfmt *py.Object, numalign *py.Object, stralign *py.Object, missingval *py.Object, showindex *py.Object, disableNumparse *py.Object, colalign *py.Object, maxcolwidths *py.Object, rowalign *py.Object, maxheadercolwidths *py.Object) *py.Object diff --git a/tabulate/tabulate/__init__.py b/tabulate/tabulate/__init__.py new file mode 100644 index 00000000..503df348 --- /dev/null +++ b/tabulate/tabulate/__init__.py @@ -0,0 +1,2716 @@ +"""Pretty-print tabular data.""" + +from collections import namedtuple +from collections.abc import Iterable, Sized +from html import escape as htmlescape +from itertools import chain, zip_longest as izip_longest +from functools import reduce, partial +import io +import re +import math +import textwrap +import dataclasses + +try: + import wcwidth # optional wide-character (CJK) support +except ImportError: + wcwidth = None + + +def _is_file(f): + return isinstance(f, io.IOBase) + + +__all__ = ["tabulate", "tabulate_formats", "simple_separated_format"] +try: + from .version import version as __version__ # noqa: F401 +except ImportError: + pass # running __init__.py as a script, AppVeyor pytests + + +# minimum extra space in headers +MIN_PADDING = 2 + +# Whether or not to preserve leading/trailing whitespace in data. +PRESERVE_WHITESPACE = False + +_DEFAULT_FLOATFMT = "g" +_DEFAULT_INTFMT = "" +_DEFAULT_MISSINGVAL = "" +# default align will be overwritten by "left", "center" or "decimal" +# depending on the formatter +_DEFAULT_ALIGN = "default" + + +# if True, enable wide-character (CJK) support +WIDE_CHARS_MODE = wcwidth is not None + +# Constant that can be used as part of passed rows to generate a separating line +# It is purposely an unprintable character, very unlikely to be used in a table +SEPARATING_LINE = "\001" + +Line = namedtuple("Line", ["begin", "hline", "sep", "end"]) + + +DataRow = namedtuple("DataRow", ["begin", "sep", "end"]) + + +# A table structure is supposed to be: +# +# --- lineabove --------- +# headerrow +# --- linebelowheader --- +# datarow +# --- linebetweenrows --- +# ... (more datarows) ... +# --- linebetweenrows --- +# last datarow +# --- linebelow --------- +# +# TableFormat's line* elements can be +# +# - either None, if the element is not used, +# - or a Line tuple, +# - or a function: [col_widths], [col_alignments] -> string. +# +# TableFormat's *row elements can be +# +# - either None, if the element is not used, +# - or a DataRow tuple, +# - or a function: [cell_values], [col_widths], [col_alignments] -> string. +# +# padding (an integer) is the amount of white space around data values. +# +# with_header_hide: +# +# - either None, to display all table elements unconditionally, +# - or a list of elements not to be displayed if the table has column headers. +# +TableFormat = namedtuple( + "TableFormat", + [ + "lineabove", + "linebelowheader", + "linebetweenrows", + "linebelow", + "headerrow", + "datarow", + "padding", + "with_header_hide", + ], +) + + +def _is_separating_line(row): + row_type = type(row) + is_sl = (row_type == list or row_type == str) and ( + (len(row) >= 1 and row[0] == SEPARATING_LINE) + or (len(row) >= 2 and row[1] == SEPARATING_LINE) + ) + return is_sl + + +def _pipe_segment_with_colons(align, colwidth): + """Return a segment of a horizontal line with optional colons which + indicate column's alignment (as in `pipe` output format).""" + w = colwidth + if align in ["right", "decimal"]: + return ("-" * (w - 1)) + ":" + elif align == "center": + return ":" + ("-" * (w - 2)) + ":" + elif align == "left": + return ":" + ("-" * (w - 1)) + else: + return "-" * w + + +def _pipe_line_with_colons(colwidths, colaligns): + """Return a horizontal line with optional colons to indicate column's + alignment (as in `pipe` output format).""" + if not colaligns: # e.g. printing an empty data frame (github issue #15) + colaligns = [""] * len(colwidths) + segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)] + return "|" + "|".join(segments) + "|" + + +def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): + alignment = { + "left": "", + "right": 'align="right"| ', + "center": 'align="center"| ', + "decimal": 'align="right"| ', + } + # hard-coded padding _around_ align attribute and value together + # rather than padding parameter which affects only the value + values_with_attrs = [ + " " + alignment.get(a, "") + c + " " for c, a in zip(cell_values, colaligns) + ] + colsep = separator * 2 + return (separator + colsep.join(values_with_attrs)).rstrip() + + +def _textile_row_with_attrs(cell_values, colwidths, colaligns): + cell_values[0] += " " + alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."} + values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values)) + return "|" + "|".join(values) + "|" + + +def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): + # this table header will be suppressed if there is a header row + return "\n" + + +def _html_row_with_attrs(celltag, unsafe, cell_values, colwidths, colaligns): + alignment = { + "left": "", + "right": ' style="text-align: right;"', + "center": ' style="text-align: center;"', + "decimal": ' style="text-align: right;"', + } + if unsafe: + values_with_attrs = [ + "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), c) + for c, a in zip(cell_values, colaligns) + ] + else: + values_with_attrs = [ + "<{0}{1}>{2}".format(celltag, alignment.get(a, ""), htmlescape(c)) + for c, a in zip(cell_values, colaligns) + ] + rowhtml = "{}".format("".join(values_with_attrs).rstrip()) + if celltag == "th": # it's a header row, create a new table header + rowhtml = f"
\n\n{rowhtml}\n\n" + return rowhtml + + +def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=""): + alignment = { + "left": "", + "right": '', + "center": '', + "decimal": '', + } + values_with_attrs = [ + "{}{} {} ".format(celltag, alignment.get(a, ""), header + c + header) + for c, a in zip(cell_values, colaligns) + ] + return "".join(values_with_attrs) + "||" + + +def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=False): + alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} + tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) + return "\n".join( + [ + ("\\begin{tabular}{" if not longtable else "\\begin{longtable}{") + + tabular_columns_fmt + + "}", + "\\toprule" if booktabs else "\\hline", + ] + ) + + +def _asciidoc_row(is_header, *args): + """handle header and data rows for asciidoc format""" + + def make_header_line(is_header, colwidths, colaligns): + # generate the column specifiers + + alignment = {"left": "<", "right": ">", "center": "^", "decimal": ">"} + # use the column widths generated by tabulate for the asciidoc column width specifiers + asciidoc_alignments = zip( + colwidths, [alignment[colalign] for colalign in colaligns] + ) + asciidoc_column_specifiers = [ + "{:d}{}".format(width, align) for width, align in asciidoc_alignments + ] + header_list = ['cols="' + (",".join(asciidoc_column_specifiers)) + '"'] + + # generate the list of options (currently only "header") + options_list = [] + + if is_header: + options_list.append("header") + + if options_list: + header_list += ['options="' + ",".join(options_list) + '"'] + + # generate the list of entries in the table header field + + return "[{}]\n|====".format(",".join(header_list)) + + if len(args) == 2: + # two arguments are passed if called in the context of aboveline + # print the table header with column widths and optional header tag + return make_header_line(False, *args) + + elif len(args) == 3: + # three arguments are passed if called in the context of dataline or headerline + # print the table line and make the aboveline if it is a header + + cell_values, colwidths, colaligns = args + data_line = "|" + "|".join(cell_values) + + if is_header: + return make_header_line(True, colwidths, colaligns) + "\n" + data_line + else: + return data_line + + else: + raise ValueError( + " _asciidoc_row() requires two (colwidths, colaligns) " + + "or three (cell_values, colwidths, colaligns) arguments) " + ) + + +LATEX_ESCAPE_RULES = { + r"&": r"\&", + r"%": r"\%", + r"$": r"\$", + r"#": r"\#", + r"_": r"\_", + r"^": r"\^{}", + r"{": r"\{", + r"}": r"\}", + r"~": r"\textasciitilde{}", + "\\": r"\textbackslash{}", + r"<": r"\ensuremath{<}", + r">": r"\ensuremath{>}", +} + + +def _latex_row(cell_values, colwidths, colaligns, escrules=LATEX_ESCAPE_RULES): + def escape_char(c): + return escrules.get(c, c) + + escaped_values = ["".join(map(escape_char, cell)) for cell in cell_values] + rowfmt = DataRow("", "&", "\\\\") + return _build_simple_row(escaped_values, rowfmt) + + +def _rst_escape_first_column(rows, headers): + def escape_empty(val): + if isinstance(val, (str, bytes)) and not val.strip(): + return ".." + else: + return val + + new_headers = list(headers) + new_rows = [] + if headers: + new_headers[0] = escape_empty(headers[0]) + for row in rows: + new_row = list(row) + if new_row: + new_row[0] = escape_empty(row[0]) + new_rows.append(new_row) + return new_rows, new_headers + + +_table_formats = { + "simple": TableFormat( + lineabove=Line("", "-", " ", ""), + linebelowheader=Line("", "-", " ", ""), + linebetweenrows=None, + linebelow=Line("", "-", " ", ""), + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, + with_header_hide=["lineabove", "linebelow"], + ), + "plain": TableFormat( + lineabove=None, + linebelowheader=None, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, + with_header_hide=None, + ), + "grid": TableFormat( + lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("+", "=", "+", "+"), + linebetweenrows=Line("+", "-", "+", "+"), + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), + "simple_grid": TableFormat( + lineabove=Line("┌", "─", "┬", "┐"), + linebelowheader=Line("├", "─", "┼", "┤"), + linebetweenrows=Line("├", "─", "┼", "┤"), + linebelow=Line("└", "─", "┴", "┘"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "rounded_grid": TableFormat( + lineabove=Line("╭", "─", "┬", "╮"), + linebelowheader=Line("├", "─", "┼", "┤"), + linebetweenrows=Line("├", "─", "┼", "┤"), + linebelow=Line("╰", "─", "┴", "╯"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "heavy_grid": TableFormat( + lineabove=Line("┏", "━", "┳", "┓"), + linebelowheader=Line("┣", "━", "╋", "┫"), + linebetweenrows=Line("┣", "━", "╋", "┫"), + linebelow=Line("┗", "━", "┻", "┛"), + headerrow=DataRow("┃", "┃", "┃"), + datarow=DataRow("┃", "┃", "┃"), + padding=1, + with_header_hide=None, + ), + "mixed_grid": TableFormat( + lineabove=Line("┍", "━", "┯", "┑"), + linebelowheader=Line("┝", "━", "┿", "┥"), + linebetweenrows=Line("├", "─", "┼", "┤"), + linebelow=Line("┕", "━", "┷", "┙"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "double_grid": TableFormat( + lineabove=Line("╔", "═", "╦", "╗"), + linebelowheader=Line("╠", "═", "╬", "╣"), + linebetweenrows=Line("╠", "═", "╬", "╣"), + linebelow=Line("╚", "═", "╩", "╝"), + headerrow=DataRow("║", "║", "║"), + datarow=DataRow("║", "║", "║"), + padding=1, + with_header_hide=None, + ), + "fancy_grid": TableFormat( + lineabove=Line("╒", "═", "╤", "╕"), + linebelowheader=Line("╞", "═", "╪", "╡"), + linebetweenrows=Line("├", "─", "┼", "┤"), + linebelow=Line("╘", "═", "╧", "╛"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "outline": TableFormat( + lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("+", "=", "+", "+"), + linebetweenrows=None, + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), + "simple_outline": TableFormat( + lineabove=Line("┌", "─", "┬", "┐"), + linebelowheader=Line("├", "─", "┼", "┤"), + linebetweenrows=None, + linebelow=Line("└", "─", "┴", "┘"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "rounded_outline": TableFormat( + lineabove=Line("╭", "─", "┬", "╮"), + linebelowheader=Line("├", "─", "┼", "┤"), + linebetweenrows=None, + linebelow=Line("╰", "─", "┴", "╯"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "heavy_outline": TableFormat( + lineabove=Line("┏", "━", "┳", "┓"), + linebelowheader=Line("┣", "━", "╋", "┫"), + linebetweenrows=None, + linebelow=Line("┗", "━", "┻", "┛"), + headerrow=DataRow("┃", "┃", "┃"), + datarow=DataRow("┃", "┃", "┃"), + padding=1, + with_header_hide=None, + ), + "mixed_outline": TableFormat( + lineabove=Line("┍", "━", "┯", "┑"), + linebelowheader=Line("┝", "━", "┿", "┥"), + linebetweenrows=None, + linebelow=Line("┕", "━", "┷", "┙"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "double_outline": TableFormat( + lineabove=Line("╔", "═", "╦", "╗"), + linebelowheader=Line("╠", "═", "╬", "╣"), + linebetweenrows=None, + linebelow=Line("╚", "═", "╩", "╝"), + headerrow=DataRow("║", "║", "║"), + datarow=DataRow("║", "║", "║"), + padding=1, + with_header_hide=None, + ), + "fancy_outline": TableFormat( + lineabove=Line("╒", "═", "╤", "╕"), + linebelowheader=Line("╞", "═", "╪", "╡"), + linebetweenrows=None, + linebelow=Line("╘", "═", "╧", "╛"), + headerrow=DataRow("│", "│", "│"), + datarow=DataRow("│", "│", "│"), + padding=1, + with_header_hide=None, + ), + "github": TableFormat( + lineabove=Line("|", "-", "|", "|"), + linebelowheader=Line("|", "-", "|", "|"), + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=["lineabove"], + ), + "pipe": TableFormat( + lineabove=_pipe_line_with_colons, + linebelowheader=_pipe_line_with_colons, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=["lineabove"], + ), + "orgtbl": TableFormat( + lineabove=None, + linebelowheader=Line("|", "-", "+", "|"), + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), + "jira": TableFormat( + lineabove=None, + linebelowheader=None, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("||", "||", "||"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), + "presto": TableFormat( + lineabove=None, + linebelowheader=Line("", "-", "+", ""), + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("", "|", ""), + datarow=DataRow("", "|", ""), + padding=1, + with_header_hide=None, + ), + "pretty": TableFormat( + lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("+", "-", "+", "+"), + linebetweenrows=None, + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), + "psql": TableFormat( + lineabove=Line("+", "-", "+", "+"), + linebelowheader=Line("|", "-", "+", "|"), + linebetweenrows=None, + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), + "rst": TableFormat( + lineabove=Line("", "=", " ", ""), + linebelowheader=Line("", "=", " ", ""), + linebetweenrows=None, + linebelow=Line("", "=", " ", ""), + headerrow=DataRow("", " ", ""), + datarow=DataRow("", " ", ""), + padding=0, + with_header_hide=None, + ), + "mediawiki": TableFormat( + lineabove=Line( + '{| class="wikitable" style="text-align: left;"', + "", + "", + "\n|+ \n|-", + ), + linebelowheader=Line("|-", "", "", ""), + linebetweenrows=Line("|-", "", "", ""), + linebelow=Line("|}", "", "", ""), + headerrow=partial(_mediawiki_row_with_attrs, "!"), + datarow=partial(_mediawiki_row_with_attrs, "|"), + padding=0, + with_header_hide=None, + ), + "moinmoin": TableFormat( + lineabove=None, + linebelowheader=None, + linebetweenrows=None, + linebelow=None, + headerrow=partial(_moin_row_with_attrs, "||", header="'''"), + datarow=partial(_moin_row_with_attrs, "||"), + padding=1, + with_header_hide=None, + ), + "youtrack": TableFormat( + lineabove=None, + linebelowheader=None, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|| ", " || ", " || "), + datarow=DataRow("| ", " | ", " |"), + padding=1, + with_header_hide=None, + ), + "html": TableFormat( + lineabove=_html_begin_table_without_header, + linebelowheader="", + linebetweenrows=None, + linebelow=Line("\n
", "", "", ""), + headerrow=partial(_html_row_with_attrs, "th", False), + datarow=partial(_html_row_with_attrs, "td", False), + padding=0, + with_header_hide=["lineabove"], + ), + "unsafehtml": TableFormat( + lineabove=_html_begin_table_without_header, + linebelowheader="", + linebetweenrows=None, + linebelow=Line("\n", "", "", ""), + headerrow=partial(_html_row_with_attrs, "th", True), + datarow=partial(_html_row_with_attrs, "td", True), + padding=0, + with_header_hide=["lineabove"], + ), + "latex": TableFormat( + lineabove=_latex_line_begin_tabular, + linebelowheader=Line("\\hline", "", "", ""), + linebetweenrows=None, + linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), + headerrow=_latex_row, + datarow=_latex_row, + padding=1, + with_header_hide=None, + ), + "latex_raw": TableFormat( + lineabove=_latex_line_begin_tabular, + linebelowheader=Line("\\hline", "", "", ""), + linebetweenrows=None, + linebelow=Line("\\hline\n\\end{tabular}", "", "", ""), + headerrow=partial(_latex_row, escrules={}), + datarow=partial(_latex_row, escrules={}), + padding=1, + with_header_hide=None, + ), + "latex_booktabs": TableFormat( + lineabove=partial(_latex_line_begin_tabular, booktabs=True), + linebelowheader=Line("\\midrule", "", "", ""), + linebetweenrows=None, + linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""), + headerrow=_latex_row, + datarow=_latex_row, + padding=1, + with_header_hide=None, + ), + "latex_longtable": TableFormat( + lineabove=partial(_latex_line_begin_tabular, longtable=True), + linebelowheader=Line("\\hline\n\\endhead", "", "", ""), + linebetweenrows=None, + linebelow=Line("\\hline\n\\end{longtable}", "", "", ""), + headerrow=_latex_row, + datarow=_latex_row, + padding=1, + with_header_hide=None, + ), + "tsv": TableFormat( + lineabove=None, + linebelowheader=None, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("", "\t", ""), + datarow=DataRow("", "\t", ""), + padding=0, + with_header_hide=None, + ), + "textile": TableFormat( + lineabove=None, + linebelowheader=None, + linebetweenrows=None, + linebelow=None, + headerrow=DataRow("|_. ", "|_.", "|"), + datarow=_textile_row_with_attrs, + padding=1, + with_header_hide=None, + ), + "asciidoc": TableFormat( + lineabove=partial(_asciidoc_row, False), + linebelowheader=None, + linebetweenrows=None, + linebelow=Line("|====", "", "", ""), + headerrow=partial(_asciidoc_row, True), + datarow=partial(_asciidoc_row, False), + padding=1, + with_header_hide=["lineabove"], + ), +} + + +tabulate_formats = list(sorted(_table_formats.keys())) + +# The table formats for which multiline cells will be folded into subsequent +# table rows. The key is the original format specified at the API. The value is +# the format that will be used to represent the original format. +multiline_formats = { + "plain": "plain", + "simple": "simple", + "grid": "grid", + "simple_grid": "simple_grid", + "rounded_grid": "rounded_grid", + "heavy_grid": "heavy_grid", + "mixed_grid": "mixed_grid", + "double_grid": "double_grid", + "fancy_grid": "fancy_grid", + "pipe": "pipe", + "orgtbl": "orgtbl", + "jira": "jira", + "presto": "presto", + "pretty": "pretty", + "psql": "psql", + "rst": "rst", +} + +# TODO: Add multiline support for the remaining table formats: +# - mediawiki: Replace \n with
+# - moinmoin: TBD +# - youtrack: TBD +# - html: Replace \n with
+# - latex*: Use "makecell" package: In header, replace X\nY with +# \thead{X\\Y} and in data row, replace X\nY with \makecell{X\\Y} +# - tsv: TBD +# - textile: Replace \n with
(must be well-formed XML) + +_multiline_codes = re.compile(r"\r|\n|\r\n") +_multiline_codes_bytes = re.compile(b"\r|\n|\r\n") + +# Handle ANSI escape sequences for both control sequence introducer (CSI) and +# operating system command (OSC). Both of these begin with 0x1b (or octal 033), +# which will be shown below as ESC. +# +# CSI ANSI escape codes have the following format, defined in section 5.4 of ECMA-48: +# +# CSI: ESC followed by the '[' character (0x5b) +# Parameter Bytes: 0..n bytes in the range 0x30-0x3f +# Intermediate Bytes: 0..n bytes in the range 0x20-0x2f +# Final Byte: a single byte in the range 0x40-0x7e +# +# Also include the terminal hyperlink sequences as described here: +# https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda +# +# OSC 8 ; params ; uri ST display_text OSC 8 ;; ST +# +# Example: \x1b]8;;https://example.com\x5ctext to show\x1b]8;;\x5c +# +# Where: +# OSC: ESC followed by the ']' character (0x5d) +# params: 0..n optional key value pairs separated by ':' (e.g. foo=bar:baz=qux:abc=123) +# URI: the actual URI with protocol scheme (e.g. https://, file://, ftp://) +# ST: ESC followed by the '\' character (0x5c) +_esc = r"\x1b" +_csi = rf"{_esc}\[" +_osc = rf"{_esc}\]" +_st = rf"{_esc}\\" + +_ansi_escape_pat = rf""" + ( + # terminal colors, etc + {_csi} # CSI + [\x30-\x3f]* # parameter bytes + [\x20-\x2f]* # intermediate bytes + [\x40-\x7e] # final byte + | + # terminal hyperlinks + {_osc}8; # OSC opening + (\w+=\w+:?)* # key=value params list (submatch 2) + ; # delimiter + ([^{_esc}]+) # URI - anything but ESC (submatch 3) + {_st} # ST + ([^{_esc}]+) # link text - anything but ESC (submatch 4) + {_osc}8;;{_st} # "closing" OSC sequence + ) +""" +_ansi_codes = re.compile(_ansi_escape_pat, re.VERBOSE) +_ansi_codes_bytes = re.compile(_ansi_escape_pat.encode("utf8"), re.VERBOSE) +_ansi_color_reset_code = "\033[0m" + +_float_with_thousands_separators = re.compile( + r"^(([+-]?[0-9]{1,3})(?:,([0-9]{3}))*)?(?(1)\.[0-9]*|\.[0-9]+)?$" +) + + +def simple_separated_format(separator): + """Construct a simple TableFormat with columns separated by a separator. + + >>> tsv = simple_separated_format("\\t") ; \ + tabulate([["foo", 1], ["spam", 23]], tablefmt=tsv) == 'foo \\t 1\\nspam\\t23' + True + + """ + return TableFormat( + None, + None, + None, + None, + headerrow=DataRow("", separator, ""), + datarow=DataRow("", separator, ""), + padding=0, + with_header_hide=None, + ) + + +def _isnumber_with_thousands_separator(string): + """ + >>> _isnumber_with_thousands_separator(".") + False + >>> _isnumber_with_thousands_separator("1") + True + >>> _isnumber_with_thousands_separator("1.") + True + >>> _isnumber_with_thousands_separator(".1") + True + >>> _isnumber_with_thousands_separator("1000") + False + >>> _isnumber_with_thousands_separator("1,000") + True + >>> _isnumber_with_thousands_separator("1,0000") + False + >>> _isnumber_with_thousands_separator("1,000.1234") + True + >>> _isnumber_with_thousands_separator(b"1,000.1234") + True + >>> _isnumber_with_thousands_separator("+1,000.1234") + True + >>> _isnumber_with_thousands_separator("-1,000.1234") + True + """ + try: + string = string.decode() + except (UnicodeDecodeError, AttributeError): + pass + + return bool(re.match(_float_with_thousands_separators, string)) + + +def _isconvertible(conv, string): + try: + conv(string) + return True + except (ValueError, TypeError): + return False + + +def _isnumber(string): + """ + >>> _isnumber("123.45") + True + >>> _isnumber("123") + True + >>> _isnumber("spam") + False + >>> _isnumber("123e45678") + False + >>> _isnumber("inf") + True + """ + if not _isconvertible(float, string): + return False + elif isinstance(string, (str, bytes)) and ( + math.isinf(float(string)) or math.isnan(float(string)) + ): + return string.lower() in ["inf", "-inf", "nan"] + return True + + +def _isint(string, inttype=int): + """ + >>> _isint("123") + True + >>> _isint("123.45") + False + """ + return ( + type(string) is inttype + or isinstance(string, (bytes, str)) + and _isconvertible(inttype, string) + ) + + +def _isbool(string): + """ + >>> _isbool(True) + True + >>> _isbool("False") + True + >>> _isbool(1) + False + """ + return type(string) is bool or ( + isinstance(string, (bytes, str)) and string in ("True", "False") + ) + + +def _type(string, has_invisible=True, numparse=True): + """The least generic type (type(None), int, float, str, unicode). + + >>> _type(None) is type(None) + True + >>> _type("foo") is type("") + True + >>> _type("1") is type(1) + True + >>> _type('\x1b[31m42\x1b[0m') is type(42) + True + >>> _type('\x1b[31m42\x1b[0m') is type(42) + True + + """ + + if has_invisible and isinstance(string, (str, bytes)): + string = _strip_ansi(string) + + if string is None: + return type(None) + elif hasattr(string, "isoformat"): # datetime.datetime, date, and time + return str + elif _isbool(string): + return bool + elif _isint(string) and numparse: + return int + elif _isnumber(string) and numparse: + return float + elif isinstance(string, bytes): + return bytes + else: + return str + + +def _afterpoint(string): + """Symbols after a decimal point, -1 if the string lacks the decimal point. + + >>> _afterpoint("123.45") + 2 + >>> _afterpoint("1001") + -1 + >>> _afterpoint("eggs") + -1 + >>> _afterpoint("123e45") + 2 + >>> _afterpoint("123,456.78") + 2 + + """ + if _isnumber(string) or _isnumber_with_thousands_separator(string): + if _isint(string): + return -1 + else: + pos = string.rfind(".") + pos = string.lower().rfind("e") if pos < 0 else pos + if pos >= 0: + return len(string) - pos - 1 + else: + return -1 # no point + else: + return -1 # not a number + + +def _padleft(width, s): + """Flush right. + + >>> _padleft(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430' + True + + """ + fmt = "{0:>%ds}" % width + return fmt.format(s) + + +def _padright(width, s): + """Flush left. + + >>> _padright(6, '\u044f\u0439\u0446\u0430') == '\u044f\u0439\u0446\u0430 ' + True + + """ + fmt = "{0:<%ds}" % width + return fmt.format(s) + + +def _padboth(width, s): + """Center string. + + >>> _padboth(6, '\u044f\u0439\u0446\u0430') == ' \u044f\u0439\u0446\u0430 ' + True + + """ + fmt = "{0:^%ds}" % width + return fmt.format(s) + + +def _padnone(ignore_width, s): + return s + + +def _strip_ansi(s): + r"""Remove ANSI escape sequences, both CSI (color codes, etc) and OSC hyperlinks. + + CSI sequences are simply removed from the output, while OSC hyperlinks are replaced + with the link text. Note: it may be desirable to show the URI instead but this is not + supported. + + >>> repr(_strip_ansi('\x1B]8;;https://example.com\x1B\\This is a link\x1B]8;;\x1B\\')) + "'This is a link'" + + >>> repr(_strip_ansi('\x1b[31mred\x1b[0m text')) + "'red text'" + + """ + if isinstance(s, str): + return _ansi_codes.sub(r"\4", s) + else: # a bytestring + return _ansi_codes_bytes.sub(r"\4", s) + + +def _visible_width(s): + """Visible width of a printed string. ANSI color codes are removed. + + >>> _visible_width('\x1b[31mhello\x1b[0m'), _visible_width("world") + (5, 5) + + """ + # optional wide-character support + if wcwidth is not None and WIDE_CHARS_MODE: + len_fn = wcwidth.wcswidth + else: + len_fn = len + if isinstance(s, (str, bytes)): + return len_fn(_strip_ansi(s)) + else: + return len_fn(str(s)) + + +def _is_multiline(s): + if isinstance(s, str): + return bool(re.search(_multiline_codes, s)) + else: # a bytestring + return bool(re.search(_multiline_codes_bytes, s)) + + +def _multiline_width(multiline_s, line_width_fn=len): + """Visible width of a potentially multiline content.""" + return max(map(line_width_fn, re.split("[\r\n]", multiline_s))) + + +def _choose_width_fn(has_invisible, enable_widechars, is_multiline): + """Return a function to calculate visible cell width.""" + if has_invisible: + line_width_fn = _visible_width + elif enable_widechars: # optional wide-character support if available + line_width_fn = wcwidth.wcswidth + else: + line_width_fn = len + if is_multiline: + width_fn = lambda s: _multiline_width(s, line_width_fn) # noqa + else: + width_fn = line_width_fn + return width_fn + + +def _align_column_choose_padfn(strings, alignment, has_invisible): + if alignment == "right": + if not PRESERVE_WHITESPACE: + strings = [s.strip() for s in strings] + padfn = _padleft + elif alignment == "center": + if not PRESERVE_WHITESPACE: + strings = [s.strip() for s in strings] + padfn = _padboth + elif alignment == "decimal": + if has_invisible: + decimals = [_afterpoint(_strip_ansi(s)) for s in strings] + else: + decimals = [_afterpoint(s) for s in strings] + maxdecimals = max(decimals) + strings = [s + (maxdecimals - decs) * " " for s, decs in zip(strings, decimals)] + padfn = _padleft + elif not alignment: + padfn = _padnone + else: + if not PRESERVE_WHITESPACE: + strings = [s.strip() for s in strings] + padfn = _padright + return strings, padfn + + +def _align_column_choose_width_fn(has_invisible, enable_widechars, is_multiline): + if has_invisible: + line_width_fn = _visible_width + elif enable_widechars: # optional wide-character support if available + line_width_fn = wcwidth.wcswidth + else: + line_width_fn = len + if is_multiline: + width_fn = lambda s: _align_column_multiline_width(s, line_width_fn) # noqa + else: + width_fn = line_width_fn + return width_fn + + +def _align_column_multiline_width(multiline_s, line_width_fn=len): + """Visible width of a potentially multiline content.""" + return list(map(line_width_fn, re.split("[\r\n]", multiline_s))) + + +def _flat_list(nested_list): + ret = [] + for item in nested_list: + if isinstance(item, list): + for subitem in item: + ret.append(subitem) + else: + ret.append(item) + return ret + + +def _align_column( + strings, + alignment, + minwidth=0, + has_invisible=True, + enable_widechars=False, + is_multiline=False, +): + """[string] -> [padded_string]""" + strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible) + width_fn = _align_column_choose_width_fn( + has_invisible, enable_widechars, is_multiline + ) + + s_widths = list(map(width_fn, strings)) + maxwidth = max(max(_flat_list(s_widths)), minwidth) + # TODO: refactor column alignment in single-line and multiline modes + if is_multiline: + if not enable_widechars and not has_invisible: + padded_strings = [ + "\n".join([padfn(maxwidth, s) for s in ms.splitlines()]) + for ms in strings + ] + else: + # enable wide-character width corrections + s_lens = [[len(s) for s in re.split("[\r\n]", ms)] for ms in strings] + visible_widths = [ + [maxwidth - (w - l) for w, l in zip(mw, ml)] + for mw, ml in zip(s_widths, s_lens) + ] + # wcswidth and _visible_width don't count invisible characters; + # padfn doesn't need to apply another correction + padded_strings = [ + "\n".join([padfn(w, s) for s, w in zip((ms.splitlines() or ms), mw)]) + for ms, mw in zip(strings, visible_widths) + ] + else: # single-line cell values + if not enable_widechars and not has_invisible: + padded_strings = [padfn(maxwidth, s) for s in strings] + else: + # enable wide-character width corrections + s_lens = list(map(len, strings)) + visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)] + # wcswidth and _visible_width don't count invisible characters; + # padfn doesn't need to apply another correction + padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)] + return padded_strings + + +def _more_generic(type1, type2): + types = { + type(None): 0, + bool: 1, + int: 2, + float: 3, + bytes: 4, + str: 5, + } + invtypes = { + 5: str, + 4: bytes, + 3: float, + 2: int, + 1: bool, + 0: type(None), + } + moregeneric = max(types.get(type1, 5), types.get(type2, 5)) + return invtypes[moregeneric] + + +def _column_type(strings, has_invisible=True, numparse=True): + """The least generic type all column values are convertible to. + + >>> _column_type([True, False]) is bool + True + >>> _column_type(["1", "2"]) is int + True + >>> _column_type(["1", "2.3"]) is float + True + >>> _column_type(["1", "2.3", "four"]) is str + True + >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is str + True + >>> _column_type([None, "brux"]) is str + True + >>> _column_type([1, 2, None]) is int + True + >>> import datetime as dt + >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is str + True + + """ + types = [_type(s, has_invisible, numparse) for s in strings] + return reduce(_more_generic, types, bool) + + +def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): + """Format a value according to its type. + + Unicode is supported: + + >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \ + tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \ + good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430 \\u0446\\u0438\\u0444\\u0440\\u0430\\n------- -------\\n\\u0430\\u0437 2\\n\\u0431\\u0443\\u043a\\u0438 4' ; \ + tabulate(tbl, headers=hrow) == good_result + True + + """ # noqa + if val is None: + return missingval + + if valtype is str: + return f"{val}" + elif valtype is int: + return format(val, intfmt) + elif valtype is bytes: + try: + return str(val, "ascii") + except (TypeError, UnicodeDecodeError): + return str(val) + elif valtype is float: + is_a_colored_number = has_invisible and isinstance(val, (str, bytes)) + if is_a_colored_number: + raw_val = _strip_ansi(val) + formatted_val = format(float(raw_val), floatfmt) + return val.replace(raw_val, formatted_val) + else: + return format(float(val), floatfmt) + else: + return f"{val}" + + +def _align_header( + header, alignment, width, visible_width, is_multiline=False, width_fn=None +): + "Pad string header to width chars given known visible_width of the header." + if is_multiline: + header_lines = re.split(_multiline_codes, header) + padded_lines = [ + _align_header(h, alignment, width, width_fn(h)) for h in header_lines + ] + return "\n".join(padded_lines) + # else: not multiline + ninvisible = len(header) - visible_width + width += ninvisible + if alignment == "left": + return _padright(width, header) + elif alignment == "center": + return _padboth(width, header) + elif not alignment: + return f"{header}" + else: + return _padleft(width, header) + + +def _remove_separating_lines(rows): + if type(rows) == list: + separating_lines = [] + sans_rows = [] + for index, row in enumerate(rows): + if _is_separating_line(row): + separating_lines.append(index) + else: + sans_rows.append(row) + return sans_rows, separating_lines + else: + return rows, None + + +def _reinsert_separating_lines(rows, separating_lines): + if separating_lines: + for index in separating_lines: + rows.insert(index, SEPARATING_LINE) + + +def _prepend_row_index(rows, index): + """Add a left-most index column.""" + if index is None or index is False: + return rows + if isinstance(index, Sized) and len(index) != len(rows): + raise ValueError( + "index must be as long as the number of data rows: " + + "len(index)={} len(rows)={}".format(len(index), len(rows)) + ) + sans_rows, separating_lines = _remove_separating_lines(rows) + new_rows = [] + index_iter = iter(index) + for row in sans_rows: + index_v = next(index_iter) + new_rows.append([index_v] + list(row)) + rows = new_rows + _reinsert_separating_lines(rows, separating_lines) + return rows + + +def _bool(val): + "A wrapper around standard bool() which doesn't throw on NumPy arrays" + try: + return bool(val) + except ValueError: # val is likely to be a numpy array with many elements + return False + + +def _normalize_tabular_data(tabular_data, headers, showindex="default"): + """Transform a supported data type to a list of lists, and a list of headers. + + Supported tabular data types: + + * list-of-lists or another iterable of iterables + + * list of named tuples (usually used with headers="keys") + + * list of dicts (usually used with headers="keys") + + * list of OrderedDicts (usually used with headers="keys") + + * list of dataclasses (Python 3.7+ only, usually used with headers="keys") + + * 2D NumPy arrays + + * NumPy record arrays (usually used with headers="keys") + + * dict of iterables (usually used with headers="keys") + + * pandas.DataFrame (usually used with headers="keys") + + The first row can be used as headers if headers="firstrow", + column indices can be used as headers if headers="keys". + + If showindex="default", show row indices of the pandas.DataFrame. + If showindex="always", show row indices for all types of data. + If showindex="never", don't show row indices for all types of data. + If showindex is an iterable, show its values as row indices. + + """ + + try: + bool(headers) + is_headers2bool_broken = False # noqa + except ValueError: # numpy.ndarray, pandas.core.index.Index, ... + is_headers2bool_broken = True # noqa + headers = list(headers) + + index = None + if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"): + # dict-like and pandas.DataFrame? + if hasattr(tabular_data.values, "__call__"): + # likely a conventional dict + keys = tabular_data.keys() + rows = list( + izip_longest(*tabular_data.values()) + ) # columns have to be transposed + elif hasattr(tabular_data, "index"): + # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0) + keys = list(tabular_data) + if ( + showindex in ["default", "always", True] + and tabular_data.index.name is not None + ): + if isinstance(tabular_data.index.name, list): + keys[:0] = tabular_data.index.name + else: + keys[:0] = [tabular_data.index.name] + vals = tabular_data.values # values matrix doesn't need to be transposed + # for DataFrames add an index per default + index = list(tabular_data.index) + rows = [list(row) for row in vals] + else: + raise ValueError("tabular data doesn't appear to be a dict or a DataFrame") + + if headers == "keys": + headers = list(map(str, keys)) # headers should be strings + + else: # it's a usual iterable of iterables, or a NumPy array, or an iterable of dataclasses + rows = list(tabular_data) + + if headers == "keys" and not rows: + # an empty table (issue #81) + headers = [] + elif ( + headers == "keys" + and hasattr(tabular_data, "dtype") + and getattr(tabular_data.dtype, "names") + ): + # numpy record array + headers = tabular_data.dtype.names + elif ( + headers == "keys" + and len(rows) > 0 + and isinstance(rows[0], tuple) + and hasattr(rows[0], "_fields") + ): + # namedtuple + headers = list(map(str, rows[0]._fields)) + elif len(rows) > 0 and hasattr(rows[0], "keys") and hasattr(rows[0], "values"): + # dict-like object + uniq_keys = set() # implements hashed lookup + keys = [] # storage for set + if headers == "firstrow": + firstdict = rows[0] if len(rows) > 0 else {} + keys.extend(firstdict.keys()) + uniq_keys.update(keys) + rows = rows[1:] + for row in rows: + for k in row.keys(): + # Save unique items in input order + if k not in uniq_keys: + keys.append(k) + uniq_keys.add(k) + if headers == "keys": + headers = keys + elif isinstance(headers, dict): + # a dict of headers for a list of dicts + headers = [headers.get(k, k) for k in keys] + headers = list(map(str, headers)) + elif headers == "firstrow": + if len(rows) > 0: + headers = [firstdict.get(k, k) for k in keys] + headers = list(map(str, headers)) + else: + headers = [] + elif headers: + raise ValueError( + "headers for a list of dicts is not a dict or a keyword" + ) + rows = [[row.get(k) for k in keys] for row in rows] + + elif ( + headers == "keys" + and hasattr(tabular_data, "description") + and hasattr(tabular_data, "fetchone") + and hasattr(tabular_data, "rowcount") + ): + # Python Database API cursor object (PEP 0249) + # print tabulate(cursor, headers='keys') + headers = [column[0] for column in tabular_data.description] + + elif ( + dataclasses is not None + and len(rows) > 0 + and dataclasses.is_dataclass(rows[0]) + ): + # Python 3.7+'s dataclass + field_names = [field.name for field in dataclasses.fields(rows[0])] + if headers == "keys": + headers = field_names + rows = [[getattr(row, f) for f in field_names] for row in rows] + + elif headers == "keys" and len(rows) > 0: + # keys are column indices + headers = list(map(str, range(len(rows[0])))) + + # take headers from the first row if necessary + if headers == "firstrow" and len(rows) > 0: + if index is not None: + headers = [index[0]] + list(rows[0]) + index = index[1:] + else: + headers = rows[0] + headers = list(map(str, headers)) # headers should be strings + rows = rows[1:] + elif headers == "firstrow": + headers = [] + + headers = list(map(str, headers)) + # rows = list(map(list, rows)) + rows = list(map(lambda r: r if _is_separating_line(r) else list(r), rows)) + + # add or remove an index column + showindex_is_a_str = type(showindex) in [str, bytes] + if showindex == "default" and index is not None: + rows = _prepend_row_index(rows, index) + elif isinstance(showindex, Sized) and not showindex_is_a_str: + rows = _prepend_row_index(rows, list(showindex)) + elif isinstance(showindex, Iterable) and not showindex_is_a_str: + rows = _prepend_row_index(rows, showindex) + elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str): + if index is None: + index = list(range(len(rows))) + rows = _prepend_row_index(rows, index) + elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str): + pass + + # pad with empty headers for initial columns if necessary + if headers and len(rows) > 0: + nhs = len(headers) + ncols = len(rows[0]) + if nhs < ncols: + headers = [""] * (ncols - nhs) + headers + + return rows, headers + + +def _wrap_text_to_colwidths(list_of_lists, colwidths, numparses=True): + numparses = _expand_iterable(numparses, len(list_of_lists[0]), True) + + result = [] + + for row in list_of_lists: + new_row = [] + for cell, width, numparse in zip(row, colwidths, numparses): + if _isnumber(cell) and numparse: + new_row.append(cell) + continue + + if width is not None: + wrapper = _CustomTextWrap(width=width) + # Cast based on our internal type handling + # Any future custom formatting of types (such as datetimes) + # may need to be more explicit than just `str` of the object + casted_cell = ( + str(cell) if _isnumber(cell) else _type(cell, numparse)(cell) + ) + wrapped = wrapper.wrap(casted_cell) + new_row.append("\n".join(wrapped)) + else: + new_row.append(cell) + result.append(new_row) + + return result + + +def _to_str(s, encoding="utf8", errors="ignore"): + """ + A type safe wrapper for converting a bytestring to str. This is essentially just + a wrapper around .decode() intended for use with things like map(), but with some + specific behavior: + + 1. if the given parameter is not a bytestring, it is returned unmodified + 2. decode() is called for the given parameter and assumes utf8 encoding, but the + default error behavior is changed from 'strict' to 'ignore' + + >>> repr(_to_str(b'foo')) + "'foo'" + + >>> repr(_to_str('foo')) + "'foo'" + + >>> repr(_to_str(42)) + "'42'" + + """ + if isinstance(s, bytes): + return s.decode(encoding=encoding, errors=errors) + return str(s) + + +def tabulate( + tabular_data, + headers=(), + tablefmt="simple", + floatfmt=_DEFAULT_FLOATFMT, + intfmt=_DEFAULT_INTFMT, + numalign=_DEFAULT_ALIGN, + stralign=_DEFAULT_ALIGN, + missingval=_DEFAULT_MISSINGVAL, + showindex="default", + disable_numparse=False, + colalign=None, + maxcolwidths=None, + rowalign=None, + maxheadercolwidths=None, +): + """Format a fixed width table for pretty printing. + + >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]])) + --- --------- + 1 2.34 + -56 8.999 + 2 10001 + --- --------- + + The first required argument (`tabular_data`) can be a + list-of-lists (or another iterable of iterables), a list of named + tuples, a dictionary of iterables, an iterable of dictionaries, + an iterable of dataclasses (Python 3.7+), a two-dimensional NumPy array, + NumPy record array, or a Pandas' dataframe. + + + Table headers + ------------- + + To print nice column headers, supply the second argument (`headers`): + + - `headers` can be an explicit list of column headers + - if `headers="firstrow"`, then the first row of data is used + - if `headers="keys"`, then dictionary keys or column indices are used + + Otherwise a headerless table is produced. + + If the number of headers is less than the number of columns, they + are supposed to be names of the last columns. This is consistent + with the plain-text format of R and Pandas' dataframes. + + >>> print(tabulate([["sex","age"],["Alice","F",24],["Bob","M",19]], + ... headers="firstrow")) + sex age + ----- ----- ----- + Alice F 24 + Bob M 19 + + By default, pandas.DataFrame data have an additional column called + row index. To add a similar column to all other types of data, + use `showindex="always"` or `showindex=True`. To suppress row indices + for all types of data, pass `showindex="never" or `showindex=False`. + To add a custom row index column, pass `showindex=some_iterable`. + + >>> print(tabulate([["F",24],["M",19]], showindex="always")) + - - -- + 0 F 24 + 1 M 19 + - - -- + + + Column alignment + ---------------- + + `tabulate` tries to detect column types automatically, and aligns + the values properly. By default it aligns decimal points of the + numbers (or flushes integer numbers to the right), and flushes + everything else to the left. Possible column alignments + (`numalign`, `stralign`) are: "right", "center", "left", "decimal" + (only for `numalign`), and None (to disable alignment). + + + Table formats + ------------- + + `intfmt` is a format specification used for columns which + contain numeric data without a decimal point. This can also be + a list or tuple of format strings, one per column. + + `floatfmt` is a format specification used for columns which + contain numeric data with a decimal point. This can also be + a list or tuple of format strings, one per column. + + `None` values are replaced with a `missingval` string (like + `floatfmt`, this can also be a list of values for different + columns): + + >>> print(tabulate([["spam", 1, None], + ... ["eggs", 42, 3.14], + ... ["other", None, 2.7]], missingval="?")) + ----- -- ---- + spam 1 ? + eggs 42 3.14 + other ? 2.7 + ----- -- ---- + + Various plain-text table formats (`tablefmt`) are supported: + 'plain', 'simple', 'grid', 'pipe', 'orgtbl', 'rst', 'mediawiki', + 'latex', 'latex_raw', 'latex_booktabs', 'latex_longtable' and tsv. + Variable `tabulate_formats`contains the list of currently supported formats. + + "plain" format doesn't use any pseudographics to draw tables, + it separates columns with a double space: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "plain")) + strings numbers + spam 41.9999 + eggs 451 + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain")) + spam 41.9999 + eggs 451 + + "simple" format is like Pandoc simple_tables: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "simple")) + strings numbers + --------- --------- + spam 41.9999 + eggs 451 + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple")) + ---- -------- + spam 41.9999 + eggs 451 + ---- -------- + + "grid" is similar to tables produced by Emacs table.el package or + Pandoc grid_tables: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "grid")) + +-----------+-----------+ + | strings | numbers | + +===========+===========+ + | spam | 41.9999 | + +-----------+-----------+ + | eggs | 451 | + +-----------+-----------+ + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid")) + +------+----------+ + | spam | 41.9999 | + +------+----------+ + | eggs | 451 | + +------+----------+ + + "simple_grid" draws a grid using single-line box-drawing + characters: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "simple_grid")) + ┌───────────┬───────────┐ + │ strings │ numbers │ + ├───────────┼───────────┤ + │ spam │ 41.9999 │ + ├───────────┼───────────┤ + │ eggs │ 451 │ + └───────────┴───────────┘ + + "rounded_grid" draws a grid using single-line box-drawing + characters with rounded corners: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "rounded_grid")) + ╭───────────┬───────────╮ + │ strings │ numbers │ + ├───────────┼───────────┤ + │ spam │ 41.9999 │ + ├───────────┼───────────┤ + │ eggs │ 451 │ + ╰───────────┴───────────╯ + + "heavy_grid" draws a grid using bold (thick) single-line box-drawing + characters: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "heavy_grid")) + ┏━━━━━━━━━━━┳━━━━━━━━━━━┓ + ┃ strings ┃ numbers ┃ + ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ + ┃ spam ┃ 41.9999 ┃ + ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ + ┃ eggs ┃ 451 ┃ + ┗━━━━━━━━━━━┻━━━━━━━━━━━┛ + + "mixed_grid" draws a grid using a mix of light (thin) and heavy (thick) lines + box-drawing characters: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "mixed_grid")) + ┍━━━━━━━━━━━┯━━━━━━━━━━━┑ + │ strings │ numbers │ + ┝━━━━━━━━━━━┿━━━━━━━━━━━┥ + │ spam │ 41.9999 │ + ├───────────┼───────────┤ + │ eggs │ 451 │ + ┕━━━━━━━━━━━┷━━━━━━━━━━━┙ + + "double_grid" draws a grid using double-line box-drawing + characters: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "double_grid")) + ╔═══════════╦═══════════╗ + ║ strings ║ numbers ║ + ╠═══════════╬═══════════╣ + ║ spam ║ 41.9999 ║ + ╠═══════════╬═══════════╣ + ║ eggs ║ 451 ║ + ╚═══════════╩═══════════╝ + + "fancy_grid" draws a grid using a mix of single and + double-line box-drawing characters: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "fancy_grid")) + ╒═══════════╤═══════════╕ + │ strings │ numbers │ + ╞═══════════╪═══════════╡ + │ spam │ 41.9999 │ + ├───────────┼───────────┤ + │ eggs │ 451 │ + ╘═══════════╧═══════════╛ + + "outline" is the same as the "grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "outline")) + +-----------+-----------+ + | strings | numbers | + +===========+===========+ + | spam | 41.9999 | + | eggs | 451 | + +-----------+-----------+ + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="outline")) + +------+----------+ + | spam | 41.9999 | + | eggs | 451 | + +------+----------+ + + "simple_outline" is the same as the "simple_grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "simple_outline")) + ┌───────────┬───────────┐ + │ strings │ numbers │ + ├───────────┼───────────┤ + │ spam │ 41.9999 │ + │ eggs │ 451 │ + └───────────┴───────────┘ + + "rounded_outline" is the same as the "rounded_grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "rounded_outline")) + ╭───────────┬───────────╮ + │ strings │ numbers │ + ├───────────┼───────────┤ + │ spam │ 41.9999 │ + │ eggs │ 451 │ + ╰───────────┴───────────╯ + + "heavy_outline" is the same as the "heavy_grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "heavy_outline")) + ┏━━━━━━━━━━━┳━━━━━━━━━━━┓ + ┃ strings ┃ numbers ┃ + ┣━━━━━━━━━━━╋━━━━━━━━━━━┫ + ┃ spam ┃ 41.9999 ┃ + ┃ eggs ┃ 451 ┃ + ┗━━━━━━━━━━━┻━━━━━━━━━━━┛ + + "mixed_outline" is the same as the "mixed_grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "mixed_outline")) + ┍━━━━━━━━━━━┯━━━━━━━━━━━┑ + │ strings │ numbers │ + ┝━━━━━━━━━━━┿━━━━━━━━━━━┥ + │ spam │ 41.9999 │ + │ eggs │ 451 │ + ┕━━━━━━━━━━━┷━━━━━━━━━━━┙ + + "double_outline" is the same as the "double_grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "double_outline")) + ╔═══════════╦═══════════╗ + ║ strings ║ numbers ║ + ╠═══════════╬═══════════╣ + ║ spam ║ 41.9999 ║ + ║ eggs ║ 451 ║ + ╚═══════════╩═══════════╝ + + "fancy_outline" is the same as the "fancy_grid" format but doesn't draw lines between rows: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "fancy_outline")) + ╒═══════════╤═══════════╕ + │ strings │ numbers │ + ╞═══════════╪═══════════╡ + │ spam │ 41.9999 │ + │ eggs │ 451 │ + ╘═══════════╧═══════════╛ + + "pipe" is like tables in PHP Markdown Extra extension or Pandoc + pipe_tables: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "pipe")) + | strings | numbers | + |:----------|----------:| + | spam | 41.9999 | + | eggs | 451 | + + "presto" is like tables produce by the Presto CLI: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "presto")) + strings | numbers + -----------+----------- + spam | 41.9999 + eggs | 451 + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe")) + |:-----|---------:| + | spam | 41.9999 | + | eggs | 451 | + + "orgtbl" is like tables in Emacs org-mode and orgtbl-mode. They + are slightly different from "pipe" format by not using colons to + define column alignment, and using a "+" sign to indicate line + intersections: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "orgtbl")) + | strings | numbers | + |-----------+-----------| + | spam | 41.9999 | + | eggs | 451 | + + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="orgtbl")) + | spam | 41.9999 | + | eggs | 451 | + + "rst" is like a simple table format from reStructuredText; please + note that reStructuredText accepts also "grid" tables: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "rst")) + ========= ========= + strings numbers + ========= ========= + spam 41.9999 + eggs 451 + ========= ========= + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="rst")) + ==== ======== + spam 41.9999 + eggs 451 + ==== ======== + + "mediawiki" produces a table markup used in Wikipedia and on other + MediaWiki-based sites: + + >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], + ... headers="firstrow", tablefmt="mediawiki")) + {| class="wikitable" style="text-align: left;" + |+ + |- + ! strings !! align="right"| numbers + |- + | spam || align="right"| 41.9999 + |- + | eggs || align="right"| 451 + |} + + "html" produces HTML markup as an html.escape'd str + with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML + and a .str property so that the raw HTML remains accessible + the unsafehtml table format can be used if an unescaped HTML format is required: + + >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]], + ... headers="firstrow", tablefmt="html")) + + + + + + + + +
strings numbers
spam 41.9999
eggs 451
+ + "latex" produces a tabular environment of LaTeX document markup: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex")) + \\begin{tabular}{lr} + \\hline + spam & 41.9999 \\\\ + eggs & 451 \\\\ + \\hline + \\end{tabular} + + "latex_raw" is similar to "latex", but doesn't escape special characters, + such as backslash and underscore, so LaTeX commands may embedded into + cells' values: + + >>> print(tabulate([["spam$_9$", 41.9999], ["\\\\emph{eggs}", "451.0"]], tablefmt="latex_raw")) + \\begin{tabular}{lr} + \\hline + spam$_9$ & 41.9999 \\\\ + \\emph{eggs} & 451 \\\\ + \\hline + \\end{tabular} + + "latex_booktabs" produces a tabular environment of LaTeX document markup + using the booktabs.sty package: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_booktabs")) + \\begin{tabular}{lr} + \\toprule + spam & 41.9999 \\\\ + eggs & 451 \\\\ + \\bottomrule + \\end{tabular} + + "latex_longtable" produces a tabular environment that can stretch along + multiple pages, using the longtable package for LaTeX. + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_longtable")) + \\begin{longtable}{lr} + \\hline + spam & 41.9999 \\\\ + eggs & 451 \\\\ + \\hline + \\end{longtable} + + + Number parsing + -------------- + By default, anything which can be parsed as a number is a number. + This ensures numbers represented as strings are aligned properly. + This can lead to weird results for particular strings such as + specific git SHAs e.g. "42992e1" will be parsed into the number + 429920 and aligned as such. + + To completely disable number parsing (and alignment), use + `disable_numparse=True`. For more fine grained control, a list column + indices is used to disable number parsing only on those columns + e.g. `disable_numparse=[0, 2]` would disable number parsing only on the + first and third columns. + + Column Widths and Auto Line Wrapping + ------------------------------------ + Tabulate will, by default, set the width of each column to the length of the + longest element in that column. However, in situations where fields are expected + to reasonably be too long to look good as a single line, tabulate can help automate + word wrapping long fields for you. Use the parameter `maxcolwidth` to provide a + list of maximal column widths + + >>> print(tabulate( \ + [('1', 'John Smith', \ + 'This is a rather long description that might look better if it is wrapped a bit')], \ + headers=("Issue Id", "Author", "Description"), \ + maxcolwidths=[None, None, 30], \ + tablefmt="grid" \ + )) + +------------+------------+-------------------------------+ + | Issue Id | Author | Description | + +============+============+===============================+ + | 1 | John Smith | This is a rather long | + | | | description that might look | + | | | better if it is wrapped a bit | + +------------+------------+-------------------------------+ + + Header column width can be specified in a similar way using `maxheadercolwidth` + + """ + + if tabular_data is None: + tabular_data = [] + + list_of_lists, headers = _normalize_tabular_data( + tabular_data, headers, showindex=showindex + ) + list_of_lists, separating_lines = _remove_separating_lines(list_of_lists) + + if maxcolwidths is not None: + num_cols = len(list_of_lists[0]) + if isinstance(maxcolwidths, int): # Expand scalar for all columns + maxcolwidths = _expand_iterable(maxcolwidths, num_cols, maxcolwidths) + else: # Ignore col width for any 'trailing' columns + maxcolwidths = _expand_iterable(maxcolwidths, num_cols, None) + + numparses = _expand_numparse(disable_numparse, num_cols) + list_of_lists = _wrap_text_to_colwidths( + list_of_lists, maxcolwidths, numparses=numparses + ) + + if maxheadercolwidths is not None: + num_cols = len(list_of_lists[0]) + if isinstance(maxheadercolwidths, int): # Expand scalar for all columns + maxheadercolwidths = _expand_iterable( + maxheadercolwidths, num_cols, maxheadercolwidths + ) + else: # Ignore col width for any 'trailing' columns + maxheadercolwidths = _expand_iterable(maxheadercolwidths, num_cols, None) + + numparses = _expand_numparse(disable_numparse, num_cols) + headers = _wrap_text_to_colwidths( + [headers], maxheadercolwidths, numparses=numparses + )[0] + + # empty values in the first column of RST tables should be escaped (issue #82) + # "" should be escaped as "\\ " or ".." + if tablefmt == "rst": + list_of_lists, headers = _rst_escape_first_column(list_of_lists, headers) + + # PrettyTable formatting does not use any extra padding. + # Numbers are not parsed and are treated the same as strings for alignment. + # Check if pretty is the format being used and override the defaults so it + # does not impact other formats. + min_padding = MIN_PADDING + if tablefmt == "pretty": + min_padding = 0 + disable_numparse = True + numalign = "center" if numalign == _DEFAULT_ALIGN else numalign + stralign = "center" if stralign == _DEFAULT_ALIGN else stralign + else: + numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign + stralign = "left" if stralign == _DEFAULT_ALIGN else stralign + + # optimization: look for ANSI control codes once, + # enable smart width functions only if a control code is found + # + # convert the headers and rows into a single, tab-delimited string ensuring + # that any bytestrings are decoded safely (i.e. errors ignored) + plain_text = "\t".join( + chain( + # headers + map(_to_str, headers), + # rows: chain the rows together into a single iterable after mapping + # the bytestring conversino to each cell value + chain.from_iterable(map(_to_str, row) for row in list_of_lists), + ) + ) + + has_invisible = _ansi_codes.search(plain_text) is not None + + enable_widechars = wcwidth is not None and WIDE_CHARS_MODE + if ( + not isinstance(tablefmt, TableFormat) + and tablefmt in multiline_formats + and _is_multiline(plain_text) + ): + tablefmt = multiline_formats.get(tablefmt, tablefmt) + is_multiline = True + else: + is_multiline = False + width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline) + + # format rows and columns, convert numeric values to strings + cols = list(izip_longest(*list_of_lists)) + numparses = _expand_numparse(disable_numparse, len(cols)) + coltypes = [_column_type(col, numparse=np) for col, np in zip(cols, numparses)] + if isinstance(floatfmt, str): # old version + float_formats = len(cols) * [ + floatfmt + ] # just duplicate the string to use in each column + else: # if floatfmt is list, tuple etc we have one per column + float_formats = list(floatfmt) + if len(float_formats) < len(cols): + float_formats.extend((len(cols) - len(float_formats)) * [_DEFAULT_FLOATFMT]) + if isinstance(intfmt, str): # old version + int_formats = len(cols) * [ + intfmt + ] # just duplicate the string to use in each column + else: # if intfmt is list, tuple etc we have one per column + int_formats = list(intfmt) + if len(int_formats) < len(cols): + int_formats.extend((len(cols) - len(int_formats)) * [_DEFAULT_INTFMT]) + if isinstance(missingval, str): + missing_vals = len(cols) * [missingval] + else: + missing_vals = list(missingval) + if len(missing_vals) < len(cols): + missing_vals.extend((len(cols) - len(missing_vals)) * [_DEFAULT_MISSINGVAL]) + cols = [ + [_format(v, ct, fl_fmt, int_fmt, miss_v, has_invisible) for v in c] + for c, ct, fl_fmt, int_fmt, miss_v in zip( + cols, coltypes, float_formats, int_formats, missing_vals + ) + ] + + # align columns + aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] + if colalign is not None: + assert isinstance(colalign, Iterable) + for idx, align in enumerate(colalign): + aligns[idx] = align + minwidths = ( + [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) + ) + cols = [ + _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) + for c, a, minw in zip(cols, aligns, minwidths) + ] + + if headers: + # align headers and add headers + t_cols = cols or [[""]] * len(headers) + t_aligns = aligns or [stralign] * len(headers) + minwidths = [ + max(minw, max(width_fn(cl) for cl in c)) + for minw, c in zip(minwidths, t_cols) + ] + headers = [ + _align_header(h, a, minw, width_fn(h), is_multiline, width_fn) + for h, a, minw in zip(headers, t_aligns, minwidths) + ] + rows = list(zip(*cols)) + else: + minwidths = [max(width_fn(cl) for cl in c) for c in cols] + rows = list(zip(*cols)) + + if not isinstance(tablefmt, TableFormat): + tablefmt = _table_formats.get(tablefmt, _table_formats["simple"]) + + ra_default = rowalign if isinstance(rowalign, str) else None + rowaligns = _expand_iterable(rowalign, len(rows), ra_default) + _reinsert_separating_lines(rows, separating_lines) + + return _format_table( + tablefmt, headers, rows, minwidths, aligns, is_multiline, rowaligns=rowaligns + ) + + +def _expand_numparse(disable_numparse, column_count): + """ + Return a list of bools of length `column_count` which indicates whether + number parsing should be used on each column. + If `disable_numparse` is a list of indices, each of those indices are False, + and everything else is True. + If `disable_numparse` is a bool, then the returned list is all the same. + """ + if isinstance(disable_numparse, Iterable): + numparses = [True] * column_count + for index in disable_numparse: + numparses[index] = False + return numparses + else: + return [not disable_numparse] * column_count + + +def _expand_iterable(original, num_desired, default): + """ + Expands the `original` argument to return a return a list of + length `num_desired`. If `original` is shorter than `num_desired`, it will + be padded with the value in `default`. + If `original` is not a list to begin with (i.e. scalar value) a list of + length `num_desired` completely populated with `default will be returned + """ + if isinstance(original, Iterable) and not isinstance(original, str): + return original + [default] * (num_desired - len(original)) + else: + return [default] * num_desired + + +def _pad_row(cells, padding): + if cells: + pad = " " * padding + padded_cells = [pad + cell + pad for cell in cells] + return padded_cells + else: + return cells + + +def _build_simple_row(padded_cells, rowfmt): + "Format row according to DataRow format without padding." + begin, sep, end = rowfmt + return (begin + sep.join(padded_cells) + end).rstrip() + + +def _build_row(padded_cells, colwidths, colaligns, rowfmt): + "Return a string which represents a row of data cells." + if not rowfmt: + return None + if hasattr(rowfmt, "__call__"): + return rowfmt(padded_cells, colwidths, colaligns) + else: + return _build_simple_row(padded_cells, rowfmt) + + +def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt, rowalign=None): + # NOTE: rowalign is ignored and exists for api compatibility with _append_multiline_row + lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt)) + return lines + + +def _align_cell_veritically(text_lines, num_lines, column_width, row_alignment): + delta_lines = num_lines - len(text_lines) + blank = [" " * column_width] + if row_alignment == "bottom": + return blank * delta_lines + text_lines + elif row_alignment == "center": + top_delta = delta_lines // 2 + bottom_delta = delta_lines - top_delta + return top_delta * blank + text_lines + bottom_delta * blank + else: + return text_lines + blank * delta_lines + + +def _append_multiline_row( + lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad, rowalign=None +): + colwidths = [w - 2 * pad for w in padded_widths] + cells_lines = [c.splitlines() for c in padded_multiline_cells] + nlines = max(map(len, cells_lines)) # number of lines in the row + # vertically pad cells where some lines are missing + # cells_lines = [ + # (cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths) + # ] + + cells_lines = [ + _align_cell_veritically(cl, nlines, w, rowalign) + for cl, w in zip(cells_lines, colwidths) + ] + lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)] + for ln in lines_cells: + padded_ln = _pad_row(ln, pad) + _append_basic_row(lines, padded_ln, colwidths, colaligns, rowfmt) + return lines + + +def _build_line(colwidths, colaligns, linefmt): + "Return a string which represents a horizontal line." + if not linefmt: + return None + if hasattr(linefmt, "__call__"): + return linefmt(colwidths, colaligns) + else: + begin, fill, sep, end = linefmt + cells = [fill * w for w in colwidths] + return _build_simple_row(cells, (begin, sep, end)) + + +def _append_line(lines, colwidths, colaligns, linefmt): + lines.append(_build_line(colwidths, colaligns, linefmt)) + return lines + + +class JupyterHTMLStr(str): + """Wrap the string with a _repr_html_ method so that Jupyter + displays the HTML table""" + + def _repr_html_(self): + return self + + @property + def str(self): + """add a .str property so that the raw string is still accessible""" + return self + + +def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline, rowaligns): + """Produce a plain-text representation of the table.""" + lines = [] + hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] + pad = fmt.padding + headerrow = fmt.headerrow + + padded_widths = [(w + 2 * pad) for w in colwidths] + if is_multiline: + pad_row = lambda row, _: row # noqa do it later, in _append_multiline_row + append_row = partial(_append_multiline_row, pad=pad) + else: + pad_row = _pad_row + append_row = _append_basic_row + + padded_headers = pad_row(headers, pad) + padded_rows = [pad_row(row, pad) for row in rows] + + if fmt.lineabove and "lineabove" not in hidden: + _append_line(lines, padded_widths, colaligns, fmt.lineabove) + + if padded_headers: + append_row(lines, padded_headers, padded_widths, colaligns, headerrow) + if fmt.linebelowheader and "linebelowheader" not in hidden: + _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) + + if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: + # initial rows with a line below + for row, ralign in zip(padded_rows[:-1], rowaligns): + append_row( + lines, row, padded_widths, colaligns, fmt.datarow, rowalign=ralign + ) + _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) + # the last row without a line below + append_row( + lines, + padded_rows[-1], + padded_widths, + colaligns, + fmt.datarow, + rowalign=rowaligns[-1], + ) + else: + separating_line = ( + fmt.linebetweenrows + or fmt.linebelowheader + or fmt.linebelow + or fmt.lineabove + or Line("", "", "", "") + ) + for row in padded_rows: + # test to see if either the 1st column or the 2nd column (account for showindex) has + # the SEPARATING_LINE flag + if _is_separating_line(row): + _append_line(lines, padded_widths, colaligns, separating_line) + else: + append_row(lines, row, padded_widths, colaligns, fmt.datarow) + + if fmt.linebelow and "linebelow" not in hidden: + _append_line(lines, padded_widths, colaligns, fmt.linebelow) + + if headers or rows: + output = "\n".join(lines) + if fmt.lineabove == _html_begin_table_without_header: + return JupyterHTMLStr(output) + else: + return output + else: # a completely empty table + return "" + + +class _CustomTextWrap(textwrap.TextWrapper): + """A custom implementation of CPython's textwrap.TextWrapper. This supports + both wide characters (Korea, Japanese, Chinese) - including mixed string. + For the most part, the `_handle_long_word` and `_wrap_chunks` functions were + copy pasted out of the CPython baseline, and updated with our custom length + and line appending logic. + """ + + def __init__(self, *args, **kwargs): + self._active_codes = [] + self.max_lines = None # For python2 compatibility + textwrap.TextWrapper.__init__(self, *args, **kwargs) + + @staticmethod + def _len(item): + """Custom len that gets console column width for wide + and non-wide characters as well as ignores color codes""" + stripped = _strip_ansi(item) + if wcwidth: + return wcwidth.wcswidth(stripped) + else: + return len(stripped) + + def _update_lines(self, lines, new_line): + """Adds a new line to the list of lines the text is being wrapped into + This function will also track any ANSI color codes in this string as well + as add any colors from previous lines order to preserve the same formatting + as a single unwrapped string. + """ + code_matches = [x for x in _ansi_codes.finditer(new_line)] + color_codes = [ + code.string[code.span()[0] : code.span()[1]] for code in code_matches + ] + + # Add color codes from earlier in the unwrapped line, and then track any new ones we add. + new_line = "".join(self._active_codes) + new_line + + for code in color_codes: + if code != _ansi_color_reset_code: + self._active_codes.append(code) + else: # A single reset code resets everything + self._active_codes = [] + + # Always ensure each line is color terminted if any colors are + # still active, otherwise colors will bleed into other cells on the console + if len(self._active_codes) > 0: + new_line = new_line + _ansi_color_reset_code + + lines.append(new_line) + + def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): + """_handle_long_word(chunks : [string], + cur_line : [string], + cur_len : int, width : int) + Handle a chunk of text (most likely a word, not whitespace) that + is too long to fit in any line. + """ + # Figure out when indent is larger than the specified width, and make + # sure at least one character is stripped off on every pass + if width < 1: + space_left = 1 + else: + space_left = width - cur_len + + # If we're allowed to break long words, then do so: put as much + # of the next chunk onto the current line as will fit. + if self.break_long_words: + # Tabulate Custom: Build the string up piece-by-piece in order to + # take each charcter's width into account + chunk = reversed_chunks[-1] + i = 1 + while self._len(chunk[:i]) <= space_left: + i = i + 1 + cur_line.append(chunk[: i - 1]) + reversed_chunks[-1] = chunk[i - 1 :] + + # Otherwise, we have to preserve the long word intact. Only add + # it to the current line if there's nothing already there -- + # that minimizes how much we violate the width constraint. + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + # If we're not allowed to break long words, and there's already + # text on the current line, do nothing. Next time through the + # main loop of _wrap_chunks(), we'll wind up here again, but + # cur_len will be zero, so the next line will be entirely + # devoted to the long word that we can't handle right now. + + def _wrap_chunks(self, chunks): + """_wrap_chunks(chunks : [string]) -> [string] + Wrap a sequence of text chunks and return a list of lines of + length 'self.width' or less. (If 'break_long_words' is false, + some lines may be longer than this.) Chunks correspond roughly + to words and the whitespace between them: each chunk is + indivisible (modulo 'break_long_words'), but a line break can + come between any two chunks. Chunks should not have internal + whitespace; ie. a chunk is either all whitespace or a "word". + Whitespace chunks will be removed from the beginning and end of + lines, but apart from that whitespace is preserved. + """ + lines = [] + if self.width <= 0: + raise ValueError("invalid width %r (must be > 0)" % self.width) + if self.max_lines is not None: + if self.max_lines > 1: + indent = self.subsequent_indent + else: + indent = self.initial_indent + if self._len(indent) + self._len(self.placeholder.lstrip()) > self.width: + raise ValueError("placeholder too large for max width") + + # Arrange in reverse order so items can be efficiently popped + # from a stack of chucks. + chunks.reverse() + + while chunks: + + # Start the list of chunks that will make up the current line. + # cur_len is just the length of all the chunks in cur_line. + cur_line = [] + cur_len = 0 + + # Figure out which static string will prefix this line. + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + # Maximum width for this line. + width = self.width - self._len(indent) + + # First chunk on line is whitespace -- drop it, unless this + # is the very beginning of the text (ie. no lines started yet). + if self.drop_whitespace and chunks[-1].strip() == "" and lines: + del chunks[-1] + + while chunks: + chunk_len = self._len(chunks[-1]) + + # Can at least squeeze this chunk onto the current line. + if cur_len + chunk_len <= width: + cur_line.append(chunks.pop()) + cur_len += chunk_len + + # Nope, this line is full. + else: + break + + # The current line is full, and the next chunk is too big to + # fit on *any* line (not just this one). + if chunks and self._len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + cur_len = sum(map(self._len, cur_line)) + + # If the last chunk on this line is all whitespace, drop it. + if self.drop_whitespace and cur_line and cur_line[-1].strip() == "": + cur_len -= self._len(cur_line[-1]) + del cur_line[-1] + + if cur_line: + if ( + self.max_lines is None + or len(lines) + 1 < self.max_lines + or ( + not chunks + or self.drop_whitespace + and len(chunks) == 1 + and not chunks[0].strip() + ) + and cur_len <= width + ): + # Convert current line back to a string and store it in + # list of all lines (return value). + self._update_lines(lines, indent + "".join(cur_line)) + else: + while cur_line: + if ( + cur_line[-1].strip() + and cur_len + self._len(self.placeholder) <= width + ): + cur_line.append(self.placeholder) + self._update_lines(lines, indent + "".join(cur_line)) + break + cur_len -= self._len(cur_line[-1]) + del cur_line[-1] + else: + if lines: + prev_line = lines[-1].rstrip() + if ( + self._len(prev_line) + self._len(self.placeholder) + <= self.width + ): + lines[-1] = prev_line + self.placeholder + break + self._update_lines(lines, indent + self.placeholder.lstrip()) + break + + return lines + + +def _main(): + """\ + Usage: tabulate [options] [FILE ...] + + Pretty-print tabular data. + See also https://github.com/astanin/python-tabulate + + FILE a filename of the file with tabular data; + if "-" or missing, read data from stdin. + + Options: + + -h, --help show this message + -1, --header use the first row of data as a table header + -o FILE, --output FILE print table to FILE (default: stdout) + -s REGEXP, --sep REGEXP use a custom column separator (default: whitespace) + -F FPFMT, --float FPFMT floating point number format (default: g) + -I INTFMT, --int INTFMT integer point number format (default: "") + -f FMT, --format FMT set output table format; supported formats: + plain, simple, grid, fancy_grid, pipe, orgtbl, + rst, mediawiki, html, latex, latex_raw, + latex_booktabs, latex_longtable, tsv + (default: simple) + """ + import getopt + import sys + import textwrap + + usage = textwrap.dedent(_main.__doc__) + try: + opts, args = getopt.getopt( + sys.argv[1:], + "h1o:s:F:A:f:", + ["help", "header", "output", "sep=", "float=", "int=", "align=", "format="], + ) + except getopt.GetoptError as e: + print(e) + print(usage) + sys.exit(2) + headers = [] + floatfmt = _DEFAULT_FLOATFMT + intfmt = _DEFAULT_INTFMT + colalign = None + tablefmt = "simple" + sep = r"\s+" + outfile = "-" + for opt, value in opts: + if opt in ["-1", "--header"]: + headers = "firstrow" + elif opt in ["-o", "--output"]: + outfile = value + elif opt in ["-F", "--float"]: + floatfmt = value + elif opt in ["-I", "--int"]: + intfmt = value + elif opt in ["-C", "--colalign"]: + colalign = value.split() + elif opt in ["-f", "--format"]: + if value not in tabulate_formats: + print("%s is not a supported table format" % value) + print(usage) + sys.exit(3) + tablefmt = value + elif opt in ["-s", "--sep"]: + sep = value + elif opt in ["-h", "--help"]: + print(usage) + sys.exit(0) + files = [sys.stdin] if not args else args + with (sys.stdout if outfile == "-" else open(outfile, "w")) as out: + for f in files: + if f == "-": + f = sys.stdin + if _is_file(f): + _pprint_file( + f, + headers=headers, + tablefmt=tablefmt, + sep=sep, + floatfmt=floatfmt, + intfmt=intfmt, + file=out, + colalign=colalign, + ) + else: + with open(f) as fobj: + _pprint_file( + fobj, + headers=headers, + tablefmt=tablefmt, + sep=sep, + floatfmt=floatfmt, + intfmt=intfmt, + file=out, + colalign=colalign, + ) + + +def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, intfmt, file, colalign): + rows = fobject.readlines() + table = [re.split(sep, r.rstrip()) for r in rows if r.strip()] + print( + tabulate( + table, + headers, + tablefmt, + floatfmt=floatfmt, + intfmt=intfmt, + colalign=colalign, + ), + file=file, + ) + + +if __name__ == "__main__": + _main() diff --git a/tabulate/tabulate/__pycache__/__init__.cpython-312.pyc b/tabulate/tabulate/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..6c3b1c29 Binary files /dev/null and b/tabulate/tabulate/__pycache__/__init__.cpython-312.pyc differ diff --git a/tabulate/tabulate/__pycache__/__init__.cpython-313.pyc b/tabulate/tabulate/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 00000000..3149601c Binary files /dev/null and b/tabulate/tabulate/__pycache__/__init__.cpython-313.pyc differ diff --git a/tabulate/tabulate/__pycache__/version.cpython-312.pyc b/tabulate/tabulate/__pycache__/version.cpython-312.pyc new file mode 100644 index 00000000..dce985d7 Binary files /dev/null and b/tabulate/tabulate/__pycache__/version.cpython-312.pyc differ diff --git a/tabulate/tabulate/__pycache__/version.cpython-313.pyc b/tabulate/tabulate/__pycache__/version.cpython-313.pyc new file mode 100644 index 00000000..930b5656 Binary files /dev/null and b/tabulate/tabulate/__pycache__/version.cpython-313.pyc differ diff --git a/tabulate/tabulate/version.py b/tabulate/tabulate/version.py new file mode 100644 index 00000000..1dd234ca --- /dev/null +++ b/tabulate/tabulate/version.py @@ -0,0 +1,5 @@ +# coding: utf-8 +# file generated by setuptools_scm +# don't change, don't track in version control +__version__ = version = '0.9.0' +__version_tuple__ = version_tuple = (0, 9, 0)