Kaynağa Gözat

Add tabulate module to desktop/core/ext-py

Harshg999 4 yıl önce
ebeveyn
işleme
789f1fd2a8

+ 40 - 0
desktop/core/ext-py/tabulate-0.8.9/CHANGELOG

@@ -0,0 +1,40 @@
+- 0.8.9: Bug fix. Revert support of decimal separators.
+- 0.8.8: Python 3.9 support, 3.10 ready.
+  New formats: ``unsafehtml``, ``latex_longtable``, ``fancy_outline``.
+  Support lists of UserDicts as input.
+  Support hyperlinks in terminal output.
+  Improve testing on systems with proxies.
+  Migrate to pytest.
+  Various bug fixes and improvements.
+- 0.8.7: Bug fixes. New format: `pretty`. HTML escaping.
+- 0.8.6: Bug fixes. Stop supporting Python 3.3, 3.4.
+- 0.8.5: Fix broken Windows package. Minor documentation updates.
+- 0.8.4: Bug fixes.
+- 0.8.3: New formats: `github`. Custom colum alignment. Bug fixes.
+- 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``.

+ 20 - 0
desktop/core/ext-py/tabulate-0.8.9/LICENSE

@@ -0,0 +1,20 @@
+Copyright (c) 2011-2020 Sergey Astanin and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 6 - 0
desktop/core/ext-py/tabulate-0.8.9/MANIFEST.in

@@ -0,0 +1,6 @@
+include LICENSE
+include README
+include README.md
+include CHANGELOG
+include test/common.py
+include benchmark.py

+ 777 - 0
desktop/core/ext-py/tabulate-0.8.9/PKG-INFO

