summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2018-05-15 18:12:01 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2018-05-15 18:12:01 +0000
commit6541e1199fe2a2c073abf0db96bf59935ef3d685 (patch)
treef16dc0d59c25a917887a67873fa5dde35adb9f80
parentInitial commit. (diff)
downloadpython-tabulate-6541e1199fe2a2c073abf0db96bf59935ef3d685.zip
python-tabulate-6541e1199fe2a2c073abf0db96bf59935ef3d685.tar.xz
Adding upstream version 0.8.2.upstream/0.8.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--LICENSE20
-rw-r--r--MANIFEST.in5
-rw-r--r--PKG-INFO763
-rw-r--r--README1
-rw-r--r--README.rst750
-rw-r--r--benchmark.py118
-rw-r--r--setup.cfg4
-rw-r--r--setup.py61
-rw-r--r--tabulate.egg-info/PKG-INFO763
-rw-r--r--tabulate.egg-info/SOURCES.txt20
-rw-r--r--tabulate.egg-info/dependency_links.txt1
-rw-r--r--tabulate.egg-info/entry_points.txt3
-rw-r--r--tabulate.egg-info/requires.txt3
-rw-r--r--tabulate.egg-info/top_level.txt1
-rw-r--r--tabulate.py1514
-rw-r--r--test/common.py46
-rw-r--r--test/test_api.py58
-rw-r--r--test/test_cli.py190
-rw-r--r--test/test_input.py450
-rw-r--r--test/test_internal.py50
-rw-r--r--test/test_output.py1192
-rw-r--r--test/test_regression.py333
22 files changed, 6346 insertions, 0 deletions
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a94ae8b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2011-2017 Sergey Astanin
+
+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/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..bb21d49
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,5 @@
+include LICENSE
+include README
+include README.rst
+include test/common.py
+include benchmark.py
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..bf60834
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,763 @@
+Metadata-Version: 1.1
+Name: tabulate
+Version: 0.8.2
+Summary: Pretty-print tabular data
+Home-page: https://bitbucket.org/astanin/python-tabulate
+Author: Sergey Astanin
+Author-email: s.astanin@gmail.com
+License: MIT
+Description: ===============
+ 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::
+
+ 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:\Python27\Scripts\tabulate.exe``).
+
+ You may consider installing the library only for the current user::
+
+ 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::
+
+ TABULATE_INSTALL=lib-only pip install tabulate
+
+ On Windows::
+
+ set TABULATE_INSTALL=lib-only
+ pip install tabulate
+
+
+ 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::
+
+ >>> 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)
+ * two-dimensional NumPy array
+ * NumPy record arrays (names as columns)
+ * pandas.DataFrame
+
+ Examples in this file use Python2. Tabulate supports Python3 too.
+
+
+ Headers
+ ~~~~~~~
+
+ The second optional argument named ``headers`` defines a list of
+ column headers to be used::
+
+ >>> 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::
+
+ >>> 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::
+
+ >>> 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::
+
+ >>> 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"
+ - "grid"
+ - "fancy_grid"
+ - "pipe"
+ - "orgtbl"
+ - "jira"
+ - "presto"
+ - "psql"
+ - "rst"
+ - "mediawiki"
+ - "moinmoin"
+ - "youtrack"
+ - "html"
+ - "latex"
+ - "latex_raw"
+ - "latex_booktabs"
+ - "textile"
+
+ ``plain`` tables do not use any pseudo-graphics to draw lines::
+
+ >>> 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`::
+
+ >>> print tabulate(table, headers, tablefmt="simple")
+ item qty
+ ------ -----
+ spam 42
+ eggs 451
+ bacon 0
+
+ ``grid`` is like tables formatted by Emacs' `table.el`
+ package. It corresponds to ``grid_tables`` in Pandoc Markdown
+ extensions::
+
+ >>> print tabulate(table, headers, tablefmt="grid")
+ +--------+-------+
+ | item | qty |
+ +========+=======+
+ | spam | 42 |
+ +--------+-------+
+ | eggs | 451 |
+ +--------+-------+
+ | bacon | 0 |
+ +--------+-------+
+
+ ``fancy_grid`` draws a grid using box-drawing characters::
+
+ >>> print tabulate(table, headers, tablefmt="fancy_grid")
+ ╒════════╤═══════╕
+ │ item │ qty │
+ ╞════════╪═══════╡
+ │ spam │ 42 │
+ ├────────┼───────┤
+ │ eggs │ 451 │
+ ├────────┼───────┤
+ │ bacon │ 0 │
+ ╘════════╧═══════╛
+
+ ``presto`` is like tables formatted by Presto cli::
+
+ >>> print tabulate.tabulate()
+ item | qty
+ --------+-------
+ spam | 42
+ eggs | 451
+ bacon | 0
+
+ ``psql`` is like tables formatted by Postgres' psql cli::
+
+ >>> print tabulate.tabulate()
+ +--------+-------+
+ | item | qty |
+ |--------+-------|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+ +--------+-------+
+
+ ``pipe`` follows the conventions of `PHP Markdown Extra` extension. It
+ corresponds to ``pipe_tables`` in Pandoc. This format uses colons to
+ indicate column alignment::
+
+ >>> print tabulate(table, headers, tablefmt="pipe")
+ | item | qty |
+ |:-------|------:|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+ ``orgtbl`` follows the conventions of Emacs `org-mode`, and is editable
+ also in the minor `orgtbl-mode`. Hence its name::
+
+ >>> print tabulate(table, headers, tablefmt="orgtbl")
+ | item | qty |
+ |--------+-------|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+ ``jira`` follows the conventions of Atlassian Jira markup language::
+
+ >>> print tabulate(table, headers, tablefmt="jira")
+ || item || qty ||
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+ ``rst`` formats data like a simple table of the `reStructuredText` format::
+
+ >>> print tabulate(table, headers, tablefmt="rst")
+ ====== =====
+ item qty
+ ====== =====
+ spam 42
+ eggs 451
+ bacon 0
+ ====== =====
+
+ ``mediawiki`` format produces a table markup used in `Wikipedia` and on
+ other MediaWiki-based sites::
+
+ >>> print tabulate(table, headers, tablefmt="mediawiki")
+ {| class="wikitable" style="text-align: left;"
+ |+ <!-- caption -->
+ |-
+ ! 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`
+ wikis::
+
+ >>> print tabulate(d,headers,tablefmt="moinmoin")
+ || ''' item ''' || ''' quantity ''' ||
+ || spam || 41.999 ||
+ || eggs || 451 ||
+ || bacon || ||
+
+ ``youtrack`` format produces a table markup used in Youtrack
+ tickets::
+
+ >>> print tabulate(d,headers,tablefmt="youtrack")
+ || item || quantity ||
+ | spam | 41.999 |
+ | eggs | 451 |
+ | bacon | |
+
+ ``textile`` format produces a table markup used in `Textile` format::
+
+ >>> print tabulate(table, headers, tablefmt='textile')
+ |_. item |_. qty |
+ |<. spam |>. 42 |
+ |<. eggs |>. 451 |
+ |<. bacon |>. 0 |
+
+ ``html`` produces standard HTML markup::
+
+ >>> print tabulate(table, headers, tablefmt="html")
+ <table>
+ <tbody>
+ <tr><th>item </th><th style="text-align: right;"> qty</th></tr>
+ <tr><td>spam </td><td style="text-align: right;"> 42</td></tr>
+ <tr><td>eggs </td><td style="text-align: right;"> 451</td></tr>
+ <tr><td>bacon </td><td style="text-align: right;"> 0</td></tr>
+ </tbody>
+ </table>
+
+ ``latex`` format creates a ``tabular`` environment for LaTeX markup,
+ replacing special characters like ```` or ``\`` to their LaTeX
+ correspondents::
+
+ >>> 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.
+
+
+ .. _Pandoc Markdown extensions: http://johnmacfarlane.net/pandoc/README.html#tables
+ .. _PHP Markdown Extra: http://michelf.ca/projects/php-markdown/extra/#table
+ .. _table.el: http://table.sourceforge.net/
+ .. _org-mode: http://orgmode.org/manual/Tables.html
+ .. _reStructuredText: http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables
+ .. _Textile: http://redcloth.org/hobix.com/textile/
+ .. _Wikipedia: http://www.mediawiki.org/wiki/Help:Tables
+ .. _MoinMoin: https://moinmo.in/
+
+
+ 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::
+
+ >>> 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::
+
+ >>> 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:
+
+ ::
+
+ >>> 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
+ ---- ----
+
+
+
+ Number formatting
+ ~~~~~~~~~~~~~~~~~
+
+ ``tabulate`` allows to define custom number formatting applied to all
+ columns of decimal numbers. Use ``floatfmt`` named argument::
+
+ >>> 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::
+
+ >>> print tabulate([[0.12345, 0.12345, 0.12345]], floatfmt=(".1f", ".3f"))
+ --- ----- -------
+ 0.1 0.123 0.12345
+ --- ----- -------
+
+
+
+ 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``::
+
+ 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``::
+
+ 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``::
+
+ 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::
+
+ >>> table = [["eggs",451],["more\nspam",42]]
+ >>> headers = ["item\nname", "qty"]
+
+ ``plain`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="plain"))
+ item qty
+ name
+ eggs 451
+ more 42
+ spam
+
+ ``simple`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="simple"))
+ item qty
+ name
+ ------ -----
+ eggs 451
+ more 42
+ spam
+
+ ``grid`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="grid"))
+ +--------+-------+
+ | item | qty |
+ | name | |
+ +========+=======+
+ | eggs | 451 |
+ +--------+-------+
+ | more | 42 |
+ | spam | |
+ +--------+-------+
+
+ ``fancy_grid`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="fancy_grid"))
+ ╒════════╤═══════╕
+ │ item │ qty │
+ │ name │ │
+ ╞════════╪═══════╡
+ │ eggs │ 451 │
+ ├────────┼───────┤
+ │ more │ 42 │
+ │ spam │ │
+ ╘════════╧═══════╛
+
+ ``pipe`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="pipe"))
+ | item | qty |
+ | name | |
+ |:-------|------:|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+ ``orgtbl`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="orgtbl"))
+ | item | qty |
+ | name | |
+ |--------+-------|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+ ``jira`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="jira"))
+ | item | qty |
+ | name | |
+ |:-------|------:|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+ ``presto`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="presto"))
+ item | qty
+ name |
+ --------+-------
+ eggs | 451
+ more | 42
+ spam |
+
+ ``psql`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="psql"))
+ +--------+-------+
+ | item | qty |
+ | name | |
+ |--------+-------|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+ +--------+-------+
+
+ ``rst`` tables::
+
+ >>> 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.
+
+
+ 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)
+ -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, 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, coma or other
+ separator.
+
+ In 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.5.2 on Windows
+
+ ::
+
+ ================================= ========== ===========
+ Table formatter time, μs rel. time
+ ================================= ========== ===========
+ csv to StringIO 14.5 1.0
+ join with tabs and newlines 20.3 1.4
+ asciitable (0.8.0) 355.1 24.5
+ tabulate (0.8.2) 830.3 57.3
+ tabulate (0.8.2, WIDE_CHARS_MODE) 1483.4 102.4
+ PrettyTable (0.7.2) 1611.9 111.2
+ texttable (0.8.8) 1916.5 132.3
+ ================================= ========== ===========
+
+
+ Version history
+ ---------------
+
+ - 0.8.2: Bug fixes.
+ - 0.8.1: Multiline data in several output formats.
+ New ``latex_raw`` format.
+ Column-specific floating point formatting.
+ Python 3.5 & 3.6 support. Drop support for Python 2.6, 3.2, 3.3 (should still work).
+ - 0.7.7: Identical to 0.7.6, resolving some PyPI issues.
+ - 0.7.6: Bug fixes. New table formats (``psql``, ``jira``, ``moinmoin``, ``textile``).
+ Wide character support. Printing from database cursors.
+ Option to print row indices. Boolean columns. Ragged rows.
+ Option to disable number parsing.
+ - 0.7.5: Bug fixes. ``--float`` format option for the command line utility.
+ - 0.7.4: Bug fixes. ``fancy_grid`` and ``html`` formats. Command line utility.
+ - 0.7.3: Bug fixes. Python 3.4 support. Iterables of dicts. ``latex_booktabs`` format.
+ - 0.7.2: Python 3.2 support.
+ - 0.7.1: Bug fixes. ``tsv`` format. Column alignment can be disabled.
+ - 0.7: ``latex`` tables. Printing lists of named tuples and NumPy
+ record arrays. Fix printing date and time values. Python <= 2.6.4 is supported.
+ - 0.6: ``mediawiki`` tables, bug fixes.
+ - 0.5.1: Fix README.rst formatting. Optimize (performance similar to 0.4.4).
+ - 0.5: ANSI color sequences. Printing dicts of iterables and Pandas' dataframes.
+ - 0.4.4: Python 2.6 support.
+ - 0.4.3: Bug fix, None as a missing value.
+ - 0.4.2: Fix manifest file.
+ - 0.4.1: Update license and documentation.
+ - 0.4: Unicode support, Python3 support, ``rst`` tables.
+ - 0.3: Initial PyPI release. Table formats: ``simple``, ``plain``,
+ ``grid``, ``pipe``, and ``orgtbl``.
+
+
+ How to contribute
+ -----------------
+
+ Contributions should include tests and an explanation for the changes they
+ propose. Documentation (examples, docstrings, README.rst) should be updated
+ accordingly.
+
+ This project uses `nose` testing framework and `tox` 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, ``nose`` and ``tox`` are installed, then run ``tox`` in
+ the root of the project source tree.
+
+ On Linux ``tox`` expects to find executables like ``python2.6``,
+ ``python2.7``, ``python3.4`` etc. On Windows it looks for
+ ``C:\Python26\python.exe``, ``C:\Python27\python.exe`` and
+ ``C:\Python34\python.exe`` respectively.
+
+ To test only some Python environements, use ``-e`` option. For
+ example, to test only against Python 2.7 and Python 3.4, run::
+
+ tox -e py27,py34
+
+ in the root of the project source tree.
+
+ To enable NumPy and Pandas tests, run::
+
+ tox -e py27-extra,py34-extra
+
+ (this may take a long time the first time, because NumPy and Pandas
+ will have to be installed in the new virtual environments)
+
+ See ``tox.ini`` file to learn how to use ``nosetests`` directly to
+ test individual Python versions.
+
+ .. _nose: https://nose.readthedocs.org/
+ .. _tox: https://tox.readthedocs.io/
+
+
+ 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.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Software Development :: Libraries
diff --git a/README b/README
new file mode 100644
index 0000000..facdd59
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+./README.rst \ No newline at end of file
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..b44c9a3
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,750 @@
+===============
+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::
+
+ 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:\Python27\Scripts\tabulate.exe``).
+
+You may consider installing the library only for the current user::
+
+ 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::
+
+ TABULATE_INSTALL=lib-only pip install tabulate
+
+On Windows::
+
+ set TABULATE_INSTALL=lib-only
+ pip install tabulate
+
+
+Build status
+------------
+
+.. image:: https://circleci.com/bb/astanin/python-tabulate.svg?style=svg
+ :alt: Build status
+ :target: https://circleci.com/bb/astanin/python-tabulate/tree/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::
+
+ >>> 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)
+* two-dimensional NumPy array
+* NumPy record arrays (names as columns)
+* pandas.DataFrame
+
+Examples in this file use Python2. Tabulate supports Python3 too.
+
+
+Headers
+~~~~~~~
+
+The second optional argument named ``headers`` defines a list of
+column headers to be used::
+
+ >>> 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::
+
+ >>> 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::
+
+ >>> 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::
+
+ >>> 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"
+- "grid"
+- "fancy_grid"
+- "pipe"
+- "orgtbl"
+- "jira"
+- "presto"
+- "psql"
+- "rst"
+- "mediawiki"
+- "moinmoin"
+- "youtrack"
+- "html"
+- "latex"
+- "latex_raw"
+- "latex_booktabs"
+- "textile"
+
+``plain`` tables do not use any pseudo-graphics to draw lines::
+
+ >>> 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`_::
+
+ >>> print tabulate(table, headers, tablefmt="simple")
+ item qty
+ ------ -----
+ spam 42
+ eggs 451
+ bacon 0
+
+``grid`` is like tables formatted by Emacs' `table.el`_
+package. It corresponds to ``grid_tables`` in Pandoc Markdown
+extensions::
+
+ >>> print tabulate(table, headers, tablefmt="grid")
+ +--------+-------+
+ | item | qty |
+ +========+=======+
+ | spam | 42 |
+ +--------+-------+
+ | eggs | 451 |
+ +--------+-------+
+ | bacon | 0 |
+ +--------+-------+
+
+``fancy_grid`` draws a grid using box-drawing characters::
+
+ >>> print tabulate(table, headers, tablefmt="fancy_grid")
+ ╒════════╤═══════╕
+ │ item │ qty │
+ ╞════════╪═══════╡
+ │ spam │ 42 │
+ ├────────┼───────┤
+ │ eggs │ 451 │
+ ├────────┼───────┤
+ │ bacon │ 0 │
+ ╘════════╧═══════╛
+
+``presto`` is like tables formatted by Presto cli::
+
+ >>> print tabulate.tabulate()
+ item | qty
+ --------+-------
+ spam | 42
+ eggs | 451
+ bacon | 0
+
+``psql`` is like tables formatted by Postgres' psql cli::
+
+ >>> print tabulate.tabulate()
+ +--------+-------+
+ | item | qty |
+ |--------+-------|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+ +--------+-------+
+
+``pipe`` follows the conventions of `PHP Markdown Extra`_ extension. It
+corresponds to ``pipe_tables`` in Pandoc. This format uses colons to
+indicate column alignment::
+
+ >>> print tabulate(table, headers, tablefmt="pipe")
+ | item | qty |
+ |:-------|------:|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+``orgtbl`` follows the conventions of Emacs `org-mode`_, and is editable
+also in the minor `orgtbl-mode`. Hence its name::
+
+ >>> print tabulate(table, headers, tablefmt="orgtbl")
+ | item | qty |
+ |--------+-------|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+``jira`` follows the conventions of Atlassian Jira markup language::
+
+ >>> print tabulate(table, headers, tablefmt="jira")
+ || item || qty ||
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+``rst`` formats data like a simple table of the `reStructuredText`_ format::
+
+ >>> print tabulate(table, headers, tablefmt="rst")
+ ====== =====
+ item qty
+ ====== =====
+ spam 42
+ eggs 451
+ bacon 0
+ ====== =====
+
+``mediawiki`` format produces a table markup used in `Wikipedia`_ and on
+other MediaWiki-based sites::
+
+ >>> print tabulate(table, headers, tablefmt="mediawiki")
+ {| class="wikitable" style="text-align: left;"
+ |+ <!-- caption -->
+ |-
+ ! 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`_
+wikis::
+
+ >>> print tabulate(d,headers,tablefmt="moinmoin")
+ || ''' item ''' || ''' quantity ''' ||
+ || spam || 41.999 ||
+ || eggs || 451 ||
+ || bacon || ||
+
+``youtrack`` format produces a table markup used in Youtrack
+tickets::
+
+ >>> print tabulate(d,headers,tablefmt="youtrack")
+ || item || quantity ||
+ | spam | 41.999 |
+ | eggs | 451 |
+ | bacon | |
+
+``textile`` format produces a table markup used in `Textile`_ format::
+
+ >>> print tabulate(table, headers, tablefmt='textile')
+ |_. item |_. qty |
+ |<. spam |>. 42 |
+ |<. eggs |>. 451 |
+ |<. bacon |>. 0 |
+
+``html`` produces standard HTML markup::
+
+ >>> print tabulate(table, headers, tablefmt="html")
+ <table>
+ <tbody>
+ <tr><th>item </th><th style="text-align: right;"> qty</th></tr>
+ <tr><td>spam </td><td style="text-align: right;"> 42</td></tr>
+ <tr><td>eggs </td><td style="text-align: right;"> 451</td></tr>
+ <tr><td>bacon </td><td style="text-align: right;"> 0</td></tr>
+ </tbody>
+ </table>
+
+``latex`` format creates a ``tabular`` environment for LaTeX markup,
+replacing special characters like ``_`` or ``\`` to their LaTeX
+correspondents::
+
+ >>> 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.
+
+
+.. _Pandoc Markdown extensions: http://johnmacfarlane.net/pandoc/README.html#tables
+.. _PHP Markdown Extra: http://michelf.ca/projects/php-markdown/extra/#table
+.. _table.el: http://table.sourceforge.net/
+.. _org-mode: http://orgmode.org/manual/Tables.html
+.. _reStructuredText: http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables
+.. _Textile: http://redcloth.org/hobix.com/textile/
+.. _Wikipedia: http://www.mediawiki.org/wiki/Help:Tables
+.. _MoinMoin: https://moinmo.in/
+
+
+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::
+
+ >>> 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::
+
+ >>> 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:
+
+::
+
+ >>> 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
+ ---- ----
+
+
+
+Number formatting
+~~~~~~~~~~~~~~~~~
+
+``tabulate`` allows to define custom number formatting applied to all
+columns of decimal numbers. Use ``floatfmt`` named argument::
+
+ >>> 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::
+
+ >>> print tabulate([[0.12345, 0.12345, 0.12345]], floatfmt=(".1f", ".3f"))
+ --- ----- -------
+ 0.1 0.123 0.12345
+ --- ----- -------
+
+
+
+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``::
+
+ 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``::
+
+ 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``::
+
+ 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::
+
+ >>> table = [["eggs",451],["more\nspam",42]]
+ >>> headers = ["item\nname", "qty"]
+
+``plain`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="plain"))
+ item qty
+ name
+ eggs 451
+ more 42
+ spam
+
+``simple`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="simple"))
+ item qty
+ name
+ ------ -----
+ eggs 451
+ more 42
+ spam
+
+``grid`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="grid"))
+ +--------+-------+
+ | item | qty |
+ | name | |
+ +========+=======+
+ | eggs | 451 |
+ +--------+-------+
+ | more | 42 |
+ | spam | |
+ +--------+-------+
+
+``fancy_grid`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="fancy_grid"))
+ ╒════════╤═══════╕
+ │ item │ qty │
+ │ name │ │
+ ╞════════╪═══════╡
+ │ eggs │ 451 │
+ ├────────┼───────┤
+ │ more │ 42 │
+ │ spam │ │
+ ╘════════╧═══════╛
+
+``pipe`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="pipe"))
+ | item | qty |
+ | name | |
+ |:-------|------:|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+``orgtbl`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="orgtbl"))
+ | item | qty |
+ | name | |
+ |--------+-------|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+``jira`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="jira"))
+ | item | qty |
+ | name | |
+ |:-------|------:|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+``presto`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="presto"))
+ item | qty
+ name |
+ --------+-------
+ eggs | 451
+ more | 42
+ spam |
+
+``psql`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="psql"))
+ +--------+-------+
+ | item | qty |
+ | name | |
+ |--------+-------|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+ +--------+-------+
+
+``rst`` tables::
+
+ >>> 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.
+
+
+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)
+ -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, 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, coma or other
+separator.
+
+In 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.5.2 on Windows
+
+::
+
+ ================================= ========== ===========
+ Table formatter time, μs rel. time
+ ================================= ========== ===========
+ csv to StringIO 14.5 1.0
+ join with tabs and newlines 20.3 1.4
+ asciitable (0.8.0) 355.1 24.5
+ tabulate (0.8.2) 830.3 57.3
+ tabulate (0.8.2, WIDE_CHARS_MODE) 1483.4 102.4
+ PrettyTable (0.7.2) 1611.9 111.2
+ texttable (0.8.8) 1916.5 132.3
+ ================================= ========== ===========
+
+
+Version history
+---------------
+
+- 0.8.2: Bug fixes.
+- 0.8.1: Multiline data in several output formats.
+ New ``latex_raw`` format.
+ Column-specific floating point formatting.
+ Python 3.5 & 3.6 support. Drop support for Python 2.6, 3.2, 3.3 (should still work).
+- 0.7.7: Identical to 0.7.6, resolving some PyPI issues.
+- 0.7.6: Bug fixes. New table formats (``psql``, ``jira``, ``moinmoin``, ``textile``).
+ Wide character support. Printing from database cursors.
+ Option to print row indices. Boolean columns. Ragged rows.
+ Option to disable number parsing.
+- 0.7.5: Bug fixes. ``--float`` format option for the command line utility.
+- 0.7.4: Bug fixes. ``fancy_grid`` and ``html`` formats. Command line utility.
+- 0.7.3: Bug fixes. Python 3.4 support. Iterables of dicts. ``latex_booktabs`` format.
+- 0.7.2: Python 3.2 support.
+- 0.7.1: Bug fixes. ``tsv`` format. Column alignment can be disabled.
+- 0.7: ``latex`` tables. Printing lists of named tuples and NumPy
+ record arrays. Fix printing date and time values. Python <= 2.6.4 is supported.
+- 0.6: ``mediawiki`` tables, bug fixes.
+- 0.5.1: Fix README.rst formatting. Optimize (performance similar to 0.4.4).
+- 0.5: ANSI color sequences. Printing dicts of iterables and Pandas' dataframes.
+- 0.4.4: Python 2.6 support.
+- 0.4.3: Bug fix, None as a missing value.
+- 0.4.2: Fix manifest file.
+- 0.4.1: Update license and documentation.
+- 0.4: Unicode support, Python3 support, ``rst`` tables.
+- 0.3: Initial PyPI release. Table formats: ``simple``, ``plain``,
+ ``grid``, ``pipe``, and ``orgtbl``.
+
+
+How to contribute
+-----------------
+
+Contributions should include tests and an explanation for the changes they
+propose. Documentation (examples, docstrings, README.rst) should be updated
+accordingly.
+
+This project uses `nose`_ testing framework and `tox`_ 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, ``nose`` and ``tox`` are installed, then run ``tox`` in
+the root of the project source tree.
+
+On Linux ``tox`` expects to find executables like ``python2.6``,
+``python2.7``, ``python3.4`` etc. On Windows it looks for
+``C:\Python26\python.exe``, ``C:\Python27\python.exe`` and
+``C:\Python34\python.exe`` respectively.
+
+To test only some Python environements, use ``-e`` option. For
+example, to test only against Python 2.7 and Python 3.4, run::
+
+ tox -e py27,py34
+
+in the root of the project source tree.
+
+To enable NumPy and Pandas tests, run::
+
+ tox -e py27-extra,py34-extra
+
+(this may take a long time the first time, because NumPy and Pandas
+will have to be installed in the new virtual environments)
+
+See ``tox.ini`` file to learn how to use ``nosetests`` directly to
+test individual Python versions.
+
+.. _nose: https://nose.readthedocs.org/
+.. _tox: https://tox.readthedocs.io/
+
+
+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.
diff --git a/benchmark.py b/benchmark.py
new file mode 100644
index 0000000..b50e4ff
--- /dev/null
+++ b/benchmark.py
@@ -0,0 +1,118 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from __future__ import print_function
+from timeit import timeit
+import tabulate
+import asciitable
+import prettytable
+import texttable
+import sys
+import codecs
+
+setup_code = r"""
+from csv import writer
+try: # Python 2
+ from StringIO import StringIO
+except: # Python 3
+ from io import StringIO
+import tabulate
+import asciitable
+import prettytable
+import texttable
+
+
+import platform
+if platform.platform().startswith("Windows") \
+ and \
+ platform.python_version_tuple() < ('3','6','0'):
+ import win_unicode_console
+ win_unicode_console.enable()
+
+
+table=[["some text"]+list(range(i,i+9)) for i in range(10)]
+
+
+def csv_table(table):
+ buf = StringIO()
+ writer(buf).writerows(table)
+ return buf.getvalue()
+
+
+def join_table(table):
+ return "\n".join(("\t".join(map(str,row)) for row in table))
+
+
+def run_prettytable(table):
+ pp = prettytable.PrettyTable()
+ for row in table:
+ pp.add_row(row)
+ return str(pp)
+
+
+def run_asciitable(table):
+ buf = StringIO()
+ asciitable.write(table, output=buf, Writer=asciitable.FixedWidth)
+ return buf.getvalue()
+
+
+def run_texttable(table):
+ pp = texttable.Texttable()
+ pp.set_cols_align(["l"] + ["r"]*9)
+ pp.add_rows(table)
+ return pp.draw()
+
+
+def run_tabletext(table):
+ return tabletext.to_text(table)
+
+
+def run_tabulate(table, widechars=False):
+ tabulate.WIDE_CHARS_MODE = tabulate.wcwidth is not None and widechars
+ return tabulate.tabulate(table)
+
+
+"""
+
+methods = [(u"join with tabs and newlines", "join_table(table)"),
+ (u"csv to StringIO", "csv_table(table)"),
+ (u"asciitable (%s)" % asciitable.__version__, "run_asciitable(table)"),
+ (u"tabulate (%s)" % tabulate.__version__, "run_tabulate(table)"),
+ (u"tabulate (%s, WIDE_CHARS_MODE)" % tabulate.__version__, "run_tabulate(table, widechars=True)"),
+ (u"PrettyTable (%s)" % prettytable.__version__, "run_prettytable(table)"),
+ (u"texttable (%s)" % texttable.__version__, "run_texttable(table)"),
+ ]
+
+
+if tabulate.wcwidth is None:
+ del(methods[4])
+
+
+def benchmark(n):
+ global methods
+ if '--onlyself' in sys.argv[1:]:
+ methods = [ m for m in methods if m[0].startswith("tabulate") ]
+ else:
+ methods = methods
+
+ results = [(desc, timeit(code, setup_code, number=n)/n * 1e6)
+ for desc, code in methods]
+ mintime = min(map(lambda x: x[1], results))
+ results = [(desc, t, t/mintime) for desc, t in
+ sorted(results, key=lambda x: x[1])]
+ table = tabulate.tabulate(results,
+ [u"Table formatter", u"time, μs", u"rel. time"],
+ u"rst", floatfmt=".1f")
+
+ import platform
+ if platform.platform().startswith("Windows"):
+ print(table)
+ else:
+ print(codecs.encode(table, "utf-8"))
+
+
+if __name__ == "__main__":
+ if sys.argv[1:]:
+ n = int(sys.argv[1])
+ else:
+ n = 10000
+ benchmark(n)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..4927abe
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,4 @@
+[egg_info]
+tag_build =
+tag_date = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..077fded
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,61 @@
+#!/usr/bin/env python
+
+try:
+ from setuptools import setup
+except ImportError:
+ from distutils.core import setup
+
+
+from platform import python_version_tuple, python_implementation
+import os
+import re
+
+# strip links from the descripton on the PyPI
+if python_version_tuple()[0] >= '3':
+ LONG_DESCRIPTION = open("README.rst", "r", encoding="utf-8").read().replace("`_", "`")
+else:
+ LONG_DESCRIPTION = open("README.rst", "r").read().replace("`_", "`")
+
+# strip Build Status from the PyPI package
+try:
+ if python_version_tuple()[:2] >= ('2', '7'):
+ status_re = "^Build status\n(.*\n){7}"
+ LONG_DESCRIPTION = re.sub(status_re, "", LONG_DESCRIPTION, flags=re.M)
+except TypeError:
+ if python_implementation() == "IronPython":
+ # IronPython doesn't support flags in re.sub (IronPython issue #923)
+ pass
+ else:
+ raise
+
+install_options = os.environ.get("TABULATE_INSTALL","").split(",")
+libonly_flags = set(["lib-only", "libonly", "no-cli", "without-cli"])
+if libonly_flags.intersection(install_options):
+ console_scripts = []
+else:
+ console_scripts = ['tabulate = tabulate:_main']
+
+
+setup(name='tabulate',
+ version='0.8.2',
+ description='Pretty-print tabular data',
+ long_description=LONG_DESCRIPTION,
+ author='Sergey Astanin',
+ author_email='s.astanin@gmail.com',
+ url='https://bitbucket.org/astanin/python-tabulate',
+ license='MIT',
+ classifiers= [ "Development Status :: 4 - Beta",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Topic :: Software Development :: Libraries" ],
+ py_modules = ['tabulate'],
+ entry_points = {'console_scripts': console_scripts},
+ extras_require = {'widechars': ['wcwidth']},
+ test_suite = 'nose.collector')
diff --git a/tabulate.egg-info/PKG-INFO b/tabulate.egg-info/PKG-INFO
new file mode 100644
index 0000000..bf60834
--- /dev/null
+++ b/tabulate.egg-info/PKG-INFO
@@ -0,0 +1,763 @@
+Metadata-Version: 1.1
+Name: tabulate
+Version: 0.8.2
+Summary: Pretty-print tabular data
+Home-page: https://bitbucket.org/astanin/python-tabulate
+Author: Sergey Astanin
+Author-email: s.astanin@gmail.com
+License: MIT
+Description: ===============
+ 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::
+
+ 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:\Python27\Scripts\tabulate.exe``).
+
+ You may consider installing the library only for the current user::
+
+ 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::
+
+ TABULATE_INSTALL=lib-only pip install tabulate
+
+ On Windows::
+
+ set TABULATE_INSTALL=lib-only
+ pip install tabulate
+
+
+ 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::
+
+ >>> 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)
+ * two-dimensional NumPy array
+ * NumPy record arrays (names as columns)
+ * pandas.DataFrame
+
+ Examples in this file use Python2. Tabulate supports Python3 too.
+
+
+ Headers
+ ~~~~~~~
+
+ The second optional argument named ``headers`` defines a list of
+ column headers to be used::
+
+ >>> 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::
+
+ >>> 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::
+
+ >>> 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::
+
+ >>> 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"
+ - "grid"
+ - "fancy_grid"
+ - "pipe"
+ - "orgtbl"
+ - "jira"
+ - "presto"
+ - "psql"
+ - "rst"
+ - "mediawiki"
+ - "moinmoin"
+ - "youtrack"
+ - "html"
+ - "latex"
+ - "latex_raw"
+ - "latex_booktabs"
+ - "textile"
+
+ ``plain`` tables do not use any pseudo-graphics to draw lines::
+
+ >>> 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`::
+
+ >>> print tabulate(table, headers, tablefmt="simple")
+ item qty
+ ------ -----
+ spam 42
+ eggs 451
+ bacon 0
+
+ ``grid`` is like tables formatted by Emacs' `table.el`
+ package. It corresponds to ``grid_tables`` in Pandoc Markdown
+ extensions::
+
+ >>> print tabulate(table, headers, tablefmt="grid")
+ +--------+-------+
+ | item | qty |
+ +========+=======+
+ | spam | 42 |
+ +--------+-------+
+ | eggs | 451 |
+ +--------+-------+
+ | bacon | 0 |
+ +--------+-------+
+
+ ``fancy_grid`` draws a grid using box-drawing characters::
+
+ >>> print tabulate(table, headers, tablefmt="fancy_grid")
+ ╒════════╤═══════╕
+ │ item │ qty │
+ ╞════════╪═══════╡
+ │ spam │ 42 │
+ ├────────┼───────┤
+ │ eggs │ 451 │
+ ├────────┼───────┤
+ │ bacon │ 0 │
+ ╘════════╧═══════╛
+
+ ``presto`` is like tables formatted by Presto cli::
+
+ >>> print tabulate.tabulate()
+ item | qty
+ --------+-------
+ spam | 42
+ eggs | 451
+ bacon | 0
+
+ ``psql`` is like tables formatted by Postgres' psql cli::
+
+ >>> print tabulate.tabulate()
+ +--------+-------+
+ | item | qty |
+ |--------+-------|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+ +--------+-------+
+
+ ``pipe`` follows the conventions of `PHP Markdown Extra` extension. It
+ corresponds to ``pipe_tables`` in Pandoc. This format uses colons to
+ indicate column alignment::
+
+ >>> print tabulate(table, headers, tablefmt="pipe")
+ | item | qty |
+ |:-------|------:|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+ ``orgtbl`` follows the conventions of Emacs `org-mode`, and is editable
+ also in the minor `orgtbl-mode`. Hence its name::
+
+ >>> print tabulate(table, headers, tablefmt="orgtbl")
+ | item | qty |
+ |--------+-------|
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+ ``jira`` follows the conventions of Atlassian Jira markup language::
+
+ >>> print tabulate(table, headers, tablefmt="jira")
+ || item || qty ||
+ | spam | 42 |
+ | eggs | 451 |
+ | bacon | 0 |
+
+ ``rst`` formats data like a simple table of the `reStructuredText` format::
+
+ >>> print tabulate(table, headers, tablefmt="rst")
+ ====== =====
+ item qty
+ ====== =====
+ spam 42
+ eggs 451
+ bacon 0
+ ====== =====
+
+ ``mediawiki`` format produces a table markup used in `Wikipedia` and on
+ other MediaWiki-based sites::
+
+ >>> print tabulate(table, headers, tablefmt="mediawiki")
+ {| class="wikitable" style="text-align: left;"
+ |+ <!-- caption -->
+ |-
+ ! 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`
+ wikis::
+
+ >>> print tabulate(d,headers,tablefmt="moinmoin")
+ || ''' item ''' || ''' quantity ''' ||
+ || spam || 41.999 ||
+ || eggs || 451 ||
+ || bacon || ||
+
+ ``youtrack`` format produces a table markup used in Youtrack
+ tickets::
+
+ >>> print tabulate(d,headers,tablefmt="youtrack")
+ || item || quantity ||
+ | spam | 41.999 |
+ | eggs | 451 |
+ | bacon | |
+
+ ``textile`` format produces a table markup used in `Textile` format::
+
+ >>> print tabulate(table, headers, tablefmt='textile')
+ |_. item |_. qty |
+ |<. spam |>. 42 |
+ |<. eggs |>. 451 |
+ |<. bacon |>. 0 |
+
+ ``html`` produces standard HTML markup::
+
+ >>> print tabulate(table, headers, tablefmt="html")
+ <table>
+ <tbody>
+ <tr><th>item </th><th style="text-align: right;"> qty</th></tr>
+ <tr><td>spam </td><td style="text-align: right;"> 42</td></tr>
+ <tr><td>eggs </td><td style="text-align: right;"> 451</td></tr>
+ <tr><td>bacon </td><td style="text-align: right;"> 0</td></tr>
+ </tbody>
+ </table>
+
+ ``latex`` format creates a ``tabular`` environment for LaTeX markup,
+ replacing special characters like ```` or ``\`` to their LaTeX
+ correspondents::
+
+ >>> 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.
+
+
+ .. _Pandoc Markdown extensions: http://johnmacfarlane.net/pandoc/README.html#tables
+ .. _PHP Markdown Extra: http://michelf.ca/projects/php-markdown/extra/#table
+ .. _table.el: http://table.sourceforge.net/
+ .. _org-mode: http://orgmode.org/manual/Tables.html
+ .. _reStructuredText: http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables
+ .. _Textile: http://redcloth.org/hobix.com/textile/
+ .. _Wikipedia: http://www.mediawiki.org/wiki/Help:Tables
+ .. _MoinMoin: https://moinmo.in/
+
+
+ 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::
+
+ >>> 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::
+
+ >>> 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:
+
+ ::
+
+ >>> 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
+ ---- ----
+
+
+
+ Number formatting
+ ~~~~~~~~~~~~~~~~~
+
+ ``tabulate`` allows to define custom number formatting applied to all
+ columns of decimal numbers. Use ``floatfmt`` named argument::
+
+ >>> 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::
+
+ >>> print tabulate([[0.12345, 0.12345, 0.12345]], floatfmt=(".1f", ".3f"))
+ --- ----- -------
+ 0.1 0.123 0.12345
+ --- ----- -------
+
+
+
+ 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``::
+
+ 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``::
+
+ 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``::
+
+ 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::
+
+ >>> table = [["eggs",451],["more\nspam",42]]
+ >>> headers = ["item\nname", "qty"]
+
+ ``plain`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="plain"))
+ item qty
+ name
+ eggs 451
+ more 42
+ spam
+
+ ``simple`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="simple"))
+ item qty
+ name
+ ------ -----
+ eggs 451
+ more 42
+ spam
+
+ ``grid`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="grid"))
+ +--------+-------+
+ | item | qty |
+ | name | |
+ +========+=======+
+ | eggs | 451 |
+ +--------+-------+
+ | more | 42 |
+ | spam | |
+ +--------+-------+
+
+ ``fancy_grid`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="fancy_grid"))
+ ╒════════╤═══════╕
+ │ item │ qty │
+ │ name │ │
+ ╞════════╪═══════╡
+ │ eggs │ 451 │
+ ├────────┼───────┤
+ │ more │ 42 │
+ │ spam │ │
+ ╘════════╧═══════╛
+
+ ``pipe`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="pipe"))
+ | item | qty |
+ | name | |
+ |:-------|------:|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+ ``orgtbl`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="orgtbl"))
+ | item | qty |
+ | name | |
+ |--------+-------|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+ ``jira`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="jira"))
+ | item | qty |
+ | name | |
+ |:-------|------:|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+
+ ``presto`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="presto"))
+ item | qty
+ name |
+ --------+-------
+ eggs | 451
+ more | 42
+ spam |
+
+ ``psql`` tables::
+
+ >>> print(tabulate(table, headers, tablefmt="psql"))
+ +--------+-------+
+ | item | qty |
+ | name | |
+ |--------+-------|
+ | eggs | 451 |
+ | more | 42 |
+ | spam | |
+ +--------+-------+
+
+ ``rst`` tables::
+
+ >>> 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.
+
+
+ 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)
+ -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, 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, coma or other
+ separator.
+
+ In 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.5.2 on Windows
+
+ ::
+
+ ================================= ========== ===========
+ Table formatter time, μs rel. time
+ ================================= ========== ===========
+ csv to StringIO 14.5 1.0
+ join with tabs and newlines 20.3 1.4
+ asciitable (0.8.0) 355.1 24.5
+ tabulate (0.8.2) 830.3 57.3
+ tabulate (0.8.2, WIDE_CHARS_MODE) 1483.4 102.4
+ PrettyTable (0.7.2) 1611.9 111.2
+ texttable (0.8.8) 1916.5 132.3
+ ================================= ========== ===========
+
+
+ Version history
+ ---------------
+
+ - 0.8.2: Bug fixes.
+ - 0.8.1: Multiline data in several output formats.
+ New ``latex_raw`` format.
+ Column-specific floating point formatting.
+ Python 3.5 & 3.6 support. Drop support for Python 2.6, 3.2, 3.3 (should still work).
+ - 0.7.7: Identical to 0.7.6, resolving some PyPI issues.
+ - 0.7.6: Bug fixes. New table formats (``psql``, ``jira``, ``moinmoin``, ``textile``).
+ Wide character support. Printing from database cursors.
+ Option to print row indices. Boolean columns. Ragged rows.
+ Option to disable number parsing.
+ - 0.7.5: Bug fixes. ``--float`` format option for the command line utility.
+ - 0.7.4: Bug fixes. ``fancy_grid`` and ``html`` formats. Command line utility.
+ - 0.7.3: Bug fixes. Python 3.4 support. Iterables of dicts. ``latex_booktabs`` format.
+ - 0.7.2: Python 3.2 support.
+ - 0.7.1: Bug fixes. ``tsv`` format. Column alignment can be disabled.
+ - 0.7: ``latex`` tables. Printing lists of named tuples and NumPy
+ record arrays. Fix printing date and time values. Python <= 2.6.4 is supported.
+ - 0.6: ``mediawiki`` tables, bug fixes.
+ - 0.5.1: Fix README.rst formatting. Optimize (performance similar to 0.4.4).
+ - 0.5: ANSI color sequences. Printing dicts of iterables and Pandas' dataframes.
+ - 0.4.4: Python 2.6 support.
+ - 0.4.3: Bug fix, None as a missing value.
+ - 0.4.2: Fix manifest file.
+ - 0.4.1: Update license and documentation.
+ - 0.4: Unicode support, Python3 support, ``rst`` tables.
+ - 0.3: Initial PyPI release. Table formats: ``simple``, ``plain``,
+ ``grid``, ``pipe``, and ``orgtbl``.
+
+
+ How to contribute
+ -----------------
+
+ Contributions should include tests and an explanation for the changes they
+ propose. Documentation (examples, docstrings, README.rst) should be updated
+ accordingly.
+
+ This project uses `nose` testing framework and `tox` 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, ``nose`` and ``tox`` are installed, then run ``tox`` in
+ the root of the project source tree.
+
+ On Linux ``tox`` expects to find executables like ``python2.6``,
+ ``python2.7``, ``python3.4`` etc. On Windows it looks for
+ ``C:\Python26\python.exe``, ``C:\Python27\python.exe`` and
+ ``C:\Python34\python.exe`` respectively.
+
+ To test only some Python environements, use ``-e`` option. For
+ example, to test only against Python 2.7 and Python 3.4, run::
+
+ tox -e py27,py34
+
+ in the root of the project source tree.
+
+ To enable NumPy and Pandas tests, run::
+
+ tox -e py27-extra,py34-extra
+
+ (this may take a long time the first time, because NumPy and Pandas
+ will have to be installed in the new virtual environments)
+
+ See ``tox.ini`` file to learn how to use ``nosetests`` directly to
+ test individual Python versions.
+
+ .. _nose: https://nose.readthedocs.org/
+ .. _tox: https://tox.readthedocs.io/
+
+
+ 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.
+
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: License :: OSI Approved :: MIT License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Topic :: Software Development :: Libraries
diff --git a/tabulate.egg-info/SOURCES.txt b/tabulate.egg-info/SOURCES.txt
new file mode 100644
index 0000000..b7a2d92
--- /dev/null
+++ b/tabulate.egg-info/SOURCES.txt
@@ -0,0 +1,20 @@
+LICENSE
+MANIFEST.in
+README
+README.rst
+benchmark.py
+setup.py
+tabulate.py
+tabulate.egg-info/PKG-INFO
+tabulate.egg-info/SOURCES.txt
+tabulate.egg-info/dependency_links.txt
+tabulate.egg-info/entry_points.txt
+tabulate.egg-info/requires.txt
+tabulate.egg-info/top_level.txt
+test/common.py
+test/test_api.py
+test/test_cli.py
+test/test_input.py
+test/test_internal.py
+test/test_output.py
+test/test_regression.py \ No newline at end of file
diff --git a/tabulate.egg-info/dependency_links.txt b/tabulate.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/tabulate.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/tabulate.egg-info/entry_points.txt b/tabulate.egg-info/entry_points.txt
new file mode 100644
index 0000000..29393b6
--- /dev/null
+++ b/tabulate.egg-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+tabulate = tabulate:_main
+
diff --git a/tabulate.egg-info/requires.txt b/tabulate.egg-info/requires.txt
new file mode 100644
index 0000000..c4971cc
--- /dev/null
+++ b/tabulate.egg-info/requires.txt
@@ -0,0 +1,3 @@
+
+[widechars]
+wcwidth
diff --git a/tabulate.egg-info/top_level.txt b/tabulate.egg-info/top_level.txt
new file mode 100644
index 0000000..a5d5159
--- /dev/null
+++ b/tabulate.egg-info/top_level.txt
@@ -0,0 +1 @@
+tabulate
diff --git a/tabulate.py b/tabulate.py
new file mode 100644
index 0000000..2d6d150
--- /dev/null
+++ b/tabulate.py
@@ -0,0 +1,1514 @@
+# -*- coding: utf-8 -*-
+
+"""Pretty-print tabular data."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from collections import namedtuple, Iterable
+from platform import python_version_tuple
+import re
+import math
+
+
+if python_version_tuple()[0] < "3":
+ from itertools import izip_longest
+ from functools import partial
+ _none_type = type(None)
+ _bool_type = bool
+ _int_type = int
+ _long_type = long
+ _float_type = float
+ _text_type = unicode
+ _binary_type = str
+
+ def _is_file(f):
+ return isinstance(f, file)
+
+else:
+ from itertools import zip_longest as izip_longest
+ from functools import reduce, partial
+ _none_type = type(None)
+ _bool_type = bool
+ _int_type = int
+ _long_type = int
+ _float_type = float
+ _text_type = str
+ _binary_type = bytes
+ basestring = str
+
+ import io
+ def _is_file(f):
+ return isinstance(f, io.IOBase)
+
+try:
+ import wcwidth # optional wide-character (CJK) support
+except ImportError:
+ wcwidth = None
+
+
+__all__ = ["tabulate", "tabulate_formats", "simple_separated_format"]
+__version__ = "0.8.2"
+
+
+# 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_MISSINGVAL=""
+
+
+# if True, enable wide-character (CJK) support
+WIDE_CHARS_MODE = wcwidth is not None
+
+
+Line = namedtuple("Line", ["begin", "hline", "sep", "end"])
+
+
+DataRow = namedtuple("DataRow", ["begin", "sep", "end"])
+
+
+# A table structure is suppposed to be:
+#
+# --- lineabove ---------
+# headerrow
+# --- linebelowheader ---
+# datarow
+# --- linebewteenrows ---
+# ... (more datarows) ...
+# --- linebewteenrows ---
+# 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 _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)."""
+ 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".join(["<table>", "<tbody>"])
+
+
+def _html_row_with_attrs(celltag, cell_values, colwidths, colaligns):
+ alignment = { "left": '',
+ "right": ' style="text-align: right;"',
+ "center": ' style="text-align: center;"',
+ "decimal": ' style="text-align: right;"' }
+ values_with_attrs = ["<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ''), c)
+ for c, a in zip(cell_values, colaligns)]
+ rowhtml = "<tr>" + "".join(values_with_attrs).rstrip() + "</tr>"
+ if celltag == "th": # it's a header row, create a new table header
+ rowhtml = "\n".join(["<table>",
+ "<thead>",
+ rowhtml,
+ "</thead>",
+ "<tbody>"])
+ return rowhtml
+
+def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=''):
+ alignment = { "left": '',
+ "right": '<style="text-align: right;">',
+ "center": '<style="text-align: center;">',
+ "decimal": '<style="text-align: right;">' }
+ values_with_attrs = ["{0}{1} {2} ".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):
+ 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}{" + tabular_columns_fmt + "}",
+ "\\toprule" if booktabs else "\hline"])
+
+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, (_text_type, _binary_type)) 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),
+ "fancy_grid":
+ TableFormat(lineabove=Line("╒", "═", "╤", "╕"),
+ linebelowheader=Line("╞", "═", "╪", "╡"),
+ linebetweenrows=Line("├", "─", "┼", "┤"),
+ linebelow=Line("╘", "═", "╧", "╛"),
+ headerrow=DataRow("│", "│", "│"),
+ datarow=DataRow("│", "│", "│"),
+ padding=1, with_header_hide=None),
+ "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),
+ "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|+ <!-- caption -->\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("</tbody>\n</table>", "", "", ""),
+ headerrow=partial(_html_row_with_attrs, "th"),
+ datarow=partial(_html_row_with_attrs, "td"),
+ 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),
+ "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)}
+
+
+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",
+ "fancy_grid": "fancy_grid",
+ "pipe": "pipe",
+ "orgtbl": "orgtbl",
+ "jira": "jira",
+ "presto": "presto",
+ "psql": "psql",
+ "rst": "rst",
+}
+
+# TODO: Add multiline support for the remaining table formats:
+# - mediawiki: Replace \n with <br>
+# - moinmoin: TBD
+# - youtrack: TBD
+# - html: Replace \n with <br>
+# - 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 <br/> (must be well-formed XML)
+
+_multiline_codes = re.compile(r"\r|\n|\r\n")
+_multiline_codes_bytes = re.compile(b"\r|\n|\r\n")
+_invisible_codes = re.compile(r"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes
+_invisible_codes_bytes = re.compile(b"\x1b\[\d+[;\d]*m|\x1b\[\d*\;\d*\;\d*m") # ANSI color codes
+
+
+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 _isconvertible(conv, string):
+ try:
+ n = 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, (_text_type, _binary_type)) 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, _binary_type) or isinstance(string, _text_type))\
+ and\
+ _isconvertible(inttype, string)
+
+
+def _isbool(string):
+ """
+ >>> _isbool(True)
+ True
+ >>> _isbool("False")
+ True
+ >>> _isbool(1)
+ False
+ """
+ return type(string) is _bool_type or\
+ (isinstance(string, (_binary_type, _text_type))\
+ 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, _text_type) or isinstance(string, _binary_type)):
+ string = _strip_invisible(string)
+
+ if string is None:
+ return _none_type
+ elif hasattr(string, "isoformat"): # datetime.datetime, date, and time
+ return _text_type
+ elif _isbool(string):
+ return _bool_type
+ elif _isint(string) and numparse:
+ return int
+ elif _isint(string, _long_type) and numparse:
+ return int
+ elif _isnumber(string) and numparse:
+ return float
+ elif isinstance(string, _binary_type):
+ return _binary_type
+ else:
+ return _text_type
+
+
+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
+
+ """
+ if _isnumber(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_invisible(s):
+ "Remove invisible ANSI color codes."
+ if isinstance(s, _text_type):
+ return re.sub(_invisible_codes, "", s)
+ else: # a bytestring
+ return re.sub(_invisible_codes_bytes, "", 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, _text_type) or isinstance(s, _binary_type):
+ return len_fn(_strip_invisible(s))
+ else:
+ return len_fn(_text_type(s))
+
+
+def _is_multiline(s):
+ if isinstance(s, _text_type):
+ 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)
+ 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_invisible(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(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 = _choose_width_fn(has_invisible, enable_widechars, is_multiline)
+
+ s_widths = list(map(width_fn, strings))
+ maxwidth = max(max(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 = [max((len(s) for s in re.split("[\r\n]", ms))) for ms in 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 = ["\n".join([padfn(w, s) for s in (ms.splitlines() or ms)])
+ for ms, w 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 = { _none_type: 0, _bool_type: 1, int: 2, float: 3, _binary_type: 4, _text_type: 5 }
+ invtypes = { 5: _text_type, 4: _binary_type, 3: float, 2: int, 1: _bool_type, 0: _none_type }
+ 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_type
+ True
+ >>> _column_type(["1", "2"]) is _int_type
+ True
+ >>> _column_type(["1", "2.3"]) is _float_type
+ True
+ >>> _column_type(["1", "2.3", "four"]) is _text_type
+ True
+ >>> _column_type(["four", '\u043f\u044f\u0442\u044c']) is _text_type
+ True
+ >>> _column_type([None, "brux"]) is _text_type
+ True
+ >>> _column_type([1, 2, None]) is _int_type
+ True
+ >>> import datetime as dt
+ >>> _column_type([dt.datetime(1991,2,19), dt.time(17,35)]) is _text_type
+ True
+
+ """
+ types = [_type(s, has_invisible, numparse) for s in strings ]
+ return reduce(_more_generic, types, _bool_type)
+
+
+def _format(val, valtype, floatfmt, missingval="", has_invisible=True):
+ """Format a value accoding 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
+
+ """
+ if val is None:
+ return missingval
+
+ if valtype in [int, _text_type]:
+ return "{0}".format(val)
+ elif valtype is _binary_type:
+ try:
+ return _text_type(val, "ascii")
+ except TypeError:
+ return _text_type(val)
+ elif valtype is float:
+ is_a_colored_number = has_invisible and isinstance(val, (_text_type, _binary_type))
+ if is_a_colored_number:
+ raw_val = _strip_invisible(val)
+ formatted_val = format(float(raw_val), floatfmt)
+ return val.replace(raw_val, formatted_val)
+ else:
+ return format(float(val), floatfmt)
+ else:
+ return "{0}".format(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 "{0}".format(header)
+ else:
+ return _padleft(width, header)
+
+
+def _prepend_row_index(rows, index):
+ """Add a left-most index column."""
+ if index is None or index is False:
+ return rows
+ if len(index) != len(rows):
+ print('index=', index)
+ print('rows=', rows)
+ raise ValueError('index must be as long as the number of data rows')
+ rows = [[v]+list(row) for v,row in zip(index, rows)]
+ 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")
+
+ * 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
+ except ValueError: # numpy.ndarray, pandas.core.index.Index, ...
+ is_headers2bool_broken = True
+ 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 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(_text_type,keys)) # headers should be strings
+
+ else: # it's a usual an iterable of iterables, or a NumPy array
+ 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(_text_type, rows[0]._fields))
+ elif (len(rows) > 0
+ and isinstance(rows[0], dict)):
+ # dict or OrderedDict
+ 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(_text_type, headers))
+ elif headers == "firstrow":
+ if len(rows) > 0:
+ headers = [firstdict.get(k, k) for k in keys]
+ headers = list(map(_text_type, 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 headers == "keys" and len(rows) > 0:
+ # keys are column indices
+ headers = list(map(_text_type, 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(_text_type, headers)) # headers should be strings
+ rows = rows[1:]
+
+ headers = list(map(_text_type,headers))
+ rows = list(map(list,rows))
+
+ # add or remove an index column
+ showindex_is_a_str = type(showindex) in [_text_type, _binary_type]
+ if showindex == "default" and index is not None:
+ rows = _prepend_row_index(rows, index)
+ elif isinstance(showindex, Iterable) and not showindex_is_a_str:
+ rows = _prepend_row_index(rows, list(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 tabulate(tabular_data, headers=(), tablefmt="simple",
+ floatfmt=_DEFAULT_FLOATFMT, numalign="decimal", stralign="left",
+ missingval=_DEFAULT_MISSINGVAL, showindex="default", disable_numparse=False):
+ """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,
+ 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
+ -------------
+
+ `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' and 'latex_booktabs'. 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 |
+ +------+----------+
+
+ "fancy_grid" draws a grid using box-drawing characters:
+
+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+ ... ["strings", "numbers"], "fancy_grid"))
+ ╒═══════════╤═══════════╕
+ │ 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;"
+ |+ <!-- caption -->
+ |-
+ ! strings !! align="right"| numbers
+ |-
+ | spam || align="right"| 41.9999
+ |-
+ | eggs || align="right"| 451
+ |}
+
+ "html" produces HTML markup:
+
+ >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
+ ... headers="firstrow", tablefmt="html"))
+ <table>
+ <thead>
+ <tr><th>strings </th><th style="text-align: right;"> numbers</th></tr>
+ </thead>
+ <tbody>
+ <tr><td>spam </td><td style="text-align: right;"> 41.9999</td></tr>
+ <tr><td>eggs </td><td style="text-align: right;"> 451 </td></tr>
+ </tbody>
+ </table>
+
+ "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}
+
+ 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.
+ """
+ if tabular_data is None:
+ tabular_data = []
+ list_of_lists, headers = _normalize_tabular_data(
+ tabular_data, headers, showindex=showindex)
+
+ # 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)
+
+ # optimization: look for ANSI control codes once,
+ # enable smart width functions only if a control code is found
+ plain_text = '\t'.join(['\t'.join(map(_text_type, headers))] + \
+ ['\t'.join(map(_text_type, row)) for row in list_of_lists])
+
+ has_invisible = re.search(_invisible_codes, plain_text)
+ enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
+ if 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, basestring): #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(missingval, basestring):
+ 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, miss_v, has_invisible) for v in c]
+ for c, ct, fl_fmt, miss_v in zip(cols, coltypes, float_formats, missing_vals)]
+
+ # align columns
+ aligns = [numalign if ct in [int,float] else stralign for ct in coltypes]
+ 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"])
+
+ return _format_table(tablefmt, headers, rows, minwidths, aligns, is_multiline)
+
+
+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 _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):
+ lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt))
+ return lines
+
+
+def _append_multiline_row(lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad):
+ 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)]
+ 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
+
+
+def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline):
+ """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 # 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 in padded_rows[:-1]:
+ append_row(lines, row, padded_widths, colaligns, fmt.datarow)
+ _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)
+ else:
+ for row in padded_rows:
+ 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:
+ return "\n".join(lines)
+ else: # a completely empty table
+ return ""
+
+
+def _main():
+ """\
+ Usage: tabulate [options] [FILE ...]
+
+ Pretty-print tabular data.
+ See also https://bitbucket.org/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)
+ -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, 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:f:",
+ ["help", "header", "output", "sep=", "float=", "format="])
+ except getopt.GetoptError as e:
+ print(e)
+ print(usage)
+ sys.exit(2)
+ headers = []
+ floatfmt = _DEFAULT_FLOATFMT
+ 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 ["-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, file=out)
+ else:
+ with open(f) as fobj:
+ _pprint_file(fobj, headers=headers, tablefmt=tablefmt,
+ sep=sep, floatfmt=floatfmt, file=out)
+
+
+def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, file):
+ rows = fobject.readlines()
+ table = [re.split(sep, r.rstrip()) for r in rows if r.strip()]
+ print(tabulate(table, headers, tablefmt, floatfmt=floatfmt), file=file)
+
+
+if __name__ == "__main__":
+ _main()
diff --git a/test/common.py b/test/common.py
new file mode 100644
index 0000000..1ef4fbe
--- /dev/null
+++ b/test/common.py
@@ -0,0 +1,46 @@
+try:
+ from nose.plugins.skip import SkipTest
+except ImportError:
+ try:
+ from unittest.case import SkipTest # Python >= 2.7
+ except ImportError:
+ try:
+ from unittest2.case import SkipTest # Python < 2.7
+ except ImportError:
+ class SkipTest(Exception):
+ """Raise this exception to mark a test as skipped.
+ """
+ pass
+
+
+try:
+ from nose.tools import assert_equal, assert_in, assert_raises
+
+
+except ImportError:
+ def assert_equal(expected, result):
+ print("Expected:\n%s\n" % expected)
+ print("Got:\n%s\n" % result)
+ assert expected == result
+
+
+ def assert_in(result, expected_set):
+ nums = xrange(1, len(expected_set)+1)
+ for i, expected in zip(nums, expected_set):
+ print("Expected %d:\n%s\n" % (i, expected))
+ print("Got:\n%s\n" % result)
+ assert result in expected_set
+
+
+ class assert_raises(object):
+ def __init__(self, exception_type):
+ self.watch_exception_type = exception_type
+ def __enter__(self):
+ pass
+ def __exit__(self, exception_type, exception_value, traceback):
+ if isinstance(exception_value, self.watch_exception_type):
+ return True # suppress exception
+ elif exception_type is None:
+ msg = "%s not raised" % self.watch_exception_type.__name__
+ raise AssertionError(msg)
+ # otherwise propagate whatever other exception is raised
diff --git a/test/test_api.py b/test/test_api.py
new file mode 100644
index 0000000..2023a81
--- /dev/null
+++ b/test/test_api.py
@@ -0,0 +1,58 @@
+"""API properties.
+
+"""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from tabulate import tabulate, tabulate_formats, simple_separated_format
+from platform import python_version_tuple
+from common import SkipTest
+
+
+try:
+ if python_version_tuple() >= ('3','3','0'):
+ from inspect import signature, _empty
+ else:
+ signature = None
+ _empty = None
+except ImportError:
+ signature = None
+ _empty = None
+
+
+def test_tabulate_formats():
+ "API: tabulate_formats is a list of strings"""
+ supported = tabulate_formats
+ print("tabulate_formats = %r" % supported)
+ assert type(supported) is list
+ for fmt in supported:
+ assert type(fmt) is type("")
+
+
+def _check_signature(function, expected_sig):
+ if not signature:
+ raise SkipTest()
+ actual_sig = signature(function)
+ print("expected: %s\nactual: %s\n" % (expected_sig, str(actual_sig)))
+ for (e, ev), (a, av) in zip(expected_sig, actual_sig.parameters.items()):
+ assert e == a and ev == av.default
+
+
+def test_tabulate_signature():
+ "API: tabulate() type signature is unchanged"""
+ assert type(tabulate) is type(lambda: None)
+ expected_sig = [("tabular_data", _empty),
+ ("headers", ()),
+ ("tablefmt", "simple"),
+ ("floatfmt", "g"),
+ ("numalign", "decimal"),
+ ("stralign", "left"),
+ ("missingval", "")]
+ _check_signature(tabulate, expected_sig)
+
+
+def test_simple_separated_format_signature():
+ "API: simple_separated_format() type signature is unchanged"""
+ assert type(simple_separated_format) is type(lambda: None)
+ expected_sig = [("separator", _empty)]
+ _check_signature(simple_separated_format, expected_sig)
diff --git a/test/test_cli.py b/test/test_cli.py
new file mode 100644
index 0000000..043e823
--- /dev/null
+++ b/test/test_cli.py
@@ -0,0 +1,190 @@
+"""Command-line interface.
+
+"""
+
+
+from __future__ import print_function
+from __future__ import unicode_literals
+import os
+
+
+import subprocess
+import tempfile
+
+
+from common import assert_equal
+
+
+SAMPLE_SIMPLE_FORMAT = "\n".join(
+ ['----- ------ -------------',
+ 'Sun 696000 1.9891e+09',
+ 'Earth 6371 5973.6',
+ 'Moon 1737 73.5',
+ 'Mars 3390 641.85',
+ '----- ------ -------------'])
+
+
+SAMPLE_SIMPLE_FORMAT_WITH_HEADERS = "\n".join(
+ ['Planet Radius Mass',
+ '-------- -------- -------------',
+ 'Sun 696000 1.9891e+09',
+ 'Earth 6371 5973.6',
+ 'Moon 1737 73.5',
+ 'Mars 3390 641.85'])
+
+
+SAMPLE_GRID_FORMAT_WITH_HEADERS = "\n".join(
+ ['+----------+----------+---------------+',
+ '| Planet | Radius | Mass |',
+ '+==========+==========+===============+',
+ '| Sun | 696000 | 1.9891e+09 |',
+ '+----------+----------+---------------+',
+ '| Earth | 6371 | 5973.6 |',
+ '+----------+----------+---------------+',
+ '| Moon | 1737 | 73.5 |',
+ '+----------+----------+---------------+',
+ '| Mars | 3390 | 641.85 |',
+ '+----------+----------+---------------+'])
+
+
+SAMPLE_GRID_FORMAT_WITH_DOT1E_FLOATS = "\n".join(
+ ['+-------+--------+---------+',
+ '| Sun | 696000 | 2.0e+09 |',
+ '+-------+--------+---------+',
+ '| Earth | 6371 | 6.0e+03 |',
+ '+-------+--------+---------+',
+ '| Moon | 1737 | 7.4e+01 |',
+ '+-------+--------+---------+',
+ '| Mars | 3390 | 6.4e+02 |',
+ '+-------+--------+---------+'])
+
+
+def sample_input(sep=' ', with_headers=False):
+ headers = sep.join(['Planet', 'Radius', 'Mass'])
+ rows = [sep.join(['Sun', '696000', '1.9891e9']),
+ sep.join(['Earth', '6371', '5973.6']),
+ sep.join(['Moon', '1737', '73.5']),
+ sep.join(['Mars', '3390', '641.85'])]
+ all_rows = ([headers] + rows) if with_headers else rows
+ table = "\n".join(all_rows)
+ return table
+
+
+def run_and_capture_stdout(cmd, input=None):
+ x = subprocess.Popen(cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ input_buf = input.encode() if input else None
+ out, err = x.communicate(input=input_buf)
+ out = out.decode("utf-8")
+ if x.returncode != 0:
+ raise IOError(err)
+ return out
+
+
+class TemporaryTextFile(object):
+ def __init__(self):
+ self.tmpfile = None
+ def __enter__(self):
+ self.tmpfile = tempfile.NamedTemporaryFile("w+", prefix="tabulate-test-tmp-", delete=False)
+ return self.tmpfile
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if self.tmpfile:
+ self.tmpfile.close()
+ os.unlink(self.tmpfile.name)
+
+
+def test_script_from_stdin_to_stdout():
+ """Command line utility: read from stdin, print to stdout"""
+ cmd = ["python", "tabulate.py"]
+ out = run_and_capture_stdout(cmd, input=sample_input())
+ expected = SAMPLE_SIMPLE_FORMAT
+ print("got: ",repr(out))
+ print("expected:",repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+
+
+def test_script_from_file_to_stdout():
+ """Command line utility: read from file, print to stdout"""
+ with TemporaryTextFile() as tmpfile:
+ tmpfile.write(sample_input())
+ tmpfile.seek(0)
+ cmd = ["python", "tabulate.py", tmpfile.name]
+ out = run_and_capture_stdout(cmd)
+ expected = SAMPLE_SIMPLE_FORMAT
+ print("got: ",repr(out))
+ print("expected:",repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+
+
+def test_script_from_file_to_file():
+ """Command line utility: read from file, write to file"""
+ with TemporaryTextFile() as input_file:
+ with TemporaryTextFile() as output_file:
+ input_file.write(sample_input())
+ input_file.seek(0)
+ cmd = ["python", "tabulate.py", "-o", output_file.name, input_file.name]
+ out = run_and_capture_stdout(cmd)
+ # check that nothing is printed to stdout
+ expected = ""
+ print("got: ", repr(out))
+ print("expected:", repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+ # check that the output was written to file
+ output_file.seek(0)
+ out = output_file.file.read()
+ expected = SAMPLE_SIMPLE_FORMAT
+ print("got: ", repr(out))
+ print("expected:", repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+
+
+def test_script_header_option():
+ """Command line utility: -1, --header option"""
+ for option in ["-1", "--header"]:
+ cmd = ["python", "tabulate.py", option]
+ raw_table = sample_input(with_headers=True)
+ out = run_and_capture_stdout(cmd, input=raw_table)
+ expected = SAMPLE_SIMPLE_FORMAT_WITH_HEADERS
+ print(out)
+ print("got: ",repr(out))
+ print("expected:",repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+
+
+def test_script_sep_option():
+ """Command line utility: -s, --sep option"""
+ for option in ["-s", "--sep"]:
+ cmd = ["python", "tabulate.py", option, ","]
+ raw_table = sample_input(sep=",")
+ out = run_and_capture_stdout(cmd, input=raw_table)
+ expected = SAMPLE_SIMPLE_FORMAT
+ print("got: ",repr(out))
+ print("expected:",repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+
+
+def test_script_floatfmt_option():
+ """Command line utility: -F, --float option"""
+ for option in ["-F", "--float"]:
+ cmd = ["python", "tabulate.py", option, ".1e", "--format", "grid"]
+ raw_table = sample_input()
+ out = run_and_capture_stdout(cmd, input=raw_table)
+ expected = SAMPLE_GRID_FORMAT_WITH_DOT1E_FLOATS
+ print("got: ",repr(out))
+ print("expected:",repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
+
+
+def test_script_format_option():
+ """Command line utility: -f, --format option"""
+ for option in ["-f", "--format"]:
+ cmd = ["python", "tabulate.py", "-1", option, "grid"]
+ raw_table = sample_input(with_headers=True)
+ out = run_and_capture_stdout(cmd, input=raw_table)
+ expected = SAMPLE_GRID_FORMAT_WITH_HEADERS
+ print(out)
+ print("got: ",repr(out))
+ print("expected:",repr(expected))
+ assert_equal(out.splitlines(), expected.splitlines())
diff --git a/test/test_input.py b/test/test_input.py
new file mode 100644
index 0000000..d44fe1d
--- /dev/null
+++ b/test/test_input.py
@@ -0,0 +1,450 @@
+# -*- coding: utf-8 -*-
+
+"""Test support of the various forms of tabular data."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from tabulate import tabulate
+from common import assert_equal, assert_in, assert_raises, SkipTest
+
+
+def test_iterable_of_iterables():
+ "Input: an interable of iterables."
+ ii = iter(map(lambda x: iter(x), [range(5), range(5,0,-1)]))
+ expected = "\n".join(
+ ['- - - - -',
+ '0 1 2 3 4',
+ '5 4 3 2 1',
+ '- - - - -'])
+ result = tabulate(ii)
+ assert_equal(expected, result)
+
+
+def test_iterable_of_iterables_headers():
+ "Input: an interable of iterables with headers."
+ ii = iter(map(lambda x: iter(x), [range(5), range(5,0,-1)]))
+ expected = "\n".join(
+ [' a b c d e',
+ '--- --- --- --- ---',
+ ' 0 1 2 3 4',
+ ' 5 4 3 2 1'])
+ result = tabulate(ii, "abcde")
+ assert_equal(expected, result)
+
+
+def test_iterable_of_iterables_firstrow():
+ "Input: an interable of iterables with the first row as headers"
+ ii = iter(map(lambda x: iter(x), ["abcde", range(5), range(5,0,-1)]))
+ expected = "\n".join(
+ [' a b c d e',
+ '--- --- --- --- ---',
+ ' 0 1 2 3 4',
+ ' 5 4 3 2 1'])
+ result = tabulate(ii, "firstrow")
+ assert_equal(expected, result)
+
+
+def test_list_of_lists():
+ "Input: a list of lists with headers."
+ ll = [["a","one",1],["b","two",None]]
+ expected = "\n".join([
+ ' string number',
+ '-- -------- --------',
+ 'a one 1',
+ 'b two'])
+ result = tabulate(ll, headers=["string","number"])
+ assert_equal(expected, result)
+
+
+def test_list_of_lists_firstrow():
+ "Input: a list of lists with the first row as headers."
+ ll = [["string","number"],["a","one",1],["b","two",None]]
+ expected = "\n".join([
+ ' string number',
+ '-- -------- --------',
+ 'a one 1',
+ 'b two'])
+ result = tabulate(ll, headers="firstrow")
+ assert_equal(expected, result)
+
+
+def test_list_of_lists_keys():
+ "Input: a list of lists with column indices as headers."
+ ll = [["a","one",1],["b","two",None]]
+ expected = "\n".join([
+ '0 1 2',
+ '--- --- ---',
+ 'a one 1',
+ 'b two'])
+ result = tabulate(ll, headers="keys")
+ assert_equal(expected, result)
+
+
+def test_dict_like():
+ "Input: a dict of iterables with keys as headers."
+ # columns should be padded with None, keys should be used as headers
+ dd = {"a": range(3), "b": range(101,105)}
+ # keys' order (hence columns' order) is not deterministic in Python 3
+ # => we have to consider both possible results as valid
+ expected1 = "\n".join([
+ ' a b',
+ '--- ---',
+ ' 0 101',
+ ' 1 102',
+ ' 2 103',
+ ' 104'])
+ expected2 = "\n".join([
+ ' b a',
+ '--- ---',
+ '101 0',
+ '102 1',
+ '103 2',
+ '104'])
+ result = tabulate(dd, "keys")
+ print("Keys' order: %s" % dd.keys())
+ assert_in(result, [expected1, expected2])
+
+
+def test_numpy_2d():
+ "Input: a 2D NumPy array with headers."
+ try:
+ import numpy
+ na = (numpy.arange(1,10, dtype=numpy.float32).reshape((3,3))**3)*0.5
+ expected = "\n".join([
+ ' a b c',
+ '----- ----- -----',
+ ' 0.5 4 13.5',
+ ' 32 62.5 108',
+ '171.5 256 364.5'])
+ result = tabulate(na, ["a", "b", "c"])
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_numpy_2d is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_numpy_2d_firstrow():
+ "Input: a 2D NumPy array with the first row as headers."
+ try:
+ import numpy
+ na = (numpy.arange(1,10, dtype=numpy.int32).reshape((3,3))**3)
+ expected = "\n".join([
+ ' 1 8 27',
+ '--- --- ----',
+ ' 64 125 216',
+ '343 512 729'])
+ result = tabulate(na, headers="firstrow")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_numpy_2d_firstrow is skipped")
+ raise SkipTest() # this test is optional
+
+
+
+def test_numpy_2d_keys():
+ "Input: a 2D NumPy array with column indices as headers."
+ try:
+ import numpy
+ na = (numpy.arange(1,10, dtype=numpy.float32).reshape((3,3))**3)*0.5
+ expected = "\n".join([
+ ' 0 1 2',
+ '----- ----- -----',
+ ' 0.5 4 13.5',
+ ' 32 62.5 108',
+ '171.5 256 364.5'])
+ result = tabulate(na, headers="keys")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_numpy_2d_keys is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_numpy_record_array():
+ "Input: a 2D NumPy record array without header."
+ try:
+ import numpy
+ na = numpy.asarray([("Alice", 23, 169.5),
+ ("Bob", 27, 175.0)],
+ dtype={"names":["name","age","height"],
+ "formats":["a32","uint8","float32"]})
+ expected = "\n".join([
+ "----- -- -----",
+ "Alice 23 169.5",
+ "Bob 27 175",
+ "----- -- -----" ])
+ result = tabulate(na)
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_numpy_2d_keys is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_numpy_record_array_keys():
+ "Input: a 2D NumPy record array with column names as headers."
+ try:
+ import numpy
+ na = numpy.asarray([("Alice", 23, 169.5),
+ ("Bob", 27, 175.0)],
+ dtype={"names":["name","age","height"],
+ "formats":["a32","uint8","float32"]})
+ expected = "\n".join([
+ "name age height",
+ "------ ----- --------",
+ "Alice 23 169.5",
+ "Bob 27 175" ])
+ result = tabulate(na, headers="keys")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_numpy_2d_keys is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_numpy_record_array_headers():
+ "Input: a 2D NumPy record array with user-supplied headers."
+ try:
+ import numpy
+ na = numpy.asarray([("Alice", 23, 169.5),
+ ("Bob", 27, 175.0)],
+ dtype={"names":["name","age","height"],
+ "formats":["a32","uint8","float32"]})
+ expected = "\n".join([
+ "person years cm",
+ "-------- ------- -----",
+ "Alice 23 169.5",
+ "Bob 27 175" ])
+ result = tabulate(na, headers=["person", "years", "cm"])
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_numpy_2d_keys is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_pandas():
+ "Input: a Pandas DataFrame."
+ try:
+ import pandas
+ df = pandas.DataFrame([["one",1],["two",None]], index=["a","b"])
+ expected = "\n".join([
+ ' string number',
+ '-- -------- --------',
+ 'a one 1',
+ 'b two nan'])
+ result = tabulate(df, headers=["string", "number"])
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_pandas_firstrow():
+ "Input: a Pandas DataFrame with the first row as headers."
+ try:
+ import pandas
+ df = pandas.DataFrame([["one",1],["two",None]],
+ columns=["string","number"],
+ index=["a","b"])
+ expected = "\n".join([
+ 'a one 1.0',
+ '--- ----- -----',
+ 'b two nan'])
+ result = tabulate(df, headers="firstrow")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas_firstrow is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_pandas_keys():
+ "Input: a Pandas DataFrame with keys as headers."
+ try:
+ import pandas
+ df = pandas.DataFrame([["one",1],["two",None]],
+ columns=["string","number"],
+ index=["a","b"])
+ expected = "\n".join(
+ [' string number',
+ '-- -------- --------',
+ 'a one 1',
+ 'b two nan'])
+ result = tabulate(df, headers="keys")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas_keys is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_sqlite3():
+ "Input: an sqlite3 cursor"
+ try:
+ import sqlite3
+ conn = sqlite3.connect(':memory:')
+ cursor = conn.cursor()
+ cursor.execute('CREATE TABLE people (name, age, height)')
+ for values in [
+ ("Alice", 23, 169.5),
+ ("Bob", 27, 175.0)]:
+ cursor.execute('INSERT INTO people VALUES (?, ?, ?)', values)
+ cursor.execute('SELECT name, age, height FROM people ORDER BY name')
+ result = tabulate(cursor, headers=["whom", "how old", "how tall"])
+ expected = """\
+whom how old how tall
+------ --------- ----------
+Alice 23 169.5
+Bob 27 175"""
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_sqlite3 is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_sqlite3_keys():
+ "Input: an sqlite3 cursor with keys as headers"
+ try:
+ import sqlite3
+ conn = sqlite3.connect(':memory:')
+ cursor = conn.cursor()
+ cursor.execute('CREATE TABLE people (name, age, height)')
+ for values in [
+ ("Alice", 23, 169.5),
+ ("Bob", 27, 175.0)]:
+ cursor.execute('INSERT INTO people VALUES (?, ?, ?)', values)
+ cursor.execute('SELECT name "whom", age "how old", height "how tall" FROM people ORDER BY name')
+ result = tabulate(cursor, headers="keys")
+ expected = """\
+whom how old how tall
+------ --------- ----------
+Alice 23 169.5
+Bob 27 175"""
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_sqlite3_keys is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_list_of_namedtuples():
+ "Input: a list of named tuples with field names as headers."
+ from collections import namedtuple
+ NT = namedtuple("NT", ['foo', 'bar'])
+ lt = [NT(1,2), NT(3,4)]
+ expected = "\n".join([
+ '- -',
+ '1 2',
+ '3 4',
+ '- -'])
+ result = tabulate(lt)
+ assert_equal(expected, result)
+
+
+def test_list_of_namedtuples_keys():
+ "Input: a list of named tuples with field names as headers."
+ from collections import namedtuple
+ NT = namedtuple("NT", ['foo', 'bar'])
+ lt = [NT(1,2), NT(3,4)]
+ expected = "\n".join([
+ ' foo bar',
+ '----- -----',
+ ' 1 2',
+ ' 3 4'])
+ result = tabulate(lt, headers="keys")
+ assert_equal(expected, result)
+
+
+def test_list_of_dicts():
+ "Input: a list of dictionaries."
+ lod = [{'foo' : 1, 'bar' : 2}, {'foo' : 3, 'bar' : 4}]
+ expected1 = "\n".join([
+ '- -',
+ '1 2',
+ '3 4',
+ '- -'])
+ expected2 = "\n".join([
+ '- -',
+ '2 1',
+ '4 3',
+ '- -'])
+ result = tabulate(lod)
+ assert_in(result, [expected1, expected2])
+
+
+def test_list_of_dicts_keys():
+ "Input: a list of dictionaries, with keys as headers."
+ lod = [{'foo' : 1, 'bar' : 2}, {'foo' : 3, 'bar' : 4}]
+ expected1 = "\n".join([
+ ' foo bar',
+ '----- -----',
+ ' 1 2',
+ ' 3 4'])
+ expected2 = "\n".join([
+ ' bar foo',
+ '----- -----',
+ ' 2 1',
+ ' 4 3'])
+ result = tabulate(lod, headers="keys")
+ assert_in(result, [expected1, expected2])
+
+
+def test_list_of_dicts_with_missing_keys():
+ "Input: a list of dictionaries, with missing keys."
+ lod = [{"foo": 1}, {"bar": 2}, {"foo":4, "baz": 3}]
+ expected = "\n".join([
+ ' foo bar baz',
+ '----- ----- -----',
+ ' 1',
+ ' 2',
+ ' 4 3'])
+ result = tabulate(lod, headers="keys")
+ assert_equal(expected, result)
+
+
+def test_list_of_dicts_firstrow():
+ "Input: a list of dictionaries, with the first dict as headers."
+ lod = [{'foo' : "FOO", 'bar' : "BAR"}, {'foo' : 3, 'bar': 4, 'baz': 5}]
+ # if some key is missing in the first dict, use the key name instead
+ expected1 = "\n".join([
+ ' FOO BAR baz',
+ '----- ----- -----',
+ ' 3 4 5'])
+ expected2 = "\n".join([
+ ' BAR FOO baz',
+ '----- ----- -----',
+ ' 4 3 5'])
+ result = tabulate(lod, headers="firstrow")
+ assert_in(result, [expected1, expected2])
+
+
+def test_list_of_dicts_with_dict_of_headers():
+ "Input: a dict of user headers for a list of dicts (issue #23)"
+ table = [{"letters": "ABCDE", "digits": 12345}]
+ headers = {"digits": "DIGITS", "letters": "LETTERS"}
+ expected1 = "\n".join([
+ ' DIGITS LETTERS',
+ '-------- ---------',
+ ' 12345 ABCDE'])
+ expected2 = "\n".join([
+ 'LETTERS DIGITS',
+ '--------- --------',
+ 'ABCDE 12345'])
+ result = tabulate(table, headers=headers)
+ assert_in(result, [expected1, expected2])
+
+
+def test_list_of_dicts_with_list_of_headers():
+ "Input: ValueError on a list of headers with a list of dicts (issue #23)"
+ table = [{"letters": "ABCDE", "digits": 12345}]
+ headers = ["DIGITS", "LETTERS"]
+ with assert_raises(ValueError):
+ tabulate(table, headers=headers)
+
+
+def test_py27orlater_list_of_ordereddicts():
+ "Input: a list of OrderedDicts."
+ from collections import OrderedDict
+ od = OrderedDict([('b', 1), ('a', 2)])
+ lod = [od, od]
+ expected = "\n".join([
+ ' b a',
+ '--- ---',
+ ' 1 2',
+ ' 1 2'])
+ result = tabulate(lod, headers="keys")
+ assert_equal(expected, result)
diff --git a/test/test_internal.py b/test/test_internal.py
new file mode 100644
index 0000000..0e5d7e9
--- /dev/null
+++ b/test/test_internal.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+
+"""Tests of the internal tabulate functions."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+import tabulate as T
+from common import assert_equal, assert_in, assert_raises, SkipTest
+
+
+def test_multiline_width():
+ "Internal: _multiline_width()"
+ multiline_string = "\n".join(["foo", "barbaz", "spam"])
+ assert_equal(T._multiline_width(multiline_string), 6)
+ oneline_string = "12345"
+ assert_equal(T._multiline_width(oneline_string), len(oneline_string))
+
+
+def test_align_column_decimal():
+ "Internal: _align_column(..., 'decimal')"
+ column = ["12.345", "-1234.5", "1.23", "1234.5", "1e+234", "1.0e234"]
+ output = T._align_column(column, "decimal")
+ expected = [
+ ' 12.345 ',
+ '-1234.5 ',
+ ' 1.23 ',
+ ' 1234.5 ',
+ ' 1e+234 ',
+ ' 1.0e234']
+ assert_equal(output, expected)
+
+
+def test_align_column_none():
+ "Internal: _align_column(..., None)"
+ column = ['123.4', '56.7890']
+ output = T._align_column(column, None)
+ expected = ['123.4', '56.7890']
+ assert_equal(output, expected)
+
+
+def test_align_column_multiline():
+ "Internal: _align_column(..., is_multiline=True)"
+ column = ["1", "123", "12345\n6"]
+ output = T._align_column(column, "center", is_multiline=True)
+ expected = [
+ " 1 ",
+ " 123 ",
+ "12345" + "\n" +
+ " 6 "]
+ assert_equal(output, expected)
diff --git a/test/test_output.py b/test/test_output.py
new file mode 100644
index 0000000..cee333f
--- /dev/null
+++ b/test/test_output.py
@@ -0,0 +1,1192 @@
+# -*- coding: utf-8 -*-
+
+"""Test output of the various forms of tabular data."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+import tabulate as tabulate_module
+from tabulate import tabulate, simple_separated_format
+from common import assert_equal, assert_raises, SkipTest
+
+
+# _test_table shows
+# - coercion of a string to a number,
+# - left alignment of text,
+# - decimal point alignment of numbers
+_test_table = [["spam", 41.9999], ["eggs", "451.0"]]
+_test_table_headers = ["strings", "numbers"]
+
+
+def test_plain():
+ "Output: plain with headers"
+ expected = "\n".join(['strings numbers',
+ 'spam 41.9999',
+ 'eggs 451',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="plain")
+ assert_equal(expected, result)
+
+
+def test_plain_headerless():
+ "Output: plain without headers"
+ expected = "\n".join(['spam 41.9999',
+ 'eggs 451',])
+ result = tabulate(_test_table, tablefmt="plain")
+ assert_equal(expected, result)
+
+
+def test_plain_multiline_headerless():
+ "Output: plain with multiline cells without headers"
+ table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]]
+ expected = "\n".join([
+ "foo bar hello",
+ " baz",
+ " bau",
+ " multiline",
+ " world"])
+ result = tabulate(table, stralign="center", tablefmt="plain")
+ assert_equal(expected, result)
+
+
+def test_plain_multiline():
+ "Output: plain with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ " more more spam",
+ " spam \x1b[31meggs\x1b[0m & eggs",
+ " 2 foo",
+ " bar"])
+ result = tabulate(table, headers, tablefmt="plain")
+ assert_equal(expected, result)
+
+
+def test_plain_multiline_with_empty_cells():
+ "Output: plain with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ " hdr data fold",
+ " 1",
+ " 2 very long data fold",
+ " this"])
+ result = tabulate(table, headers="firstrow", tablefmt="plain")
+ assert_equal(expected, result)
+
+
+def test_plain_multiline_with_empty_cells_headerless():
+ "Output: plain with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "0",
+ "1",
+ "2 very long data fold",
+ " this"])
+ result = tabulate(table, tablefmt="plain")
+ assert_equal(expected, result)
+
+
+def test_simple():
+ "Output: simple with headers"
+ expected = "\n".join(['strings numbers',
+ '--------- ---------',
+ 'spam 41.9999',
+ 'eggs 451',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_simple_multiline():
+ "Output: simple with multiline cells"
+ expected = "\n".join([
+ " key value",
+ "----- ---------",
+ " foo bar",
+ "spam multiline",
+ " world",
+ ])
+ table = [["key", "value"], ["foo", "bar"], ["spam", "multiline\nworld"]]
+ result = tabulate(table, headers='firstrow', stralign="center",
+ tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_simple_headerless():
+ "Output: simple without headers"
+ expected = "\n".join(['---- --------',
+ 'spam 41.9999',
+ 'eggs 451',
+ '---- --------',])
+ result = tabulate(_test_table, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_simple_multiline_headerless():
+ "Output: simple with multiline cells without headers"
+ table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]]
+ expected = "\n".join([
+ "------- ---------",
+ "foo bar hello",
+ " baz",
+ " bau",
+ " multiline",
+ " world",
+ "------- ---------",])
+ result = tabulate(table, stralign="center", tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_simple_multiline():
+ "Output: simple with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ " more more spam",
+ " spam \x1b[31meggs\x1b[0m & eggs",
+ "----------- -----------",
+ " 2 foo",
+ " bar"])
+ result = tabulate(table, headers, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_simple_multiline_with_empty_cells():
+ "Output: simple with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ " hdr data fold",
+ "----- -------------- ------",
+ " 1",
+ " 2 very long data fold",
+ " this"])
+ result = tabulate(table, headers="firstrow", tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_simple_multiline_with_empty_cells_headerless():
+ "Output: simple with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "- -------------- ----",
+ "0",
+ "1",
+ "2 very long data fold",
+ " this",
+ "- -------------- ----",])
+ result = tabulate(table, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_grid():
+ "Output: grid with headers"
+ expected = '\n'.join(['+-----------+-----------+',
+ '| strings | numbers |',
+ '+===========+===========+',
+ '| spam | 41.9999 |',
+ '+-----------+-----------+',
+ '| eggs | 451 |',
+ '+-----------+-----------+',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_grid_wide_characters():
+ "Output: grid with wide characters in headers"
+ try:
+ import wcwidth
+ except ImportError:
+ print("test_grid_wide_characters is skipped")
+ raise SkipTest() # this test is optional
+ headers = list(_test_table_headers)
+ headers[1] = '配列'
+ expected = '\n'.join(['+-----------+----------+',
+ '| strings | 配列 |',
+ '+===========+==========+',
+ '| spam | 41.9999 |',
+ '+-----------+----------+',
+ '| eggs | 451 |',
+ '+-----------+----------+',])
+ result = tabulate(_test_table, headers, tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_grid_headerless():
+ "Output: grid without headers"
+ expected = '\n'.join(['+------+----------+',
+ '| spam | 41.9999 |',
+ '+------+----------+',
+ '| eggs | 451 |',
+ '+------+----------+',])
+ result = tabulate(_test_table, tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_grid_multiline_headerless():
+ "Output: grid with multiline cells without headers"
+ table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]]
+ expected = "\n".join([
+ "+---------+-----------+",
+ "| foo bar | hello |",
+ "| baz | |",
+ "| bau | |",
+ "+---------+-----------+",
+ "| | multiline |",
+ "| | world |",
+ "+---------+-----------+"])
+ result = tabulate(table, stralign="center", tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_grid_multiline():
+ "Output: grid with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ "+-------------+-------------+",
+ "| more | more spam |",
+ "| spam \x1b[31meggs\x1b[0m | & eggs |",
+ "+=============+=============+",
+ "| 2 | foo |",
+ "| | bar |",
+ "+-------------+-------------+"])
+ result = tabulate(table, headers, tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_grid_multiline_with_empty_cells():
+ "Output: grid with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "+-------+----------------+--------+",
+ "| hdr | data | fold |",
+ "+=======+================+========+",
+ "| 1 | | |",
+ "+-------+----------------+--------+",
+ "| 2 | very long data | fold |",
+ "| | | this |",
+ "+-------+----------------+--------+"])
+ result = tabulate(table, headers="firstrow", tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_grid_multiline_with_empty_cells_headerless():
+ "Output: grid with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "+---+----------------+------+",
+ "| 0 | | |",
+ "+---+----------------+------+",
+ "| 1 | | |",
+ "+---+----------------+------+",
+ "| 2 | very long data | fold |",
+ "| | | this |",
+ "+---+----------------+------+"])
+ result = tabulate(table, tablefmt="grid")
+ assert_equal(expected, result)
+
+
+def test_fancy_grid():
+ "Output: fancy_grid with headers"
+ expected = '\n'.join([
+ '╒═══════════╤═══════════╕',
+ '│ strings │ numbers │',
+ '╞═══════════╪═══════════╡',
+ '│ spam │ 41.9999 │',
+ '├───────────┼───────────┤',
+ '│ eggs │ 451 │',
+ '╘═══════════╧═══════════╛',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="fancy_grid")
+ assert_equal(expected, result)
+
+
+def test_fancy_grid_headerless():
+ "Output: fancy_grid without headers"
+ expected = '\n'.join([
+ '╒══════╤══════════╕',
+ '│ spam │ 41.9999 │',
+ '├──────┼──────────┤',
+ '│ eggs │ 451 │',
+ '╘══════╧══════════╛',])
+ result = tabulate(_test_table, tablefmt="fancy_grid")
+ assert_equal(expected, result)
+
+
+def test_fancy_grid_multiline_headerless():
+ "Output: fancy_grid with multiline cells without headers"
+ table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]]
+ expected = "\n".join([
+ "╒═════════╤═══════════╕",
+ "│ foo bar │ hello │",
+ "│ baz │ │",
+ "│ bau │ │",
+ "├─────────┼───────────┤",
+ "│ │ multiline │",
+ "│ │ world │",
+ "╘═════════╧═══════════╛"])
+ result = tabulate(table, stralign="center", tablefmt="fancy_grid")
+ assert_equal(expected, result)
+
+
+def test_fancy_grid_multiline():
+ "Output: fancy_grid with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ "╒═════════════╤═════════════╕",
+ "│ more │ more spam │",
+ "│ spam \x1b[31meggs\x1b[0m │ & eggs │",
+ "╞═════════════╪═════════════╡",
+ "│ 2 │ foo │",
+ "│ │ bar │",
+ "╘═════════════╧═════════════╛"])
+ result = tabulate(table, headers, tablefmt="fancy_grid")
+ assert_equal(expected, result)
+
+
+def test_fancy_grid_multiline_with_empty_cells():
+ "Output: fancy_grid with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "╒═══════╤════════════════╤════════╕",
+ "│ hdr │ data │ fold │",
+ "╞═══════╪════════════════╪════════╡",
+ "│ 1 │ │ │",
+ "├───────┼────────────────┼────────┤",
+ "│ 2 │ very long data │ fold │",
+ "│ │ │ this │",
+ "╘═══════╧════════════════╧════════╛"])
+ result = tabulate(table, headers="firstrow", tablefmt="fancy_grid")
+ assert_equal(expected, result)
+
+
+def test_fancy_grid_multiline_with_empty_cells_headerless():
+ "Output: fancy_grid with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "╒═══╤════════════════╤══════╕",
+ "│ 0 │ │ │",
+ "├───┼────────────────┼──────┤",
+ "│ 1 │ │ │",
+ "├───┼────────────────┼──────┤",
+ "│ 2 │ very long data │ fold │",
+ "│ │ │ this │",
+ "╘═══╧════════════════╧══════╛"])
+ result = tabulate(table, tablefmt="fancy_grid")
+ assert_equal(expected, result)
+
+
+def test_pipe():
+ "Output: pipe with headers"
+ expected = '\n'.join(['| strings | numbers |',
+ '|:----------|----------:|',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="pipe")
+ assert_equal(expected, result)
+
+
+def test_pipe_headerless():
+ "Output: pipe without headers"
+ expected = '\n'.join(['|:-----|---------:|',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',])
+ result = tabulate(_test_table, tablefmt="pipe")
+ assert_equal(expected, result)
+
+
+def test_presto():
+ "Output: presto with headers"
+ expected = '\n'.join([' strings | numbers',
+ '-----------+-----------',
+ ' spam | 41.9999',
+ ' eggs | 451',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="presto")
+ assert_equal(expected, result)
+
+
+def test_presto_headerless():
+ "Output: presto without headers"
+ expected = '\n'.join([' spam | 41.9999',
+ ' eggs | 451',])
+ result = tabulate(_test_table, tablefmt="presto")
+ assert_equal(expected, result)
+
+
+def test_presto_multiline_headerless():
+ "Output: presto with multiline cells without headers"
+ table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]]
+ expected = "\n".join([
+ " foo bar | hello",
+ " baz |",
+ " bau |",
+ " | multiline",
+ " | world"])
+ result = tabulate(table, stralign="center", tablefmt="presto")
+ assert_equal(expected, result)
+
+
+def test_presto_multiline():
+ "Output: presto with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ " more | more spam",
+ " spam \x1b[31meggs\x1b[0m | & eggs",
+ "-------------+-------------",
+ " 2 | foo",
+ " | bar"])
+ result = tabulate(table, headers, tablefmt="presto")
+ assert_equal(expected, result)
+
+
+def test_presto_multiline_with_empty_cells():
+ "Output: presto with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ " hdr | data | fold",
+ "-------+----------------+--------",
+ " 1 | |",
+ " 2 | very long data | fold",
+ " | | this"])
+ result = tabulate(table, headers="firstrow", tablefmt="presto")
+ assert_equal(expected, result)
+
+
+def test_presto_multiline_with_empty_cells_headerless():
+ "Output: presto with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ " 0 | |",
+ " 1 | |",
+ " 2 | very long data | fold",
+ " | | this"])
+ result = tabulate(table, tablefmt="presto")
+ assert_equal(expected, result)
+
+
+def test_orgtbl():
+ "Output: orgtbl with headers"
+ expected = '\n'.join(['| strings | numbers |',
+ '|-----------+-----------|',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="orgtbl")
+ assert_equal(expected, result)
+
+
+def test_orgtbl_headerless():
+ "Output: orgtbl without headers"
+ expected = '\n'.join(['| spam | 41.9999 |',
+ '| eggs | 451 |',])
+ result = tabulate(_test_table, tablefmt="orgtbl")
+ assert_equal(expected, result)
+
+def test_psql():
+ "Output: psql with headers"
+ expected = '\n'.join(['+-----------+-----------+',
+ '| strings | numbers |',
+ '|-----------+-----------|',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',
+ '+-----------+-----------+',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="psql")
+ assert_equal(expected, result)
+
+def test_psql_headerless():
+ "Output: psql without headers"
+ expected = '\n'.join(['+------+----------+',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',
+ '+------+----------+',])
+ result = tabulate(_test_table, tablefmt="psql")
+ assert_equal(expected, result)
+
+def test_psql_multiline_headerless():
+ "Output: psql with multiline cells without headers"
+ table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]]
+ expected = "\n".join([
+ "+---------+-----------+",
+ "| foo bar | hello |",
+ "| baz | |",
+ "| bau | |",
+ "| | multiline |",
+ "| | world |",
+ "+---------+-----------+"])
+ result = tabulate(table, stralign="center", tablefmt="psql")
+ assert_equal(expected, result)
+
+
+def test_psql_multiline():
+ "Output: psql with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ "+-------------+-------------+",
+ "| more | more spam |",
+ "| spam \x1b[31meggs\x1b[0m | & eggs |",
+ "|-------------+-------------|",
+ "| 2 | foo |",
+ "| | bar |",
+ "+-------------+-------------+"])
+ result = tabulate(table, headers, tablefmt="psql")
+ assert_equal(expected, result)
+
+
+def test_psql_multiline_with_empty_cells():
+ "Output: psql with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "+-------+----------------+--------+",
+ "| hdr | data | fold |",
+ "|-------+----------------+--------|",
+ "| 1 | | |",
+ "| 2 | very long data | fold |",
+ "| | | this |",
+ "+-------+----------------+--------+"])
+ result = tabulate(table, headers="firstrow", tablefmt="psql")
+ assert_equal(expected, result)
+
+
+def test_psql_multiline_with_empty_cells_headerless():
+ "Output: psql with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "+---+----------------+------+",
+ "| 0 | | |",
+ "| 1 | | |",
+ "| 2 | very long data | fold |",
+ "| | | this |",
+ "+---+----------------+------+"])
+ result = tabulate(table, tablefmt="psql")
+ assert_equal(expected, result)
+
+
+def test_jira():
+ "Output: jira with headers"
+ expected = '\n'.join(['|| strings || numbers ||',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',])
+
+ result = tabulate(_test_table, _test_table_headers, tablefmt="jira")
+ assert_equal(expected, result)
+
+def test_jira_headerless():
+ "Output: jira without headers"
+ expected = '\n'.join(['| spam | 41.9999 |',
+ '| eggs | 451 |',])
+
+ result = tabulate(_test_table, tablefmt="jira")
+ assert_equal(expected, result)
+
+def test_rst():
+ "Output: rst with headers"
+ expected = '\n'.join(['========= =========',
+ 'strings numbers',
+ '========= =========',
+ 'spam 41.9999',
+ 'eggs 451',
+ '========= =========',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="rst")
+ assert_equal(expected, result)
+
+def test_rst_with_empty_values_in_first_column():
+ "Output: rst with dots in first column"
+ test_headers = ['', 'what']
+ test_data = [('', 'spam'), ('', 'eggs')]
+ expected = '\n'.join(['==== ======',
+ '.. what',
+ '==== ======',
+ '.. spam',
+ '.. eggs',
+ '==== ======'])
+ result = tabulate(test_data, test_headers, tablefmt="rst")
+ assert_equal(expected, result)
+
+def test_rst_headerless():
+ "Output: rst without headers"
+ expected = '\n'.join(['==== ========',
+ 'spam 41.9999',
+ 'eggs 451',
+ '==== ========',])
+ result = tabulate(_test_table, tablefmt="rst")
+ assert_equal(expected, result)
+
+def test_rst_multiline():
+ "Output: rst with multiline cells with headers"
+ table = [[2, "foo\nbar"]]
+ headers = ("more\nspam \x1b[31meggs\x1b[0m", "more spam\n& eggs")
+ expected = "\n".join([
+ "=========== ===========",
+ " more more spam",
+ " spam \x1b[31meggs\x1b[0m & eggs",
+ "=========== ===========",
+ " 2 foo",
+ " bar",
+ "=========== ==========="])
+ result = tabulate(table, headers, tablefmt="rst")
+ assert_equal(expected, result)
+
+
+def test_rst_multiline_with_empty_cells():
+ "Output: rst with multiline cells and empty cells with headers"
+ table = [
+ ['hdr', 'data', 'fold'],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "===== ============== ======",
+ " hdr data fold",
+ "===== ============== ======",
+ " 1",
+ " 2 very long data fold",
+ " this",
+ "===== ============== ======"])
+ result = tabulate(table, headers="firstrow", tablefmt="rst")
+ assert_equal(expected, result)
+
+
+def test_rst_multiline_with_empty_cells_headerless():
+ "Output: rst with multiline cells and empty cells without headers"
+ table = [
+ ['0', '', ''],
+ ['1', '', ''],
+ ['2', 'very long data', 'fold\nthis']
+ ]
+ expected = "\n".join([
+ "= ============== ====",
+ "0",
+ "1",
+ "2 very long data fold",
+ " this",
+ "= ============== ===="])
+ result = tabulate(table, tablefmt="rst")
+ assert_equal(expected, result)
+
+
+def test_mediawiki():
+ "Output: mediawiki with headers"
+ expected = '\n'.join(['{| class="wikitable" style="text-align: left;"',
+ '|+ <!-- caption -->',
+ '|-',
+ '! strings !! align="right"| numbers',
+ '|-',
+ '| spam || align="right"| 41.9999',
+ '|-',
+ '| eggs || align="right"| 451',
+ '|}',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="mediawiki")
+ assert_equal(expected, result)
+
+
+def test_mediawiki_headerless():
+ "Output: mediawiki without headers"
+ expected = '\n'.join(['{| class="wikitable" style="text-align: left;"',
+ '|+ <!-- caption -->',
+ '|-',
+ '| spam || align="right"| 41.9999',
+ '|-',
+ '| eggs || align="right"| 451',
+ '|}',])
+ result = tabulate(_test_table, tablefmt="mediawiki")
+ assert_equal(expected, result)
+
+def test_moinmoin():
+ "Output: moinmoin with headers"
+ expected = "\n".join(['|| \'\'\' strings \'\'\' ||<style="text-align: right;"> \'\'\' numbers \'\'\' ||',
+ '|| spam ||<style="text-align: right;"> 41.9999 ||',
+ '|| eggs ||<style="text-align: right;"> 451 ||',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="moinmoin")
+ assert_equal(expected, result)
+
+def test_youtrack():
+ "Output: youtrack with headers"
+ expected = "\n".join(['|| strings || numbers ||',
+ '| spam | 41.9999 |',
+ '| eggs | 451 |',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="youtrack")
+ assert_equal(expected, result)
+
+
+def test_moinmoin_headerless():
+ "Output: moinmoin without headers"
+ expected = "\n".join(['|| spam ||<style="text-align: right;"> 41.9999 ||',
+ '|| eggs ||<style="text-align: right;"> 451 ||',])
+ result = tabulate (_test_table, tablefmt="moinmoin")
+ assert_equal(expected, result)
+
+
+def test_html():
+ "Output: html with headers"
+ expected = '\n'.join([
+ '<table>',
+ '<thead>',
+ '<tr><th>strings </th><th style="text-align: right;"> numbers</th></tr>',
+ '</thead>',
+ '<tbody>',
+ '<tr><td>spam </td><td style="text-align: right;"> 41.9999</td></tr>',
+ '<tr><td>eggs </td><td style="text-align: right;"> 451 </td></tr>',
+ '</tbody>',
+ '</table>',])
+ result = tabulate(_test_table, _test_table_headers, tablefmt="html")
+ assert_equal(expected, result)
+
+
+def test_html_headerless():
+ "Output: html without headers"
+ expected = '\n'.join([
+ '<table>',
+ '<tbody>',
+ '<tr><td>spam</td><td style="text-align: right;"> 41.9999</td></tr>',
+ '<tr><td>eggs</td><td style="text-align: right;">451 </td></tr>',
+ '</tbody>',
+ '</table>',])
+ result = tabulate(_test_table, tablefmt="html")
+ assert_equal(expected, result)
+
+
+def test_latex():
+ "Output: latex with headers and replaced characters"
+ raw_test_table_headers = list(_test_table_headers)
+ raw_test_table_headers[-1] += " ($N_0$)"
+ result = tabulate(_test_table, raw_test_table_headers, tablefmt="latex")
+ expected = "\n".join([r"\begin{tabular}{lr}",
+ r"\hline",
+ r" strings & numbers (\$N\_0\$) \\",
+ r"\hline",
+ r" spam & 41.9999 \\",
+ r" eggs & 451 \\",
+ r"\hline",
+ r"\end{tabular}"])
+ assert_equal(expected, result)
+
+def test_latex_raw():
+ "Output: raw latex with headers"
+ raw_test_table_headers = list(_test_table_headers)
+ raw_test_table_headers[-1] += " ($N_0$)"
+ raw_test_table = list(map(list,_test_table))
+ raw_test_table[0][0] += "$_1$"
+ raw_test_table[1][0] = "\\emph{" + raw_test_table[1][0] + "}"
+ print(raw_test_table)
+ result = tabulate(raw_test_table, raw_test_table_headers, tablefmt="latex_raw")
+ expected = "\n".join([r"\begin{tabular}{lr}",
+ r"\hline",
+ r" strings & numbers ($N_0$) \\",
+ r"\hline",
+ r" spam$_1$ & 41.9999 \\",
+ r" \emph{eggs} & 451 \\",
+ r"\hline",
+ r"\end{tabular}"])
+ assert_equal(expected, result)
+
+def test_latex_headerless():
+ "Output: latex without headers"
+ result = tabulate(_test_table, tablefmt="latex")
+ expected = "\n".join([r"\begin{tabular}{lr}",
+ r"\hline",
+ r" spam & 41.9999 \\",
+ r" eggs & 451 \\",
+ r"\hline",
+ r"\end{tabular}"])
+ assert_equal(expected, result)
+
+def test_latex_booktabs():
+ "Output: latex with headers, using the booktabs format"
+ result = tabulate(_test_table, _test_table_headers, tablefmt="latex_booktabs")
+ expected = "\n".join([r"\begin{tabular}{lr}",
+ r"\toprule",
+ r" strings & numbers \\",
+ r"\midrule",
+ r" spam & 41.9999 \\",
+ r" eggs & 451 \\",
+ r"\bottomrule",
+ r"\end{tabular}"])
+ assert_equal(expected, result)
+
+
+def test_latex_booktabs_headerless():
+ "Output: latex without headers, using the booktabs format"
+ result = tabulate(_test_table, tablefmt="latex_booktabs")
+ expected = "\n".join([r"\begin{tabular}{lr}",
+ r"\toprule",
+ r" spam & 41.9999 \\",
+ r" eggs & 451 \\",
+ r"\bottomrule",
+ r"\end{tabular}"])
+ assert_equal(expected, result)
+
+
+def test_textile():
+ "Output: textile without header"
+ result = tabulate(_test_table, tablefmt="textile")
+ expected = """\
+|<. spam |>. 41.9999 |
+|<. eggs |>. 451 |"""
+
+ assert_equal(expected, result)
+
+
+def test_textile_with_header():
+ "Output: textile with header"
+ result = tabulate(_test_table, ['strings', 'numbers'], tablefmt="textile")
+ expected = """\
+|_. strings |_. numbers |
+|<. spam |>. 41.9999 |
+|<. eggs |>. 451 |"""
+
+ assert_equal(expected, result)
+
+
+def test_textile_with_center_align():
+ "Output: textile with center align"
+ result = tabulate(_test_table, tablefmt="textile", stralign='center')
+ expected = """\
+|=. spam |>. 41.9999 |
+|=. eggs |>. 451 |"""
+
+ assert_equal(expected, result)
+
+
+def test_no_data():
+ "Output: table with no data"
+ expected = "\n".join(['strings numbers',
+ '--------- ---------'])
+ result = tabulate(None, _test_table_headers, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_empty_data():
+ "Output: table with empty data"
+ expected = "\n".join(['strings numbers',
+ '--------- ---------'])
+ result = tabulate([], _test_table_headers, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_no_data_without_headers():
+ "Output: table with no data and no headers"
+ expected = ""
+ result = tabulate(None, tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_empty_data_without_headers():
+ "Output: table with empty data and no headers"
+ expected = ""
+ result = tabulate([], tablefmt="simple")
+ assert_equal(expected, result)
+
+
+def test_floatfmt():
+ "Output: floating point format"
+ result = tabulate([['1.23456789'],[1.0]], floatfmt=".3f", tablefmt="plain")
+ expected = '1.235\n1.000'
+ assert_equal(expected, result)
+
+
+def test_floatfmt_multi():
+ "Output: floating point format different for each column"
+ result = tabulate([[0.12345, 0.12345, 0.12345]], floatfmt=(".1f", ".3f"), tablefmt="plain")
+ expected = '0.1 0.123 0.12345'
+ assert_equal(expected, result)
+
+def test_float_conversions():
+ "Output: float format parsed"
+ test_headers = ["str", "bad_float", "just_float", 'with_inf', 'with_nan', 'neg_inf']
+ test_table = [
+ ["spam", 41.9999, "123.345", '12.2', 'nan', '0.123123'],
+ ["eggs", "451.0", 66.2222, 'inf', 123.1234, '-inf'],
+ ["asd", "437e6548", 1.234e2, float('inf'), float('nan'), 0.22e23]
+ ]
+ result = tabulate(test_table, test_headers, tablefmt="grid")
+ expected = "\n".join([
+ '+-------+-------------+--------------+------------+------------+-------------+',
+ '| str | bad_float | just_float | with_inf | with_nan | neg_inf |',
+ '+=======+=============+==============+============+============+=============+',
+ '| spam | 41.9999 | 123.345 | 12.2 | nan | 0.123123 |',
+ '+-------+-------------+--------------+------------+------------+-------------+',
+ '| eggs | 451.0 | 66.2222 | inf | 123.123 | -inf |',
+ '+-------+-------------+--------------+------------+------------+-------------+',
+ '| asd | 437e6548 | 123.4 | inf | nan | 2.2e+22 |',
+ '+-------+-------------+--------------+------------+------------+-------------+'
+ ])
+ assert_equal(expected, result)
+
+def test_missingval():
+ "Output: substitution of missing values"
+ result = tabulate([['Alice', 10],['Bob', None]], missingval="n/a", tablefmt="plain")
+ expected = 'Alice 10\nBob n/a'
+ assert_equal(expected, result)
+
+
+def test_missingval_multi():
+ "Output: substitution of missing values with different values per column"
+ result = tabulate([["Alice", "Bob", "Charlie"], [None, None, None]],
+ missingval=("n/a", "?"), tablefmt="plain")
+ expected = 'Alice Bob Charlie\nn/a ?'
+ assert_equal(expected, result)
+
+
+def test_column_alignment():
+ "Output: custom alignment for text and numbers"
+ expected = '\n'.join(['----- ---',
+ 'Alice 1',
+ ' Bob 333',
+ '----- ---',])
+ result = tabulate([['Alice', 1],['Bob', 333]], stralign="right", numalign="center")
+ assert_equal(expected, result)
+
+
+def test_unaligned_separated():
+ "Output: non-aligned data columns"
+ expected = '\n'.join(['name|score',
+ 'Alice|1',
+ 'Bob|333'])
+ fmt = simple_separated_format("|")
+ result = tabulate([['Alice', 1],['Bob', 333]],
+ ["name", "score"],
+ tablefmt=fmt, stralign=None, numalign=None)
+ assert_equal(expected, result)
+
+
+def test_pandas_with_index():
+ "Output: a pandas Dataframe with an index"
+ try:
+ import pandas
+ df = pandas.DataFrame([["one",1],["two",None]],
+ columns=["string","number"],
+ index=["a","b"])
+ expected = "\n".join(
+ [' string number',
+ '-- -------- --------',
+ 'a one 1',
+ 'b two nan'])
+ result = tabulate(df, headers="keys")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas_with_index is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_pandas_without_index():
+ "Output: a pandas Dataframe without an index"
+ try:
+ import pandas
+ df = pandas.DataFrame([["one",1],["two",None]],
+ columns=["string","number"],
+ index=["a","b"])
+ expected = "\n".join(
+ ['string number',
+ '-------- --------',
+ 'one 1',
+ 'two nan'])
+ result = tabulate(df, headers="keys", showindex=False)
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas_without_index is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_pandas_rst_with_index():
+ "Output: a pandas Dataframe with an index in ReStructuredText format"
+ try:
+ import pandas
+ df = pandas.DataFrame([["one", 1], ["two", None]],
+ columns=["string", "number"],
+ index=["a", "b"])
+ expected = "\n".join(
+ ['==== ======== ========',
+ '.. string number',
+ '==== ======== ========',
+ 'a one 1',
+ 'b two nan',
+ '==== ======== ========'])
+ result = tabulate(df, tablefmt="rst", headers="keys")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas_rst_with_index is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_pandas_rst_with_named_index():
+ "Output: a pandas Dataframe with a named index in ReStructuredText format"
+ try:
+ import pandas
+ index = pandas.Index(["a", "b"], name='index')
+ df = pandas.DataFrame([["one", 1], ["two", None]],
+ columns=["string", "number"],
+ index=index)
+ expected = "\n".join(
+ ['======= ======== ========',
+ 'index string number',
+ '======= ======== ========',
+ 'a one 1',
+ 'b two nan',
+ '======= ======== ========'])
+ result = tabulate(df, tablefmt="rst", headers="keys")
+ assert_equal(expected, result)
+ except ImportError:
+ print("test_pandas_rst_with_index is skipped")
+ raise SkipTest() # this test is optional
+
+
+def test_dict_like_with_index():
+ "Output: a table with a running index"
+ dd = {"b": range(101,104)}
+ expected = "\n".join([
+ ' b',
+ '-- ---',
+ ' 0 101',
+ ' 1 102',
+ ' 2 103'])
+ result = tabulate(dd, "keys", showindex=True)
+ assert_equal(result, expected)
+
+
+def test_list_of_lists_with_index():
+ "Output: a table with a running index"
+ dd = zip(*[range(3), range(101,104)])
+ # keys' order (hence columns' order) is not deterministic in Python 3
+ # => we have to consider both possible results as valid
+ expected = "\n".join([
+ ' a b',
+ '-- --- ---',
+ ' 0 0 101',
+ ' 1 1 102',
+ ' 2 2 103'])
+ result = tabulate(dd, headers=["a","b"], showindex=True)
+ assert_equal(result, expected)
+
+def test_list_of_lists_with_supplied_index():
+ "Output: a table with a supplied index"
+ dd = zip(*[list(range(3)), list(range(101,104))])
+ expected = "\n".join([
+ ' a b',
+ '-- --- ---',
+ ' 1 0 101',
+ ' 2 1 102',
+ ' 3 2 103'])
+ result = tabulate(dd, headers=["a","b"], showindex=[1,2,3])
+ assert_equal(result, expected)
+ # TODO: make it a separate test case
+ # the index must be as long as the number of rows
+ assert_raises(ValueError, lambda: tabulate(dd, headers=["a","b"], showindex=[1,2]))
+
+
+def test_list_of_lists_with_index_firstrow():
+ "Output: a table with a running index and header='firstrow'"
+ dd = zip(*[["a"]+list(range(3)), ["b"]+list(range(101,104))])
+ expected = "\n".join([
+ ' a b',
+ '-- --- ---',
+ ' 0 0 101',
+ ' 1 1 102',
+ ' 2 2 103'])
+ result = tabulate(dd, headers="firstrow", showindex=True)
+ assert_equal(result, expected)
+ # TODO: make it a separate test case
+ # the index must be as long as the number of rows
+ assert_raises(ValueError, lambda: tabulate(dd, headers="firstrow", showindex=[1,2]))
+
+
+def test_disable_numparse_default():
+ "Output: Default table output with number parsing and alignment"
+ expected = "\n".join(['strings numbers',
+ '--------- ---------',
+ 'spam 41.9999',
+ 'eggs 451',])
+ result = tabulate(_test_table, _test_table_headers)
+ assert_equal(expected, result)
+ result = tabulate(_test_table, _test_table_headers, disable_numparse=False)
+ assert_equal(expected, result)
+
+def test_disable_numparse_true():
+ "Output: Default table output, but without number parsing and alignment"
+ expected = "\n".join(['strings numbers',
+ '--------- ---------',
+ 'spam 41.9999',
+ 'eggs 451.0',])
+ result = tabulate(_test_table, _test_table_headers, disable_numparse=True)
+ assert_equal(expected, result)
+
+def test_disable_numparse_list():
+ "Output: Default table output, but with number parsing selectively disabled"
+ table_headers = ['h1', 'h2', 'h3']
+ test_table = [['foo', 'bar', '42992e1']]
+ expected = "\n".join(['h1 h2 h3',
+ '---- ---- -------',
+ 'foo bar 42992e1',])
+ result = tabulate(test_table, table_headers, disable_numparse=[2])
+ assert_equal(expected, result)
+
+ expected = "\n".join(['h1 h2 h3',
+ '---- ---- ------',
+ 'foo bar 429920',])
+ result = tabulate(test_table, table_headers, disable_numparse=[0, 1])
+ assert_equal(expected, result)
+
+def test_preserve_whitespace():
+ "Output: Default table output, but with preserved leading whitespace."
+ tabulate_module.PRESERVE_WHITESPACE = True
+ table_headers = ['h1', 'h2', 'h3']
+ test_table = [[' foo', ' bar ', 'foo']]
+ expected = "\n".join(['h1 h2 h3',
+ '----- ------- ----',
+ ' foo bar foo'])
+ result = tabulate(test_table, table_headers)
+ assert_equal(expected, result)
+
+ tabulate_module.PRESERVE_WHITESPACE = False
+ table_headers = ['h1', 'h2', 'h3']
+ test_table = [[' foo', ' bar ', 'foo']]
+ expected = "\n".join(['h1 h2 h3',
+ '---- ---- ----',
+ 'foo bar foo'])
+ result = tabulate(test_table, table_headers)
+ assert_equal(expected, result)
diff --git a/test/test_regression.py b/test/test_regression.py
new file mode 100644
index 0000000..af22734
--- /dev/null
+++ b/test/test_regression.py
@@ -0,0 +1,333 @@
+# -*- coding: utf-8 -*-
+
+"""Regression tests."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from tabulate import tabulate, _text_type, _long_type
+from common import assert_equal, assert_in, SkipTest
+
+
+def test_ansi_color_in_table_cells():
+ "Regression: ANSI color in table cells (issue #5)."
+ colortable = [('test', '\x1b[31mtest\x1b[0m', '\x1b[32mtest\x1b[0m')]
+ colorlessheaders = ('test', 'test', 'test')
+ formatted = tabulate(colortable, colorlessheaders, 'pipe')
+ expected = "\n".join(['| test | test | test |',
+ '|:-------|:-------|:-------|',
+ '| test | \x1b[31mtest\x1b[0m | \x1b[32mtest\x1b[0m |'])
+ print("expected: %r\n\ngot: %r\n" % (expected, formatted))
+ assert_equal(expected, formatted)
+
+
+def test_alignment_of_colored_cells():
+ "Regression: Align ANSI-colored values as if they were colorless."
+ colortable = [('test', 42, '\x1b[31m42\x1b[0m'), ('test', 101, '\x1b[32m101\x1b[0m')]
+ colorheaders = ('test', '\x1b[34mtest\x1b[0m', 'test')
+ formatted = tabulate(colortable, colorheaders, 'grid')
+ expected = '\n'.join(['+--------+--------+--------+',
+ '| test | \x1b[34mtest\x1b[0m | test |',
+ '+========+========+========+',
+ '| test | 42 | \x1b[31m42\x1b[0m |',
+ '+--------+--------+--------+',
+ '| test | 101 | \x1b[32m101\x1b[0m |',
+ '+--------+--------+--------+'])
+ print("expected: %r\n\ngot: %r\n" % (expected, formatted))
+ assert_equal(expected, formatted)
+
+
+def test_iter_of_iters_with_headers():
+ "Regression: Generator of generators with a gen. of headers (issue #9)."
+
+ def mk_iter_of_iters():
+ def mk_iter():
+ for i in range(3):
+ yield i
+ for r in range(3):
+ yield mk_iter()
+
+ def mk_headers():
+ for h in ["a", "b", "c"]:
+ yield h
+
+ formatted = tabulate(mk_iter_of_iters(), headers=mk_headers())
+ expected = '\n'.join([' a b c',
+ '--- --- ---',
+ ' 0 1 2',
+ ' 0 1 2',
+ ' 0 1 2'])
+ print("expected: %r\n\ngot: %r\n" % (expected, formatted))
+ assert_equal(expected, formatted)
+
+
+def test_datetime_values():
+ "Regression: datetime, date, and time values in cells (issue #10)."
+ import datetime
+ dt = datetime.datetime(1991,2,19,17,35,26)
+ d = datetime.date(1991,2,19)
+ t = datetime.time(17,35,26)
+ formatted = tabulate([[dt, d, t]])
+ expected = '\n'.join(['------------------- ---------- --------',
+ '1991-02-19 17:35:26 1991-02-19 17:35:26',
+ '------------------- ---------- --------'])
+ print("expected: %r\n\ngot: %r\n" % (expected, formatted))
+ assert_equal(expected, formatted)
+
+
+def test_simple_separated_format():
+ "Regression: simple_separated_format() accepts any separator (issue #12)"
+ from tabulate import simple_separated_format
+ fmt = simple_separated_format("!")
+ expected = 'spam!eggs'
+ formatted = tabulate([["spam", "eggs"]], tablefmt=fmt)
+ print("expected: %r\n\ngot: %r\n" % (expected, formatted))
+ assert_equal(expected, formatted)
+
+
+def py3test_require_py3():
+ "Regression: py33 tests should actually use Python 3 (issue #13)"
+ from platform import python_version_tuple
+ print("Expected Python version: 3.x.x")
+ print("Python version used for tests: %s.%s.%s" % python_version_tuple())
+ assert_equal(python_version_tuple()[0], '3')
+
+
+def test_simple_separated_format_with_headers():
+ "Regression: simple_separated_format() on tables with headers (issue #15)"
+ from tabulate import simple_separated_format
+ expected = ' a| b\n 1| 2'
+ formatted = tabulate([[1,2]], headers=["a", "b"], tablefmt=simple_separated_format("|"))
+ assert_equal(expected, formatted)
+
+
+def test_column_type_of_bytestring_columns():
+ "Regression: column type for columns of bytestrings (issue #16)"
+ from tabulate import _column_type, _binary_type
+ result = _column_type([b"foo", b"bar"])
+ expected = _binary_type
+ assert_equal(result, expected)
+
+
+def test_numeric_column_headers():
+ "Regression: numbers as column headers (issue #22)"
+ result = tabulate([[1],[2]], [42])
+ expected = ' 42\n----\n 1\n 2'
+ assert_equal(result, expected)
+
+ lod = [dict((p,i) for p in range(5)) for i in range(5)]
+ result = tabulate(lod, "keys")
+ expected = "\n".join([
+ " 0 1 2 3 4",
+ "--- --- --- --- ---",
+ " 0 0 0 0 0",
+ " 1 1 1 1 1",
+ " 2 2 2 2 2",
+ " 3 3 3 3 3",
+ " 4 4 4 4 4",])
+ assert_equal(result, expected)
+
+
+def test_88_256_ANSI_color_codes():
+ "Regression: color codes for terminals with 88/256 colors (issue #26)"
+ colortable = [('\x1b[48;5;196mred\x1b[49m',
+ '\x1b[38;5;196mred\x1b[39m')]
+ colorlessheaders = ('background', 'foreground')
+ formatted = tabulate(colortable, colorlessheaders, 'pipe')
+ expected = "\n".join([
+ '| background | foreground |',
+ '|:-------------|:-------------|',
+ '| \x1b[48;5;196mred\x1b[49m | \x1b[38;5;196mred\x1b[39m |'])
+ print("expected: %r\n\ngot: %r\n" % (expected, formatted))
+ assert_equal(expected, formatted)
+
+
+def test_column_with_mixed_value_types():
+ "Regression: mixed value types in the same column (issue #31)"
+ expected = '\n'.join([
+ '-----',
+ '',
+ 'a',
+ 'я',
+ '0',
+ 'False',
+ '-----',
+ ])
+ data = [[None], ['a'], ['\u044f'], [0], [False]]
+ table = tabulate(data)
+ assert_equal(table, expected)
+
+
+def test_latex_escape_special_chars():
+ "Regression: escape special characters in LaTeX output (issue #32)"
+ expected = "\n".join([
+ r'\begin{tabular}{l}',
+ r'\hline',
+ r' foo\^{}bar \\',
+ r'\hline',
+ r' \&\%\^{}\_\$\#\{\}\ensuremath{<}\ensuremath{>}\textasciitilde{} \\',
+ r'\hline',
+ r'\end{tabular}'])
+ result = tabulate([["&%^_$#{}<>~"]], ["foo^bar"], tablefmt="latex")
+ assert_equal(result, expected)
+
+
+def test_isconvertible_on_set_values():
+ "Regression: don't fail with TypeError on set values (issue #35)"
+ expected_py2 = "\n".join([
+ 'a b',
+ '--- -------',
+ 'Foo set([])',])
+ expected_py3 = "\n".join([
+ 'a b',
+ '--- -----',
+ 'Foo set()',])
+ result = tabulate([["Foo",set()]], headers=["a","b"])
+ assert_in(result, [expected_py2, expected_py3])
+
+
+def test_ansi_color_for_decimal_numbers():
+ "Regression: ANSI colors for decimal numbers (issue #36)"
+ table = [["Magenta", "\033[95m" + "1.1" + "\033[0m"]]
+ expected = "\n".join([
+ '------- ---',
+ 'Magenta \x1b[95m1.1\x1b[0m',
+ '------- ---'])
+ result = tabulate(table)
+ assert_equal(result, expected)
+
+
+def test_alignment_of_decimal_numbers_with_ansi_color():
+ "Regression: alignment for decimal numbers with ANSI color (issue #42)"
+ v1 = "\033[95m" + "12.34" + "\033[0m"
+ v2 = "\033[95m" + "1.23456" + "\033[0m"
+ table = [[v1], [v2]]
+ expected = "\n".join([
+ '\x1b[95m12.34\x1b[0m',
+ ' \x1b[95m1.23456\x1b[0m'])
+ result = tabulate(table, tablefmt="plain")
+ assert_equal(result, expected)
+
+
+def test_long_integers():
+ "Regression: long integers should be printed as integers (issue #48)"
+ table = [[18446744073709551614]]
+ result = tabulate(table, tablefmt="plain")
+ expected = "18446744073709551614"
+ assert_equal(result, expected)
+
+
+def test_colorclass_colors():
+ "Regression: ANSI colors in a unicode/str subclass (issue #49)"
+ try:
+ import colorclass
+ s = colorclass.Color("{magenta}3.14{/magenta}")
+ result = tabulate([[s]], tablefmt="plain")
+ expected = "\x1b[35m3.14\x1b[39m"
+ assert_equal(result, expected)
+ except ImportError:
+ class textclass(_text_type):
+ pass
+ s = textclass("\x1b[35m3.14\x1b[39m")
+ result = tabulate([[s]], tablefmt="plain")
+ expected = "\x1b[35m3.14\x1b[39m"
+ assert_equal(result, expected)
+
+
+def test_mix_normal_and_wide_characters():
+ "Regression: wide characters in a grid format (issue #51)"
+ try:
+ import wcwidth
+ ru_text = '\u043f\u0440\u0438\u0432\u0435\u0442'
+ cn_text = '\u4f60\u597d'
+ result = tabulate([[ru_text], [cn_text]], tablefmt="grid")
+ expected = "\n".join([
+ '+--------+',
+ '| \u043f\u0440\u0438\u0432\u0435\u0442 |',
+ '+--------+',
+ '| \u4f60\u597d |',
+ '+--------+'])
+ assert_equal(result, expected)
+ except ImportError:
+ print("test_mix_normal_and_wide_characters is skipped (requires wcwidth lib)")
+ raise SkipTest()
+
+
+def test_align_long_integers():
+ "Regression: long integers should be aligned as integers (issue #61)"
+ table = [[_long_type(1)], [_long_type(234)]]
+ result = tabulate(table, tablefmt="plain")
+ expected = "\n".join([" 1",
+ "234"])
+ assert_equal(result, expected)
+
+
+def test_numpy_array_as_headers():
+ "Regression: NumPy array used as headers (issue #62)"
+ try:
+ import numpy as np
+ headers = np.array(["foo", "bar"])
+ result = tabulate([], headers, tablefmt="plain")
+ expected = "foo bar"
+ assert_equal(result, expected)
+ except ImportError:
+ raise SkipTest()
+
+
+def test_boolean_columns():
+ "Regression: recognize boolean columns (issue #64)"
+ xortable = [[False, True], [True, False]]
+ expected = "\n".join(["False True",
+ "True False"])
+ result = tabulate(xortable, tablefmt="plain")
+ assert_equal(result, expected)
+
+
+def test_ansi_color_bold_and_fgcolor():
+ "Regression: set ANSI color and bold face together (issue #65)"
+ table = [["1", "2", "3"], ["4", "\x1b[1;31m5\x1b[1;m", "6"], ["7", "8", "9"]]
+ result = tabulate(table, tablefmt="grid")
+ expected = "\n".join([
+ u'+---+---+---+',
+ u'| 1 | 2 | 3 |',
+ u'+---+---+---+',
+ u'| 4 | \x1b[1;31m5\x1b[1;m | 6 |',
+ u'+---+---+---+',
+ u'| 7 | 8 | 9 |',
+ u'+---+---+---+'])
+ assert_equal(result, expected)
+
+
+def test_empty_table_with_keys_as_header():
+ "Regression: headers='keys' on an empty table (issue #81)"
+ result = tabulate([], headers="keys")
+ expected = ""
+ assert_equal(result, expected)
+
+
+def test_escape_empty_cell_in_first_column_in_rst():
+ "Regression: escape empty cells of the first column in RST format (issue #82)"
+ table = [["foo", 1], ["", 2], ["bar", 3]]
+ headers = ["", "val"]
+ expected = "\n".join([
+ u"==== =====",
+ u".. val",
+ u"==== =====",
+ u"foo 1",
+ u".. 2",
+ u"bar 3",
+ u"==== ====="])
+ result = tabulate(table, headers, tablefmt="rst")
+ assert_equal(result, expected)
+
+
+def test_ragged_rows():
+ "Regression: allow rows with different number of columns (issue #85)"
+ table = [[1,2,3], [1,2], [1,2,3,4]]
+ expected = "\n".join([
+ u"- - - -",
+ u"1 2 3",
+ u"1 2",
+ u"1 2 3 4",
+ u"- - - -"])
+ result = tabulate(table)
+ assert_equal(result, expected)