@@ -0,0 +1,777 @@
+Metadata-Version: 2.1
+Name: tabulate
+Version: 0.8.9
+Summary: Pretty-print tabular data
+Home-page: https://github.com/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
+        
+        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"
+        -   "github"
+        -   "grid"
+        -   "fancy\_grid"
+        -   "pipe"
+        -   "orgtbl"
+        -   "jira"
+        -   "presto"
+        -   "pretty"
+        -   "psql"
+        -   "rst"
+        -   "mediawiki"
+        -   "moinmoin"
+        -   "youtrack"
+        -   "html"
+        -   "unsafehtml"
+        -   "latex"
+        -   "latex\_raw"
+        -   "latex\_booktabs"
+        -   "latex\_longtable"
+        -   "textile"
+        -   "tsv"
+        
+        `plain` tables do not use any pseudo-graphics to draw lines:
+        
+            >>> table = [["spam",42],["eggs",451],["bacon",0]]
+            >>> headers = ["item", "qty"]
+            >>> print(tabulate(table, headers, tablefmt="plain"))
+            item      qty
+            spam       42
+            eggs      451
+            bacon       0
+        
+        `simple` is the default format (the default may change in future
+        versions). It corresponds to `simple_tables` in [Pandoc Markdown
+        extensions](http://johnmacfarlane.net/pandoc/README.html#tables):
+        
+            >>> print(tabulate(table, headers, tablefmt="simple"))
+            item      qty
+            ------  -----
+            spam       42
+            eggs      451
+            bacon       0
+        
+        `github` follows the conventions of GitHub flavored Markdown. It
+        corresponds to the `pipe` format without alignment colons:
+        
+            >>> print(tabulate(table, headers, tablefmt="github"))
+            | item   | qty   |
+            |--------|-------|
+            | spam   | 42    |
+            | eggs   | 451   |
+            | bacon  | 0     |
+        
+        `grid` is like tables formatted by Emacs'
+        [table.el](http://table.sourceforge.net/) package. It corresponds to
+        `grid_tables` in Pandoc Markdown extensions:
+        
+            >>> 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(table, headers, tablefmt="presto"))
+             item   |   qty
+            --------+-------
+             spam   |    42
+             eggs   |   451
+             bacon  |     0
+        
+        `pretty` attempts to be close to the format emitted by the PrettyTables
+        library:
+        
+            >>> print(tabulate(table, headers, tablefmt="pretty"))
+            +-------+-----+
+            | item  | qty |
+            +-------+-----+
+            | spam  | 42  |
+            | eggs  | 451 |
+            | bacon |  0  |
+            +-------+-----+
+        
+        `psql` is like tables formatted by Postgres' psql cli:
+        
+            >>> print(tabulate(table, headers, tablefmt="psql"))
+            +--------+-------+
+            | item   |   qty |
+            |--------+-------|
+            | spam   |    42 |
+            | eggs   |   451 |
+            | bacon  |     0 |
+            +--------+-------+
+        
+        `pipe` follows the conventions of [PHP Markdown
+        Extra](http://michelf.ca/projects/php-markdown/extra/#table) extension.
+        It corresponds to `pipe_tables` in Pandoc. This format uses colons to
+        indicate column alignment:
+        
+            >>> print(tabulate(table, headers, tablefmt="pipe"))
+            | item   |   qty |
+            |:-------|------:|
+            | spam   |    42 |
+            | eggs   |   451 |
+            | bacon  |     0 |
+        
+        `orgtbl` follows the conventions of Emacs
+        [org-mode](http://orgmode.org/manual/Tables.html), and is editable also
+        in the minor orgtbl-mode. Hence its name:
+        
+            >>> 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](http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables)
+        format:
+        
+            >>> print(tabulate(table, headers, tablefmt="rst"))
+            ======  =====
+            item      qty
+            ======  =====
+            spam       42
+            eggs      451
+            bacon       0
+            ======  =====
+        
+        `mediawiki` format produces a table markup used in
+        [Wikipedia](http://www.mediawiki.org/wiki/Help:Tables) and on other
+        MediaWiki-based sites:
+        
+            >>> 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](https://moinmo.in/) wikis:
+        
+            >>> print(tabulate(table, headers, tablefmt="moinmoin"))
+            || ''' item   ''' || ''' quantity   ''' ||
+            ||  spam    ||  41.999      ||
+            ||  eggs    ||  451         ||
+            ||  bacon   ||              ||
+        
+        `youtrack` format produces a table markup used in Youtrack tickets:
+        
+            >>> print(tabulate(table, headers, tablefmt="youtrack"))
+            ||  item    ||  quantity   ||
+            |   spam    |  41.999      |
+            |   eggs    |  451         |
+            |   bacon   |              |
+        
+        `textile` format produces a table markup used in
+        [Textile](http://redcloth.org/hobix.com/textile/) format:
+        
+            >>> print(tabulate(table, headers, tablefmt="textile"))
+            |_.  item   |_.   qty |
+            |<. spam    |>.    42 |
+            |<. eggs    |>.   451 |
+            |<. bacon   |>.     0 |
+        
+        `html` produces standard HTML markup as an html.escape'd str
+        with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML
+        and a .str property so that the raw HTML remains accessible.
+        `unsafehtml` table format can be used if an unescaped HTML is required:
+        
+            >>> 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.
+        
+        `latex_longtable` creates a table that can stretch along multiple pages,
+        using the `longtable` package.
+        
+        ### Column alignment
+        
+        `tabulate` is smart about column alignment. It detects columns which
+        contain only numbers, and aligns them by a decimal point (or flushes
+        them to the right if they appear to be integers). Text columns are
+        flushed to the left.
+        
+        You can override the default alignment with `numalign` and `stralign`
+        named arguments. Possible column alignments are: `right`, `center`,
+        `left`, `decimal` (only for numbers), and `None` (to disable alignment).
+        
+        Aligning by a decimal point works best when you need to compare numbers
+        at a glance:
+        
+            >>> 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
+            ----  ----
+        
+        
+        To disable this feature use `disable_numparse=True`.
+        
+            >>> print(tabulate.tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True))
+            ----  ----
+            Ver1  18.0
+            Ver2  19.2
+            ----  ----
+        
+        
+        ### Custom column alignment
+        
+        `tabulate` allows a custom column alignment to override the above. The
+        `colalign` argument can be a list or a tuple of `stralign` named
+        arguments. Possible column alignments are: `right`, `center`, `left`,
+        `decimal` (only for numbers), and `None` (to disable alignment).
+        Omitting an alignment uses the default. For example:
+        
+            >>> print(tabulate([["one", "two"], ["three", "four"]], colalign=("right",))
+            -----  ----
+              one  two
+            three  four
+            -----  ----
+        
+        ### Number formatting
+        
+        `tabulate` allows to define custom number formatting applied to all
+        columns of decimal numbers. Use `floatfmt` named argument:
+        
+            >>> 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   |
+        
+        `pretty` tables:
+        
+            >>> print(tabulate(table, headers, tablefmt="pretty"))
+            +------+-----+
+            | 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, github, grid, fancy_grid, pipe,
+                                      orgtbl, rst, mediawiki, html, latex, latex_raw,
+                                      latex_booktabs, latex_longtable, tsv
+                                      (default: simple)
+        
+        Performance considerations
+        --------------------------
+        
+        Such features as decimal point alignment and trying to parse everything
+        as a number imply that `tabulate`:
+        
+        -   has to "guess" how to print a particular tabular data type
+        -   needs to keep the entire table in-memory
+        -   has to "transpose" the table twice
+        -   does much more work than it may appear
+        
+        It may not be suitable for serializing really big tables (but who's
+        going to do that, anyway?) or printing tables in performance sensitive
+        applications. `tabulate` is about two orders of magnitude slower than
+        simply joining lists of values with a tab, comma, or other separator.
+        
+        At the same time, `tabulate` is comparable to other table
+        pretty-printers. Given a 10x10 table (a list of lists) of mixed text and
+        numeric data, `tabulate` appears to be slower than `asciitable`, and
+        faster than `PrettyTable` and `texttable` The following mini-benchmark
+        was run in Python 3.8.3 in Windows 10 x64:
+        
+            =================================  ==========  ===========
+            Table formatter                      time, μs    rel. time
+            =================================  ==========  ===========
+            csv to StringIO                          12.5          1.0
+            join with tabs and newlines              15.6          1.3
+            asciitable (0.8.0)                      191.4         15.4
+            tabulate (0.8.9)                        472.8         38.0
+            tabulate (0.8.9, WIDE_CHARS_MODE)       789.6         63.4
+            PrettyTable (0.7.2)                     879.1         70.6
+            texttable (1.6.2)                      1352.2        108.6
+            =================================  ==========  ===========
+        
+        
+        Version history
+        ---------------
+        
+        The full version history can be found at the [changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG).
+        
+        How to contribute
+        -----------------
+        
+        Contributions should include tests and an explanation for the changes
+        they propose. Documentation (examples, docstrings, README.md) should be
+        updated accordingly.
+        
+        This project uses [pytest](https://docs.pytest.org/) testing
+        framework and [tox](https://tox.readthedocs.io/) to automate testing in
+        different environments. Add tests to one of the files in the `test/`
+        folder.
+        
+        To run tests on all supported Python versions, make sure all Python
+        interpreters, `pytest` and `tox` are installed, then run `tox` in the root
+        of the project source tree.
+        
+        On Linux `tox` expects to find executables like `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 environments, use `-e` option. For example, to
+        test only against Python 2.7 and Python 3.8, run:
+        
+            tox -e py27,py38
+        
+        in the root of the project source tree.
+        
+        To enable NumPy and Pandas tests, run:
+        
+            tox -e py27-extra,py38-extra
+        
+        (this may take a long time the first time, because NumPy and Pandas will
+        have to be installed in the new virtual environments)
+        
+        To fix code formatting:
+        
+            tox -e lint
+        
+        See `tox.ini` file to learn how to use to test
+        individual Python versions.
+        
+        Contributors
+        ------------
+        
+        Sergey Astanin, Pau Tallada Crespí, Erwin Marsi, Mik Kocikowski, Bill
+        Ryder, Zach Dwiel, Frederik Rietdijk, Philipp Bogensberger, Greg
+        (anonymous), Stefan Tatschner, Emiel van Miltenburg, Brandon Bennett,
+        Amjith Ramanujam, Jan Schulz, Simon Percivall, Javier Santacruz
+        López-Cepero, Sam Denton, Alexey Ziyangirov, acaird, Cesar Sanchez,
+        naught101, John Vandenberg, Zack Dever, Christian Clauss, Benjamin
+        Maier, Andy MacKinlay, Thomas Roten, Jue Wang, Joe King, Samuel Phan,
+        Nick Satterly, Daniel Robbins, Dmitry B, Lars Butler, Andreas Maier,
+        Dick Marinus, Sébastien Celles, Yago González, Andrew Gaul, Wim Glenn,
+        Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea,
+        Wes Turner, Andrew Tija, Marco Gorelli, Sean McGinnis, danja100,
+        endolith, Dominic Davis-Foster, pavlocat, Daniel Aslau, paulc,
+        Felix Yan, Shane Loretz, Frank Busse, Harsh Singh, Derek Weitzel,
+        Vladimir Vrzić, 서승우 (chrd5273), Georgy Frolov, Christian Cwienk,
+        Bart Broere, Vilhelm Prytz.
+        
+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
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Topic :: Software Development :: Libraries
+Description-Content-Type: text/markdown
+Provides-Extra: widechars

+ 1 - 0
desktop/core/ext-py/tabulate-0.8.9/README

@@ -0,0 +1 @@
+README.md

+ 761 - 0
desktop/core/ext-py/tabulate-0.8.9/README.md

@@ -0,0 +1,761 @@
+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
+------------
+
+[![Build status](https://circleci.com/gh/astanin/python-tabulate.svg?style=svg)](https://circleci.com/gh/astanin/python-tabulate/tree/master) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master)
+
+Library usage
+-------------
+
+The module provides just one function, `tabulate`, which takes a list of
+lists or another tabular data type as the first argument, and outputs a
+nicely formatted plain-text table:
+
+    >>> 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"
+-   "github"
+-   "grid"
+-   "fancy\_grid"
+-   "pipe"
+-   "orgtbl"
+-   "jira"
+-   "presto"
+-   "pretty"
+-   "psql"
+-   "rst"
+-   "mediawiki"
+-   "moinmoin"
+-   "youtrack"
+-   "html"
+-   "unsafehtml"
+-   "latex"
+-   "latex\_raw"
+-   "latex\_booktabs"
+-   "latex\_longtable"
+-   "textile"
+-   "tsv"
+
+`plain` tables do not use any pseudo-graphics to draw lines:
+
+    >>> table = [["spam",42],["eggs",451],["bacon",0]]
+    >>> headers = ["item", "qty"]
+    >>> print(tabulate(table, headers, tablefmt="plain"))
+    item      qty
+    spam       42
+    eggs      451
+    bacon       0
+
+`simple` is the default format (the default may change in future
+versions). It corresponds to `simple_tables` in [Pandoc Markdown
+extensions](http://johnmacfarlane.net/pandoc/README.html#tables):
+
+    >>> print(tabulate(table, headers, tablefmt="simple"))
+    item      qty
+    ------  -----
+    spam       42
+    eggs      451
+    bacon       0
+
+`github` follows the conventions of GitHub flavored Markdown. It
+corresponds to the `pipe` format without alignment colons:
+
+    >>> print(tabulate(table, headers, tablefmt="github"))
+    | item   | qty   |
+    |--------|-------|
+    | spam   | 42    |
+    | eggs   | 451   |
+    | bacon  | 0     |
+
+`grid` is like tables formatted by Emacs'
+[table.el](http://table.sourceforge.net/) package. It corresponds to
+`grid_tables` in Pandoc Markdown extensions:
+
+    >>> 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(table, headers, tablefmt="presto"))
+     item   |   qty
+    --------+-------
+     spam   |    42
+     eggs   |   451
+     bacon  |     0
+
+`pretty` attempts to be close to the format emitted by the PrettyTables
+library:
+
+    >>> print(tabulate(table, headers, tablefmt="pretty"))
+    +-------+-----+
+    | item  | qty |
+    +-------+-----+
+    | spam  | 42  |
+    | eggs  | 451 |
+    | bacon |  0  |
+    +-------+-----+
+
+`psql` is like tables formatted by Postgres' psql cli:
+
+    >>> print(tabulate(table, headers, tablefmt="psql"))
+    +--------+-------+
+    | item   |   qty |
+    |--------+-------|
+    | spam   |    42 |
+    | eggs   |   451 |
+    | bacon  |     0 |
+    +--------+-------+
+
+`pipe` follows the conventions of [PHP Markdown
+Extra](http://michelf.ca/projects/php-markdown/extra/#table) extension.
+It corresponds to `pipe_tables` in Pandoc. This format uses colons to
+indicate column alignment:
+
+    >>> print(tabulate(table, headers, tablefmt="pipe"))
+    | item   |   qty |
+    |:-------|------:|
+    | spam   |    42 |
+    | eggs   |   451 |
+    | bacon  |     0 |
+
+`orgtbl` follows the conventions of Emacs
+[org-mode](http://orgmode.org/manual/Tables.html), and is editable also
+in the minor orgtbl-mode. Hence its name:
+
+    >>> 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](http://docutils.sourceforge.net/docs/user/rst/quickref.html#tables)
+format:
+
+    >>> print(tabulate(table, headers, tablefmt="rst"))
+    ======  =====
+    item      qty
+    ======  =====
+    spam       42
+    eggs      451
+    bacon       0
+    ======  =====
+
+`mediawiki` format produces a table markup used in
+[Wikipedia](http://www.mediawiki.org/wiki/Help:Tables) and on other
+MediaWiki-based sites:
+
+    >>> 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](https://moinmo.in/) wikis:
+
+    >>> print(tabulate(table, headers, tablefmt="moinmoin"))
+    || ''' item   ''' || ''' quantity   ''' ||
+    ||  spam    ||  41.999      ||
+    ||  eggs    ||  451         ||
+    ||  bacon   ||              ||
+
+`youtrack` format produces a table markup used in Youtrack tickets:
+
+    >>> print(tabulate(table, headers, tablefmt="youtrack"))
+    ||  item    ||  quantity   ||
+    |   spam    |  41.999      |
+    |   eggs    |  451         |
+    |   bacon   |              |
+
+`textile` format produces a table markup used in
+[Textile](http://redcloth.org/hobix.com/textile/) format:
+
+    >>> print(tabulate(table, headers, tablefmt="textile"))
+    |_.  item   |_.   qty |
+    |<. spam    |>.    42 |
+    |<. eggs    |>.   451 |
+    |<. bacon   |>.     0 |
+
+`html` produces standard HTML markup as an html.escape'd str
+with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML
+and a .str property so that the raw HTML remains accessible.
+`unsafehtml` table format can be used if an unescaped HTML is required:
+
+    >>> 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.
+
+`latex_longtable` creates a table that can stretch along multiple pages,
+using the `longtable` package.
+
+### Column alignment
+
+`tabulate` is smart about column alignment. It detects columns which
+contain only numbers, and aligns them by a decimal point (or flushes
+them to the right if they appear to be integers). Text columns are
+flushed to the left.
+
+You can override the default alignment with `numalign` and `stralign`
+named arguments. Possible column alignments are: `right`, `center`,
+`left`, `decimal` (only for numbers), and `None` (to disable alignment).
+
+Aligning by a decimal point works best when you need to compare numbers
+at a glance:
+
+    >>> 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
+    ----  ----
+
+
+To disable this feature use `disable_numparse=True`.
+
+    >>> print(tabulate.tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True))
+    ----  ----
+    Ver1  18.0
+    Ver2  19.2
+    ----  ----
+
+
+### Custom column alignment
+
+`tabulate` allows a custom column alignment to override the above. The
+`colalign` argument can be a list or a tuple of `stralign` named
+arguments. Possible column alignments are: `right`, `center`, `left`,
+`decimal` (only for numbers), and `None` (to disable alignment).
+Omitting an alignment uses the default. For example:
+
+    >>> print(tabulate([["one", "two"], ["three", "four"]], colalign=("right",))
+    -----  ----
+      one  two
+    three  four
+    -----  ----
+
+### Number formatting
+
+`tabulate` allows to define custom number formatting applied to all
+columns of decimal numbers. Use `floatfmt` named argument:
+
+    >>> 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   |
+
+`pretty` tables:
+
+    >>> print(tabulate(table, headers, tablefmt="pretty"))
+    +------+-----+
+    | 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, github, grid, fancy_grid, pipe,
+                              orgtbl, rst, mediawiki, html, latex, latex_raw,
+                              latex_booktabs, latex_longtable, tsv
+                              (default: simple)
+
+Performance considerations
+--------------------------
+
+Such features as decimal point alignment and trying to parse everything
+as a number imply that `tabulate`:
+
+-   has to "guess" how to print a particular tabular data type
+-   needs to keep the entire table in-memory
+-   has to "transpose" the table twice
+-   does much more work than it may appear
+
+It may not be suitable for serializing really big tables (but who's
+going to do that, anyway?) or printing tables in performance sensitive
+applications. `tabulate` is about two orders of magnitude slower than
+simply joining lists of values with a tab, comma, or other separator.
+
+At the same time, `tabulate` is comparable to other table
+pretty-printers. Given a 10x10 table (a list of lists) of mixed text and
+numeric data, `tabulate` appears to be slower than `asciitable`, and
+faster than `PrettyTable` and `texttable` The following mini-benchmark
+was run in Python 3.8.3 in Windows 10 x64:
+
+    =================================  ==========  ===========
+    Table formatter                      time, μs    rel. time
+    =================================  ==========  ===========
+    csv to StringIO                          12.5          1.0
+    join with tabs and newlines              15.6          1.3
+    asciitable (0.8.0)                      191.4         15.4
+    tabulate (0.8.9)                        472.8         38.0
+    tabulate (0.8.9, WIDE_CHARS_MODE)       789.6         63.4
+    PrettyTable (0.7.2)                     879.1         70.6
+    texttable (1.6.2)                      1352.2        108.6
+    =================================  ==========  ===========
+
+
+Version history
+---------------
+
+The full version history can be found at the [changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG).
+
+How to contribute
+-----------------
+
+Contributions should include tests and an explanation for the changes
+they propose. Documentation (examples, docstrings, README.md) should be
+updated accordingly.
+
+This project uses [pytest](https://docs.pytest.org/) testing
+framework and [tox](https://tox.readthedocs.io/) to automate testing in
+different environments. Add tests to one of the files in the `test/`
+folder.
+
+To run tests on all supported Python versions, make sure all Python
+interpreters, `pytest` and `tox` are installed, then run `tox` in the root
+of the project source tree.
+
+On Linux `tox` expects to find executables like `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 environments, use `-e` option. For example, to
+test only against Python 2.7 and Python 3.8, run:
+
+    tox -e py27,py38
+
+in the root of the project source tree.
+
+To enable NumPy and Pandas tests, run:
+
+    tox -e py27-extra,py38-extra
+
+(this may take a long time the first time, because NumPy and Pandas will
+have to be installed in the new virtual environments)
+
+To fix code formatting:
+
+    tox -e lint
+
+See `tox.ini` file to learn how to use to test
+individual Python versions.
+
+Contributors
+------------
+
+Sergey Astanin, Pau Tallada Crespí, Erwin Marsi, Mik Kocikowski, Bill
+Ryder, Zach Dwiel, Frederik Rietdijk, Philipp Bogensberger, Greg
+(anonymous), Stefan Tatschner, Emiel van Miltenburg, Brandon Bennett,
+Amjith Ramanujam, Jan Schulz, Simon Percivall, Javier Santacruz
+López-Cepero, Sam Denton, Alexey Ziyangirov, acaird, Cesar Sanchez,
+naught101, John Vandenberg, Zack Dever, Christian Clauss, Benjamin
+Maier, Andy MacKinlay, Thomas Roten, Jue Wang, Joe King, Samuel Phan,
+Nick Satterly, Daniel Robbins, Dmitry B, Lars Butler, Andreas Maier,
+Dick Marinus, Sébastien Celles, Yago González, Andrew Gaul, Wim Glenn,
+Jean Michel Rouly, Tim Gates, John Vandenberg, Sorin Sbarnea,
+Wes Turner, Andrew Tija, Marco Gorelli, Sean McGinnis, danja100,
+endolith, Dominic Davis-Foster, pavlocat, Daniel Aslau, paulc,
+Felix Yan, Shane Loretz, Frank Busse, Harsh Singh, Derek Weitzel,
+Vladimir Vrzić, 서승우 (chrd5273), Georgy Frolov, Christian Cwienk,
+Bart Broere, Vilhelm Prytz.

+ 126 - 0
desktop/core/ext-py/tabulate-0.8.9/benchmark.py

@@ -0,0 +1,126 @@
+# -*- 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
+from platform import python_version_tuple
+
+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 = [
+    ("join with tabs and newlines", "join_table(table)"),
+    ("csv to StringIO", "csv_table(table)"),
+    ("asciitable (%s)" % asciitable.__version__, "run_asciitable(table)"),
+    ("tabulate (%s)" % tabulate.__version__, "run_tabulate(table)"),
+    (
+        "tabulate (%s, WIDE_CHARS_MODE)" % tabulate.__version__,
+        "run_tabulate(table, widechars=True)",
+    ),
+    ("PrettyTable (%s)" % prettytable.__version__, "run_prettytable(table)"),
+    ("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")]
+
+    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, ["Table formatter", "time, μs", "rel. time"], "rst", floatfmt=".1f"
+    )
+
+    from platform import platform
+
+    if platform().startswith("Windows"):
+        print(table)
+    elif python_version_tuple()[0] < "3":
+        print(codecs.encode(table, "utf-8"))
+    else:
+        print(table)
+
+
+if __name__ == "__main__":
+    if sys.argv[1:]:
+        n = int(sys.argv[1])
+    else:
+        n = 10000
+    benchmark(n)

+ 4 - 0
desktop/core/ext-py/tabulate-0.8.9/setup.cfg

@@ -0,0 +1,4 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+

+ 66 - 0
desktop/core/ext-py/tabulate-0.8.9/setup.py

@@ -0,0 +1,66 @@
+#!/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.md", "r", encoding="utf-8").read()
+else:
+    LONG_DESCRIPTION = open("README.md", "r").read()
+
+# 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.9",
+    description="Pretty-print tabular data",
+    long_description=LONG_DESCRIPTION,
+    long_description_content_type="text/markdown",
+    author="Sergey Astanin",
+    author_email="s.astanin@gmail.com",
+    url="https://github.com/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",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
+        "Topic :: Software Development :: Libraries",
+    ],
+    py_modules=["tabulate"],
+    entry_points={"console_scripts": console_scripts},
+    extras_require={"widechars": ["wcwidth"]},
+)

+ 1879 - 0
desktop/core/ext-py/tabulate-0.8.9/tabulate.py

@@ -0,0 +1,1879 @@
+# -*- coding: utf-8 -*-
+
+"""Pretty-print tabular data."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from collections import namedtuple
+import sys
+import re
+import math
+
+
+if sys.version_info >= (3, 3):
+    from collections.abc import Iterable
+else:
+    from collections import Iterable
+
+if sys.version_info[0] < 3:
+    from itertools import izip_longest
+    from functools import partial
+
+    _none_type = type(None)
+    _bool_type = bool
+    _int_type = int
+    _long_type = long  # noqa
+    _float_type = float
+    _text_type = unicode  # noqa
+    _binary_type = str
+
+    def _is_file(f):
+        return hasattr(f, "read")
+
+
+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
+
+try:
+    from html import escape as htmlescape
+except ImportError:
+    from cgi import escape as htmlescape
+
+
+__all__ = ["tabulate", "tabulate_formats", "simple_separated_format"]
+__version__ = "0.8.9"
+
+
+# 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 = ""
+# default align will be overwritten by "left", "center" or "decimal"
+# depending on the formatter
+_DEFAULT_ALIGN = "default"
+
+
+# if True, enable wide-character (CJK) support
+WIDE_CHARS_MODE = wcwidth is not None
+
+
+Line = namedtuple("Line", ["begin", "hline", "sep", "end"])
+
+
+DataRow = namedtuple("DataRow", ["begin", "sep", "end"])
+
+
+# A table structure is suppposed to be:
+#
+#     --- lineabove ---------
+#         headerrow
+#     --- linebelowheader ---
+#         datarow
+#     --- linebetweenrows ---
+#     ... (more datarows) ...
+#     --- linebetweenrows ---
+#         last datarow
+#     --- linebelow ---------
+#
+# TableFormat's line* elements can be
+#
+#   - either None, if the element is not used,
+#   - or a Line tuple,
+#   - or a function: [col_widths], [col_alignments] -> string.
+#
+# TableFormat's *row elements can be
+#
+#   - either None, if the element is not used,
+#   - or a DataRow tuple,
+#   - or a function: [cell_values], [col_widths], [col_alignments] -> string.
+#
+# padding (an integer) is the amount of white space around data values.
+#
+# with_header_hide:
+#
+#   - either None, to display all table elements unconditionally,
+#   - or a list of elements not to be displayed if the table has column headers.
+#
+TableFormat = namedtuple(
+    "TableFormat",
+    [
+        "lineabove",
+        "linebelowheader",
+        "linebetweenrows",
+        "linebelow",
+        "headerrow",
+        "datarow",
+        "padding",
+        "with_header_hide",
+    ],
+)
+
+
+def _pipe_segment_with_colons(align, colwidth):
+    """Return a segment of a horizontal line with optional colons which
+    indicate column's alignment (as in `pipe` output format)."""
+    w = colwidth
+    if align in ["right", "decimal"]:
+        return ("-" * (w - 1)) + ":"
+    elif align == "center":
+        return ":" + ("-" * (w - 2)) + ":"
+    elif align == "left":
+        return ":" + ("-" * (w - 1))
+    else:
+        return "-" * w
+
+
+def _pipe_line_with_colons(colwidths, colaligns):
+    """Return a horizontal line with optional colons to indicate column's
+    alignment (as in `pipe` output format)."""
+    if not colaligns:  # e.g. printing an empty data frame (github issue #15)
+        colaligns = [""] * len(colwidths)
+    segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)]
+    return "|" + "|".join(segments) + "|"
+
+
+def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
+    alignment = {
+        "left": "",
+        "right": 'align="right"| ',
+        "center": 'align="center"| ',
+        "decimal": 'align="right"| ',
+    }
+    # hard-coded padding _around_ align attribute and value together
+    # rather than padding parameter which affects only the value
+    values_with_attrs = [
+        " " + alignment.get(a, "") + c + " " for c, a in zip(cell_values, colaligns)
+    ]
+    colsep = separator * 2
+    return (separator + colsep.join(values_with_attrs)).rstrip()
+
+
+def _textile_row_with_attrs(cell_values, colwidths, colaligns):
+    cell_values[0] += " "
+    alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."}
+    values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values))
+    return "|" + "|".join(values) + "|"
+
+
+def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore):
+    # this table header will be suppressed if there is a header row
+    return "<table>\n<tbody>"
+
+
+def _html_row_with_attrs(celltag, unsafe, cell_values, colwidths, colaligns):
+    alignment = {
+        "left": "",
+        "right": ' style="text-align: right;"',
+        "center": ' style="text-align: center;"',
+        "decimal": ' style="text-align: right;"',
+    }
+    if unsafe:
+        values_with_attrs = [
+            "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), c)
+            for c, a in zip(cell_values, colaligns)
+        ]
+    else:
+        values_with_attrs = [
+            "<{0}{1}>{2}</{0}>".format(celltag, alignment.get(a, ""), htmlescape(c))
+            for c, a in zip(cell_values, colaligns)
+        ]
+    rowhtml = "<tr>{}</tr>".format("".join(values_with_attrs).rstrip())
+    if celltag == "th":  # it's a header row, create a new table header
+        rowhtml = "<table>\n<thead>\n{}\n</thead>\n<tbody>".format(rowhtml)
+    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, longtable=False):
+    alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"}
+    tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns])
+    return "\n".join(
+        [
+            ("\\begin{tabular}{" if not longtable else "\\begin{longtable}{")
+            + tabular_columns_fmt
+            + "}",
+            "\\toprule" if booktabs else "\\hline",
+        ]
+    )
+
+
+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,
+    ),
+    "fancy_outline": TableFormat(
+        lineabove=Line("╒", "═", "╤", "╕"),
+        linebelowheader=Line("╞", "═", "╪", "╡"),
+        linebetweenrows=None,
+        linebelow=Line("╘", "═", "╧", "╛"),
+        headerrow=DataRow("│", "│", "│"),
+        datarow=DataRow("│", "│", "│"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "github": TableFormat(
+        lineabove=Line("|", "-", "|", "|"),
+        linebelowheader=Line("|", "-", "|", "|"),
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=["lineabove"],
+    ),
+    "pipe": TableFormat(
+        lineabove=_pipe_line_with_colons,
+        linebelowheader=_pipe_line_with_colons,
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=["lineabove"],
+    ),
+    "orgtbl": TableFormat(
+        lineabove=None,
+        linebelowheader=Line("|", "-", "+", "|"),
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "jira": TableFormat(
+        lineabove=None,
+        linebelowheader=None,
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("||", "||", "||"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "presto": TableFormat(
+        lineabove=None,
+        linebelowheader=Line("", "-", "+", ""),
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("", "|", ""),
+        datarow=DataRow("", "|", ""),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "pretty": TableFormat(
+        lineabove=Line("+", "-", "+", "+"),
+        linebelowheader=Line("+", "-", "+", "+"),
+        linebetweenrows=None,
+        linebelow=Line("+", "-", "+", "+"),
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "psql": TableFormat(
+        lineabove=Line("+", "-", "+", "+"),
+        linebelowheader=Line("|", "-", "+", "|"),
+        linebetweenrows=None,
+        linebelow=Line("+", "-", "+", "+"),
+        headerrow=DataRow("|", "|", "|"),
+        datarow=DataRow("|", "|", "|"),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "rst": TableFormat(
+        lineabove=Line("", "=", "  ", ""),
+        linebelowheader=Line("", "=", "  ", ""),
+        linebetweenrows=None,
+        linebelow=Line("", "=", "  ", ""),
+        headerrow=DataRow("", "  ", ""),
+        datarow=DataRow("", "  ", ""),
+        padding=0,
+        with_header_hide=None,
+    ),
+    "mediawiki": TableFormat(
+        lineabove=Line(
+            '{| class="wikitable" style="text-align: left;"',
+            "",
+            "",
+            "\n|+ <!-- 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", False),
+        datarow=partial(_html_row_with_attrs, "td", False),
+        padding=0,
+        with_header_hide=["lineabove"],
+    ),
+    "unsafehtml": TableFormat(
+        lineabove=_html_begin_table_without_header,
+        linebelowheader="",
+        linebetweenrows=None,
+        linebelow=Line("</tbody>\n</table>", "", "", ""),
+        headerrow=partial(_html_row_with_attrs, "th", True),
+        datarow=partial(_html_row_with_attrs, "td", True),
+        padding=0,
+        with_header_hide=["lineabove"],
+    ),
+    "latex": TableFormat(
+        lineabove=_latex_line_begin_tabular,
+        linebelowheader=Line("\\hline", "", "", ""),
+        linebetweenrows=None,
+        linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
+        headerrow=_latex_row,
+        datarow=_latex_row,
+        padding=1,
+        with_header_hide=None,
+    ),
+    "latex_raw": TableFormat(
+        lineabove=_latex_line_begin_tabular,
+        linebelowheader=Line("\\hline", "", "", ""),
+        linebetweenrows=None,
+        linebelow=Line("\\hline\n\\end{tabular}", "", "", ""),
+        headerrow=partial(_latex_row, escrules={}),
+        datarow=partial(_latex_row, escrules={}),
+        padding=1,
+        with_header_hide=None,
+    ),
+    "latex_booktabs": TableFormat(
+        lineabove=partial(_latex_line_begin_tabular, booktabs=True),
+        linebelowheader=Line("\\midrule", "", "", ""),
+        linebetweenrows=None,
+        linebelow=Line("\\bottomrule\n\\end{tabular}", "", "", ""),
+        headerrow=_latex_row,
+        datarow=_latex_row,
+        padding=1,
+        with_header_hide=None,
+    ),
+    "latex_longtable": TableFormat(
+        lineabove=partial(_latex_line_begin_tabular, longtable=True),
+        linebelowheader=Line("\\hline\n\\endhead", "", "", ""),
+        linebetweenrows=None,
+        linebelow=Line("\\hline\n\\end{longtable}", "", "", ""),
+        headerrow=_latex_row,
+        datarow=_latex_row,
+        padding=1,
+        with_header_hide=None,
+    ),
+    "tsv": TableFormat(
+        lineabove=None,
+        linebelowheader=None,
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("", "\t", ""),
+        datarow=DataRow("", "\t", ""),
+        padding=0,
+        with_header_hide=None,
+    ),
+    "textile": TableFormat(
+        lineabove=None,
+        linebelowheader=None,
+        linebetweenrows=None,
+        linebelow=None,
+        headerrow=DataRow("|_. ", "|_.", "|"),
+        datarow=_textile_row_with_attrs,
+        padding=1,
+        with_header_hide=None,
+    ),
+}
+
+
+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",
+    "pretty": "pretty",
+    "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|\x1b\]8;;(.*?)\x1b\\"
+)  # ANSI color codes
+_invisible_codes_bytes = re.compile(
+    b"\x1b\\[\\d+\\[;\\d]*m|\x1b\\[\\d*;\\d*;\\d*m|\\x1b\\]8;;(.*?)\\x1b\\\\"
+)  # ANSI color codes
+_invisible_codes_link = re.compile(
+    r"\x1B]8;[a-zA-Z0-9:]*;[^\x1B]+\x1B\\([^\x1b]+)\x1B]8;;\x1B\\"
+)  # Terminal hyperlinks
+
+
+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:
+        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):
+    r"""Remove invisible ANSI color codes.
+
+    >>> str(_strip_invisible('\x1B]8;;https://example.com\x1B\\This is a link\x1B]8;;\x1B\\'))
+    'This is a link'
+
+    """
+    if isinstance(s, _text_type):
+        links_removed = re.sub(_invisible_codes_link, "\\1", s)
+        return re.sub(_invisible_codes, "", links_removed)
+    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)  # noqa
+    else:
+        width_fn = line_width_fn
+    return width_fn
+
+
+def _align_column_choose_padfn(strings, alignment, has_invisible):
+    if alignment == "right":
+        if not PRESERVE_WHITESPACE:
+            strings = [s.strip() for s in strings]
+        padfn = _padleft
+    elif alignment == "center":
+        if not PRESERVE_WHITESPACE:
+            strings = [s.strip() for s in strings]
+        padfn = _padboth
+    elif alignment == "decimal":
+        if has_invisible:
+            decimals = [_afterpoint(_strip_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_choose_width_fn(has_invisible, enable_widechars, is_multiline):
+    if has_invisible:
+        line_width_fn = _visible_width
+    elif enable_widechars:  # optional wide-character support if available
+        line_width_fn = wcwidth.wcswidth
+    else:
+        line_width_fn = len
+    if is_multiline:
+        width_fn = lambda s: _align_column_multiline_width(s, line_width_fn)  # noqa
+    else:
+        width_fn = line_width_fn
+    return width_fn
+
+
+def _align_column_multiline_width(multiline_s, line_width_fn=len):
+    """Visible width of a potentially multiline content."""
+    return list(map(line_width_fn, re.split("[\r\n]", multiline_s)))
+
+
+def _flat_list(nested_list):
+    ret = []
+    for item in nested_list:
+        if isinstance(item, list):
+            for subitem in item:
+                ret.append(subitem)
+        else:
+            ret.append(item)
+    return ret
+
+
+def _align_column(
+    strings,
+    alignment,
+    minwidth=0,
+    has_invisible=True,
+    enable_widechars=False,
+    is_multiline=False,
+):
+    """[string] -> [padded_string]"""
+    strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible)
+    width_fn = _align_column_choose_width_fn(
+        has_invisible, enable_widechars, is_multiline
+    )
+
+    s_widths = list(map(width_fn, strings))
+    maxwidth = max(max(_flat_list(s_widths)), minwidth)
+    # TODO: refactor column alignment in single-line and multiline modes
+    if is_multiline:
+        if not enable_widechars and not has_invisible:
+            padded_strings = [
+                "\n".join([padfn(maxwidth, s) for s in ms.splitlines()])
+                for ms in strings
+            ]
+        else:
+            # enable wide-character width corrections
+            s_lens = [[len(s) for s in re.split("[\r\n]", ms)] for ms in strings]
+            visible_widths = [
+                [maxwidth - (w - l) for w, l in zip(mw, ml)]
+                for mw, ml in zip(s_widths, s_lens)
+            ]
+            # wcswidth and _visible_width don't count invisible characters;
+            # padfn doesn't need to apply another correction
+            padded_strings = [
+                "\n".join([padfn(w, s) for s, w in zip((ms.splitlines() or ms), mw)])
+                for ms, mw in zip(strings, visible_widths)
+            ]
+    else:  # single-line cell values
+        if not enable_widechars and not has_invisible:
+            padded_strings = [padfn(maxwidth, s) for s in strings]
+        else:
+            # enable wide-character width corrections
+            s_lens = list(map(len, strings))
+            visible_widths = [maxwidth - (w - l) for w, l in zip(s_widths, s_lens)]
+            # wcswidth and _visible_width don't count invisible characters;
+            # padfn doesn't need to apply another correction
+            padded_strings = [padfn(w, s) for s, w in zip(strings, visible_widths)]
+    return padded_strings
+
+
+def _more_generic(type1, type2):
+    types = {
+        _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 according to its type.
+
+    Unicode is supported:
+
+    >>> hrow = ['\u0431\u0443\u043a\u0432\u0430', '\u0446\u0438\u0444\u0440\u0430'] ; \
+        tbl = [['\u0430\u0437', 2], ['\u0431\u0443\u043a\u0438', 4]] ; \
+        good_result = '\\u0431\\u0443\\u043a\\u0432\\u0430      \\u0446\\u0438\\u0444\\u0440\\u0430\\n-------  -------\\n\\u0430\\u0437             2\\n\\u0431\\u0443\\u043a\\u0438           4' ; \
+        tabulate(tbl, headers=hrow) == good_result
+    True
+
+    """  # noqa
+    if val is None:
+        return missingval
+
+    if valtype 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  # noqa
+    except ValueError:  # numpy.ndarray, pandas.core.index.Index, ...
+        is_headers2bool_broken = True  # noqa
+        headers = list(headers)
+
+    index = None
+    if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"):
+        # dict-like and pandas.DataFrame?
+        if hasattr(tabular_data.values, "__call__"):
+            # likely a conventional dict
+            keys = tabular_data.keys()
+            rows = list(
+                izip_longest(*tabular_data.values())
+            )  # columns have to be transposed
+        elif hasattr(tabular_data, "index"):
+            # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0)
+            keys = list(tabular_data)
+            if (
+                showindex in ["default", "always", True]
+                and tabular_data.index.name is not None
+            ):
+                if isinstance(tabular_data.index.name, list):
+                    keys[:0] = tabular_data.index.name
+                else:
+                    keys[:0] = [tabular_data.index.name]
+            vals = tabular_data.values  # values matrix doesn't need to be transposed
+            # for DataFrames add an index per default
+            index = list(tabular_data.index)
+            rows = [list(row) for row in vals]
+        else:
+            raise ValueError("tabular data doesn't appear to be a dict or a DataFrame")
+
+        if headers == "keys":
+            headers = list(map(_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 hasattr(rows[0], "keys") and hasattr(rows[0], "values"):
+            # dict-like object
+            uniq_keys = set()  # implements hashed lookup
+            keys = []  # storage for set
+            if headers == "firstrow":
+                firstdict = rows[0] if len(rows) > 0 else {}
+                keys.extend(firstdict.keys())
+                uniq_keys.update(keys)
+                rows = rows[1:]
+            for row in rows:
+                for k in row.keys():
+                    # Save unique items in input order
+                    if k not in uniq_keys:
+                        keys.append(k)
+                        uniq_keys.add(k)
+            if headers == "keys":
+                headers = keys
+            elif isinstance(headers, dict):
+                # a dict of headers for a list of dicts
+                headers = [headers.get(k, k) for k in keys]
+                headers = list(map(_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=_DEFAULT_ALIGN,
+    stralign=_DEFAULT_ALIGN,
+    missingval=_DEFAULT_MISSINGVAL,
+    showindex="default",
+    disable_numparse=False,
+    colalign=None,
+):
+    """Format a fixed width table for pretty printing.
+
+    >>> print(tabulate([[1, 2.34], [-56, "8.999"], ["2", "10001"]]))
+    ---  ---------
+      1      2.34
+    -56      8.999
+      2  10001
+    ---  ---------
+
+    The first required argument (`tabular_data`) can be a
+    list-of-lists (or another iterable of iterables), a list of named
+    tuples, a dictionary of iterables, an iterable of dictionaries,
+    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', 'latex_booktabs', 'latex_longtable' and tsv.
+    Variable `tabulate_formats`contains the list of currently supported formats.
+
+    "plain" format doesn't use any pseudographics to draw tables,
+    it separates columns with a double space:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                 ["strings", "numbers"], "plain"))
+    strings      numbers
+    spam         41.9999
+    eggs        451
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="plain"))
+    spam   41.9999
+    eggs  451
+
+    "simple" format is like Pandoc simple_tables:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                 ["strings", "numbers"], "simple"))
+    strings      numbers
+    ---------  ---------
+    spam         41.9999
+    eggs        451
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="simple"))
+    ----  --------
+    spam   41.9999
+    eggs  451
+    ----  --------
+
+    "grid" is similar to tables produced by Emacs table.el package or
+    Pandoc grid_tables:
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]],
+    ...                ["strings", "numbers"], "grid"))
+    +-----------+-----------+
+    | strings   |   numbers |
+    +===========+===========+
+    | spam      |   41.9999 |
+    +-----------+-----------+
+    | eggs      |  451      |
+    +-----------+-----------+
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="grid"))
+    +------+----------+
+    | spam |  41.9999 |
+    +------+----------+
+    | eggs | 451      |
+    +------+----------+
+
+    "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 as an html.escape'd str
+    with a ._repr_html_ method so that Jupyter Lab and Notebook display the HTML
+    and a .str property so that the raw HTML remains accessible
+    the unsafehtml table format can be used if an unescaped HTML format is required:
+
+    >>> print(tabulate([["strings", "numbers"], ["spam", 41.9999], ["eggs", "451.0"]],
+    ...                headers="firstrow", tablefmt="html"))
+    <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}
+
+    "latex_longtable" produces a tabular environment that can stretch along
+    multiple pages, using the longtable package for LaTeX.
+
+    >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="latex_longtable"))
+    \\begin{longtable}{lr}
+    \\hline
+     spam &  41.9999 \\\\
+     eggs & 451      \\\\
+    \\hline
+    \\end{longtable}
+
+
+    Number parsing
+    --------------
+    By default, anything which can be parsed as a number is a number.
+    This ensures numbers represented as strings are aligned properly.
+    This can lead to weird results for particular strings such as
+    specific git SHAs e.g. "42992e1" will be parsed into the number
+    429920 and aligned as such.
+
+    To completely disable number parsing (and alignment), use
+    `disable_numparse=True`. For more fine grained control, a list column
+    indices is used to disable number parsing only on those columns
+    e.g. `disable_numparse=[0, 2]` would disable number parsing only on the
+    first and third columns.
+    """
+
+    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)
+
+    # PrettyTable formatting does not use any extra padding.
+    # Numbers are not parsed and are treated the same as strings for alignment.
+    # Check if pretty is the format being used and override the defaults so it
+    # does not impact other formats.
+    min_padding = MIN_PADDING
+    if tablefmt == "pretty":
+        min_padding = 0
+        disable_numparse = True
+        numalign = "center" if numalign == _DEFAULT_ALIGN else numalign
+        stralign = "center" if stralign == _DEFAULT_ALIGN else stralign
+    else:
+        numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign
+        stralign = "left" if stralign == _DEFAULT_ALIGN else stralign
+
+    # optimization: look for ANSI control codes once,
+    # enable smart width functions only if a control code is found
+    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)
+    if not has_invisible:
+        has_invisible = re.search(_invisible_codes_link, plain_text)
+    enable_widechars = wcwidth is not None and WIDE_CHARS_MODE
+    if (
+        not isinstance(tablefmt, TableFormat)
+        and tablefmt in multiline_formats
+        and _is_multiline(plain_text)
+    ):
+        tablefmt = multiline_formats.get(tablefmt, tablefmt)
+        is_multiline = True
+    else:
+        is_multiline = False
+    width_fn = _choose_width_fn(has_invisible, enable_widechars, is_multiline)
+
+    # format rows and columns, convert numeric values to strings
+    cols = list(izip_longest(*list_of_lists))
+    numparses = _expand_numparse(disable_numparse, len(cols))
+    coltypes = [_column_type(col, numparse=np) for col, np in zip(cols, numparses)]
+    if isinstance(floatfmt, 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]
+    if colalign is not None:
+        assert isinstance(colalign, Iterable)
+        for idx, align in enumerate(colalign):
+            aligns[idx] = align
+    minwidths = (
+        [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols)
+    )
+    cols = [
+        _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline)
+        for c, a, minw in zip(cols, aligns, minwidths)
+    ]
+
+    if headers:
+        # align headers and add headers
+        t_cols = cols or [[""]] * len(headers)
+        t_aligns = aligns or [stralign] * len(headers)
+        minwidths = [
+            max(minw, max(width_fn(cl) for cl in c))
+            for minw, c in zip(minwidths, t_cols)
+        ]
+        headers = [
+            _align_header(h, a, minw, width_fn(h), is_multiline, width_fn)
+            for h, a, minw in zip(headers, t_aligns, minwidths)
+        ]
+        rows = list(zip(*cols))
+    else:
+        minwidths = [max(width_fn(cl) for cl in c) for c in cols]
+        rows = list(zip(*cols))
+
+    if not isinstance(tablefmt, TableFormat):
+        tablefmt = _table_formats.get(tablefmt, _table_formats["simple"])
+
+    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
+
+
+class JupyterHTMLStr(str):
+    """Wrap the string with a _repr_html_ method so that Jupyter
+    displays the HTML table"""
+
+    def _repr_html_(self):
+        return self
+
+    @property
+    def str(self):
+        """add a .str property so that the raw string is still accessible"""
+        return self
+
+
+def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline):
+    """Produce a plain-text representation of the table."""
+    lines = []
+    hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
+    pad = fmt.padding
+    headerrow = fmt.headerrow
+
+    padded_widths = [(w + 2 * pad) for w in colwidths]
+    if is_multiline:
+        pad_row = lambda row, _: row  # noqa do it later, in _append_multiline_row
+        append_row = partial(_append_multiline_row, pad=pad)
+    else:
+        pad_row = _pad_row
+        append_row = _append_basic_row
+
+    padded_headers = pad_row(headers, pad)
+    padded_rows = [pad_row(row, pad) for row in rows]
+
+    if fmt.lineabove and "lineabove" not in hidden:
+        _append_line(lines, padded_widths, colaligns, fmt.lineabove)
+
+    if padded_headers:
+        append_row(lines, padded_headers, padded_widths, colaligns, headerrow)
+        if fmt.linebelowheader and "linebelowheader" not in hidden:
+            _append_line(lines, padded_widths, colaligns, fmt.linebelowheader)
+
+    if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden:
+        # initial rows with a line below
+        for row 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:
+        output = "\n".join(lines)
+        if fmt.lineabove == _html_begin_table_without_header:
+            return JupyterHTMLStr(output)
+        else:
+            return output
+    else:  # a completely empty table
+        return ""
+
+
+def _main():
+    """\
+    Usage: tabulate [options] [FILE ...]
+
+    Pretty-print tabular data.
+    See also https://github.com/astanin/python-tabulate
+
+    FILE                      a filename of the file with tabular data;
+                              if "-" or missing, read data from stdin.
+
+    Options:
+
+    -h, --help                show this message
+    -1, --header              use the first row of data as a table header
+    -o FILE, --output FILE    print table to FILE (default: stdout)
+    -s REGEXP, --sep REGEXP   use a custom column separator (default: whitespace)
+    -F FPFMT, --float FPFMT   floating point number format (default: g)
+    -f FMT, --format FMT      set output table format; supported formats:
+                              plain, simple, grid, fancy_grid, pipe, orgtbl,
+                              rst, mediawiki, html, latex, latex_raw,
+                              latex_booktabs, latex_longtable, tsv
+                              (default: simple)
+    """
+    import getopt
+    import sys
+    import textwrap
+
+    usage = textwrap.dedent(_main.__doc__)
+    try:
+        opts, args = getopt.getopt(
+            sys.argv[1:],
+            "h1o:s:F:A:f:",
+            ["help", "header", "output", "sep=", "float=", "align=", "format="],
+        )
+    except getopt.GetoptError as e:
+        print(e)
+        print(usage)
+        sys.exit(2)
+    headers = []
+    floatfmt = _DEFAULT_FLOATFMT
+    colalign = None
+    tablefmt = "simple"
+    sep = r"\s+"
+    outfile = "-"
+    for opt, value in opts:
+        if opt in ["-1", "--header"]:
+            headers = "firstrow"
+        elif opt in ["-o", "--output"]:
+            outfile = value
+        elif opt in ["-F", "--float"]:
+            floatfmt = value
+        elif opt in ["-C", "--colalign"]:
+            colalign = value.split()
+        elif opt in ["-f", "--format"]:
+            if value not in tabulate_formats:
+                print("%s is not a supported table format" % value)
+                print(usage)
+                sys.exit(3)
+            tablefmt = value
+        elif opt in ["-s", "--sep"]:
+            sep = value
+        elif opt in ["-h", "--help"]:
+            print(usage)
+            sys.exit(0)
+    files = [sys.stdin] if not args else args
+    with (sys.stdout if outfile == "-" else open(outfile, "w")) as out:
+        for f in files:
+            if f == "-":
+                f = sys.stdin
+            if _is_file(f):
+                _pprint_file(
+                    f,
+                    headers=headers,
+                    tablefmt=tablefmt,
+                    sep=sep,
+                    floatfmt=floatfmt,
+                    file=out,
+                    colalign=colalign,
+                )
+            else:
+                with open(f) as fobj:
+                    _pprint_file(
+                        fobj,
+                        headers=headers,
+                        tablefmt=tablefmt,
+                        sep=sep,
+                        floatfmt=floatfmt,
+                        file=out,
+                        colalign=colalign,
+                    )
+
+
+def _pprint_file(fobject, headers, tablefmt, sep, floatfmt, file, colalign):
+    rows = fobject.readlines()
+    table = [re.split(sep, r.rstrip()) for r in rows if r.strip()]
+    print(
+        tabulate(table, headers, tablefmt, floatfmt=floatfmt, colalign=colalign),
+        file=file,
+    )
+
+
+if __name__ == "__main__":
+    _main()

+ 16 - 0
desktop/core/ext-py/tabulate-0.8.9/test/common.py

@@ -0,0 +1,16 @@
+import pytest  # noqa
+from pytest import skip, raises  # noqa
+
+
+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 = range(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

+ 60 - 0
desktop/core/ext-py/tabulate-0.8.9/test/test_api.py

@@ -0,0 +1,60 @@
+"""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 skip
+
+
+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("")  # noqa
+
+
+def _check_signature(function, expected_sig):
+    if not signature:
+        skip("")
+    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)  # noqa
+    expected_sig = [
+        ("tabular_data", _empty),
+        ("headers", ()),
+        ("tablefmt", "simple"),
+        ("floatfmt", "g"),
+        ("numalign", "default"),
+        ("stralign", "default"),
+        ("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)  # noqa
+    expected_sig = [("separator", _empty)]
+    _check_signature(simple_separated_format, expected_sig)

+ 214 - 0
desktop/core/ext-py/tabulate-0.8.9/test/test_cli.py

@@ -0,0 +1,214 @@
+"""Command-line interface.
+
+"""
+
+
+from __future__ import print_function
+from __future__ import unicode_literals
+import os
+import sys
+
+
+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 = [sys.executable, "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 = [sys.executable, "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 = [
+                sys.executable,
+                "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 = [sys.executable, "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 = [sys.executable, "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 = [sys.executable, "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 = [sys.executable, "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())

+ 482 - 0
desktop/core/ext-py/tabulate-0.8.9/test/test_input.py

@@ -0,0 +1,482 @@
+# -*- 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, raises, skip
+
+try:
+    from collections import UserDict
+except ImportError:
+    # Python2
+    from UserDict import UserDict
+
+
+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:
+        skip("test_numpy_2d is skipped")
+
+
+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:
+        skip("test_numpy_2d_firstrow is skipped")
+
+
+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:
+        skip("test_numpy_2d_keys is skipped")
+
+
+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:
+        skip("test_numpy_2d_keys is skipped")
+
+
+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:
+        skip("test_numpy_2d_keys is skipped")
+
+
+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:
+        skip("test_numpy_2d_keys is skipped")
+
+
+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:
+        skip("test_pandas is skipped")
+
+
+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:
+        skip("test_pandas_firstrow is skipped")
+
+
+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:
+        skip("test_pandas_keys is skipped")
+
+
+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:
+        skip("test_sqlite3 is skipped")
+
+
+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:
+        skip("test_sqlite3_keys is skipped")
+
+
+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_userdicts():
+    "Input: a list of UserDicts."
+    lod = [UserDict(foo=1, bar=2), UserDict(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_userdicts_keys():
+    "Input: a list of UserDicts."
+    lod = [UserDict(foo=1, bar=2), UserDict(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 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)

+ 47 - 0
desktop/core/ext-py/tabulate-0.8.9/test/test_internal.py

@@ -0,0 +1,47 @@
+# -*- 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
+
+
+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)

+ 1615 - 0
desktop/core/ext-py/tabulate-0.8.9/test/test_output.py

@@ -0,0 +1,1615 @@
+# -*- 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, raises, skip
+
+
+# _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_links():
+    "Output: plain with multiline cells with links and headers"
+    table = [[2, "foo\nbar"]]
+    headers = ("more\nspam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\", "more spam\n& eggs")
+    expected = "\n".join(
+        [
+            "       more  more spam",
+            "  spam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\  & 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_2():
+    "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_links():
+    "Output: simple with multiline cells with links and headers"
+    table = [[2, "foo\nbar"]]
+    headers = ("more\nspam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\", "more spam\n& eggs")
+    expected = "\n".join(
+        [
+            "       more  more spam",
+            "  spam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\  & 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_github():
+    "Output: github with headers"
+    expected = "\n".join(
+        [
+            "| strings   |   numbers |",
+            "|-----------|-----------|",
+            "| spam      |   41.9999 |",
+            "| eggs      |  451      |",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="github")
+    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  # noqa
+    except ImportError:
+        skip("test_grid_wide_characters is skipped")
+    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_pretty():
+    "Output: pretty with headers"
+    expected = "\n".join(
+        [
+            "+---------+---------+",
+            "| strings | numbers |",
+            "+---------+---------+",
+            "|  spam   | 41.9999 |",
+            "|  eggs   |  451.0  |",
+            "+---------+---------+",
+        ]
+    )
+    result = tabulate(_test_table, _test_table_headers, tablefmt="pretty")
+    assert_equal(expected, result)
+
+
+def test_pretty_headerless():
+    "Output: pretty without headers"
+    expected = "\n".join(
+        [
+            "+------+---------+",
+            "| spam | 41.9999 |",
+            "| eggs |  451.0  |",
+            "+------+---------+",
+        ]
+    )
+    result = tabulate(_test_table, tablefmt="pretty")
+    assert_equal(expected, result)
+
+
+def test_pretty_multiline_headerless():
+    "Output: pretty 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, tablefmt="pretty")
+    assert_equal(expected, result)
+
+
+def test_pretty_multiline():
+    "Output: pretty 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="pretty")
+    assert_equal(expected, result)
+
+
+def test_pretty_multiline_with_links():
+    "Output: pretty with multiline cells with headers"
+    table = [[2, "foo\nbar"]]
+    headers = ("more\nspam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\", "more spam\n& eggs")
+    expected = "\n".join(
+        [
+            "+-----------+-----------+",
+            "|   more    | more spam |",
+            "| spam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\ |  & eggs   |",
+            "+-----------+-----------+",
+            "|     2     |    foo    |",
+            "|           |    bar    |",
+            "+-----------+-----------+",
+        ]
+    )
+    result = tabulate(table, headers, tablefmt="pretty")
+    assert_equal(expected, result)
+
+
+def test_pretty_multiline_with_empty_cells():
+    "Output: pretty 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="pretty")
+    assert_equal(expected, result)
+
+
+def test_pretty_multiline_with_empty_cells_headerless():
+    "Output: pretty 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="pretty")
+    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_links():
+    "Output: rst with multiline cells with headers"
+    table = [[2, "foo\nbar"]]
+    headers = ("more\nspam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\", "more spam\n& eggs")
+    expected = "\n".join(
+        [
+            "===========  ===========",
+            "       more  more spam",
+            "  spam \x1b]8;;target\x1b\\eggs\x1b]8;;\x1b\\  & 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)
+
+
+_test_table_html_headers = ["<strings>", "<&numbers&>"]
+_test_table_html = [["spam >", 41.9999], ["eggs &", 451.0]]
+_test_table_unsafehtml_headers = ["strings", "numbers"]
+_test_table_unsafehtml = [
+    ["spam", '<font color="red">41.9999</font>'],
+    ["eggs", '<font color="red">451.0</font>'],
+]
+
+
+def test_html():
+    "Output: html with headers"
+    expected = "\n".join(
+        [
+            "<table>",
+            "<thead>",
+            '<tr><th>&lt;strings&gt;  </th><th style="text-align: right;">  &lt;&amp;numbers&amp;&gt;</th></tr>',  # noqa
+            "</thead>",
+            "<tbody>",
+            '<tr><td>spam &gt;     </td><td style="text-align: right;">      41.9999</td></tr>',
+            '<tr><td>eggs &amp;     </td><td style="text-align: right;">     451     </td></tr>',
+            "</tbody>",
+            "</table>",
+        ]
+    )
+    result = tabulate(_test_table_html, _test_table_html_headers, tablefmt="html")
+    assert_equal(expected, result)
+    assert hasattr(result, "_repr_html_")
+    assert result._repr_html_() == result.str
+
+
+def test_unsafehtml():
+    "Output: unsafe html with headers"
+    expected = "\n".join(
+        [
+            "<table>",
+            "<thead>",
+            "<tr><th>strings  </th><th>numbers                         </th></tr>",  # noqa
+            "</thead>",
+            "<tbody>",
+            '<tr><td>spam     </td><td><font color="red">41.9999</font></td></tr>',
+            '<tr><td>eggs     </td><td><font color="red">451.0</font>  </td></tr>',
+            "</tbody>",
+            "</table>",
+        ]
+    )
+    result = tabulate(
+        _test_table_unsafehtml, _test_table_unsafehtml_headers, tablefmt="unsafehtml"
+    )
+    assert_equal(expected, result)
+    assert hasattr(result, "_repr_html_")
+    assert result._repr_html_() == result.str
+
+
+def test_html_headerless():
+    "Output: html without headers"
+    expected = "\n".join(
+        [
+            "<table>",
+            "<tbody>",
+            '<tr><td>spam &gt;</td><td style="text-align: right;"> 41.9999</td></tr>',
+            '<tr><td>eggs &amp;</td><td style="text-align: right;">451     </td></tr>',
+            "</tbody>",
+            "</table>",
+        ]
+    )
+    result = tabulate(_test_table_html, tablefmt="html")
+    assert_equal(expected, result)
+    assert hasattr(result, "_repr_html_")
+    assert result._repr_html_() == result.str
+
+
+def test_unsafehtml_headerless():
+    "Output: unsafe html without headers"
+    expected = "\n".join(
+        [
+            "<table>",
+            "<tbody>",
+            '<tr><td>spam</td><td><font color="red">41.9999</font></td></tr>',
+            '<tr><td>eggs</td><td><font color="red">451.0</font>  </td></tr>',
+            "</tbody>",
+            "</table>",
+        ]
+    )
+    result = tabulate(_test_table_unsafehtml, tablefmt="unsafehtml")
+    assert_equal(expected, result)
+    assert hasattr(result, "_repr_html_")
+    assert result._repr_html_() == result.str
+
+
+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_colalign_multi():
+    "Output: string columns with custom colalign"
+    result = tabulate(
+        [["one", "two"], ["three", "four"]], colalign=("right",), tablefmt="plain"
+    )
+    expected = "  one  two\nthree  four"
+    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:
+        skip("test_pandas_with_index is skipped")
+
+
+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=pandas.Index(["a", "b"], name="index"),
+        )
+        expected = "\n".join(
+            [
+                "string      number",
+                "--------  --------",
+                "one              1",
+                "two            nan",
+            ]
+        )
+        result = tabulate(df, headers="keys", showindex=False)
+        assert_equal(expected, result)
+    except ImportError:
+        skip("test_pandas_without_index is skipped")
+
+
+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:
+        skip("test_pandas_rst_with_index is skipped")
+
+
+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:
+        skip("test_pandas_rst_with_index is skipped")
+
+
+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
+    with raises(ValueError):
+        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
+    with raises(ValueError):
+        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)

+ 471 - 0
desktop/core/ext-py/tabulate-0.8.9/test/test_regression.py

@@ -0,0 +1,471 @@
+# -*- coding: utf-8 -*-
+
+"""Regression tests."""
+
+from __future__ import print_function
+from __future__ import unicode_literals
+from tabulate import tabulate, _text_type, _long_type, TableFormat, Line, DataRow
+from common import assert_equal, assert_in, skip
+
+
+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_alignment_of_link_cells():
+    "Regression: Align links as if they were colorless."
+    linktable = [
+        ("test", 42, "\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\"),
+        ("test", 101, "\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\"),
+    ]
+    linkheaders = ("test", "\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\", "test")
+    formatted = tabulate(linktable, linkheaders, "grid")
+    expected = "\n".join(
+        [
+            "+--------+--------+--------+",
+            "| test   |   \x1b]8;;target\x1b\\test\x1b]8;;\x1b\\ | test   |",
+            "+========+========+========+",
+            "| test   |     42 | \x1b]8;;target\x1b\\test\x1b]8;;\x1b\\   |",
+            "+--------+--------+--------+",
+            "| test   |    101 | \x1b]8;;target\x1b\\test\x1b]8;;\x1b\\   |",
+            "+--------+--------+--------+",
+        ]
+    )
+    print("expected: %r\n\ngot:      %r\n" % (expected, formatted))
+    assert_equal(expected, formatted)
+
+
+def test_alignment_of_link_text_cells():
+    "Regression: Align links as if they were colorless."
+    linktable = [
+        ("test", 42, "1\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\2"),
+        ("test", 101, "3\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\4"),
+    ]
+    linkheaders = ("test", "5\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\6", "test")
+    formatted = tabulate(linktable, linkheaders, "grid")
+    expected = "\n".join(
+        [
+            "+--------+----------+--------+",
+            "| test   |   5\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\6 | test   |",
+            "+========+==========+========+",
+            "| test   |       42 | 1\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\2 |",
+            "+--------+----------+--------+",
+            "| test   |      101 | 3\x1b]8;;target\x1b\\test\x1b]8;;\x1b\\4 |",
+            "+--------+----------+--------+",
+        ]
+    )
+    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_alignment_of_decimal_numbers_with_commas():
+    "Regression: alignment for decimal numbers with comma separators"
+    skip("test is temporarily disable until the feature is reimplemented")
+    # table = [["c1r1", "14502.05"], ["c1r2", 105]]
+    # result = tabulate(table, tablefmt="grid", floatfmt=',.2f')
+    # expected = "\n".join(
+    #    ['+------+-----------+', '| c1r1 | 14,502.05 |',
+    #    '+------+-----------+', '| c1r2 |    105.00 |',
+    #    '+------+-----------+']
+    # )
+    # 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  # noqa
+
+        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:
+        skip("test_mix_normal_and_wide_characters is skipped (requires wcwidth lib)")
+
+
+def test_multiline_with_wide_characters():
+    "Regression: multiline tables with varying number of wide characters (github issue #28)"
+    try:
+        import wcwidth  # noqa
+
+        table = [["가나\n가ab", "가나", "가나"]]
+        result = tabulate(table, tablefmt="fancy_grid")
+        expected = "\n".join(
+            [
+                "╒══════╤══════╤══════╕",
+                "│ 가나 │ 가나 │ 가나 │",
+                "│ 가ab │      │      │",
+                "╘══════╧══════╧══════╛",
+            ]
+        )
+        assert_equal(result, expected)
+    except ImportError:
+        skip("test_multiline_with_wide_characters is skipped (requires wcwidth lib)")
+
+
+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 skip("")
+
+
+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(
+        [
+            "+---+---+---+",
+            "| 1 | 2 | 3 |",
+            "+---+---+---+",
+            "| 4 | \x1b[1;31m5\x1b[1;m | 6 |",
+            "+---+---+---+",
+            "| 7 | 8 | 9 |",
+            "+---+---+---+",
+        ]
+    )
+    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(
+        [
+            "====  =====",
+            "..      val",
+            "====  =====",
+            "foo       1",
+            "..        2",
+            "bar       3",
+            "====  =====",
+        ]
+    )
+    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(["-  -  -  -", "1  2  3", "1  2", "1  2  3  4", "-  -  -  -"])
+    result = tabulate(table)
+    assert_equal(result, expected)
+
+
+def test_empty_pipe_table_with_columns():
+    "Regression: allow empty pipe tables with columns, like empty dataframes (github issue #15)"
+    table = []
+    headers = ["Col1", "Col2"]
+    expected = "\n".join(["| Col1   | Col2   |", "|--------|--------|"])
+    result = tabulate(table, headers, tablefmt="pipe")
+    assert_equal(result, expected)
+
+
+def test_custom_tablefmt():
+    "Regression: allow custom TableFormat that specifies with_header_hide (github issue #20)"
+    tablefmt = TableFormat(
+        lineabove=Line("", "-", "  ", ""),
+        linebelowheader=Line("", "-", "  ", ""),
+        linebetweenrows=None,
+        linebelow=Line("", "-", "  ", ""),
+        headerrow=DataRow("", "  ", ""),
+        datarow=DataRow("", "  ", ""),
+        padding=0,
+        with_header_hide=["lineabove", "linebelow"],
+    )
+    rows = [["foo", "bar"], ["baz", "qux"]]
+    expected = "\n".join(["A    B", "---  ---", "foo  bar", "baz  qux"])
+    result = tabulate(rows, headers=["A", "B"], tablefmt=tablefmt)
+    assert_equal(result, expected)
+
+
+def test_string_with_comma_between_digits_without_floatfmt_grouping_option():
+    "Regression: accept commas in numbers-as-text when grouping is not defined (github issue #110)"
+    table = [["126,000"]]
+    expected = "126,000"
+    result = tabulate(table, tablefmt="plain")
+    assert_equal(result, expected)  # no exception