aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoraozeritsky <aozeritsky@ydb.tech>2024-05-22 13:40:51 +0300
committeraozeritsky <aozeritsky@ydb.tech>2024-05-22 13:52:22 +0300
commit2b21072ba9e98cefcbfd28d197d0c4c1335a93d6 (patch)
tree69222745e7505b63a5b54774cf2295c081f08e77
parent2166bc25086cc3437a69d2d31bb7b6da10a65062 (diff)
downloadydb-2b21072ba9e98cefcbfd28d197d0c4c1335a93d6.tar.gz
Add contrib/python/prettytable
8db8e29fdff582b08a53f0952a29362e0c760185
-rw-r--r--contrib/python/prettytable/py2/.dist-info/METADATA619
-rw-r--r--contrib/python/prettytable/py2/.dist-info/top_level.txt1
-rw-r--r--contrib/python/prettytable/py2/.yandex_meta/yamaker.yaml2
-rw-r--r--contrib/python/prettytable/py2/COPYING30
-rw-r--r--contrib/python/prettytable/py2/README.md587
-rw-r--r--contrib/python/prettytable/py2/prettytable.py2010
-rw-r--r--contrib/python/prettytable/py2/tests/prettytable_test.py859
-rw-r--r--contrib/python/prettytable/py2/tests/ya.make15
-rw-r--r--contrib/python/prettytable/py2/ya.make33
-rw-r--r--contrib/python/prettytable/py3/.dist-info/METADATA732
-rw-r--r--contrib/python/prettytable/py3/.dist-info/top_level.txt1
-rw-r--r--contrib/python/prettytable/py3/LICENSE30
-rw-r--r--contrib/python/prettytable/py3/README.md700
-rw-r--r--contrib/python/prettytable/py3/prettytable/__init__.py57
-rw-r--r--contrib/python/prettytable/py3/prettytable/colortable.py97
-rw-r--r--contrib/python/prettytable/py3/prettytable/prettytable.py2587
-rw-r--r--contrib/python/prettytable/py3/prettytable/py.typed0
-rw-r--r--contrib/python/prettytable/py3/tests/test_colortable.py96
-rw-r--r--contrib/python/prettytable/py3/tests/test_prettytable.py2196
-rw-r--r--contrib/python/prettytable/py3/tests/ya.make17
-rw-r--r--contrib/python/prettytable/py3/ya.make35
-rw-r--r--contrib/python/prettytable/ya.make20
22 files changed, 10724 insertions, 0 deletions
diff --git a/contrib/python/prettytable/py2/.dist-info/METADATA b/contrib/python/prettytable/py2/.dist-info/METADATA
new file mode 100644
index 0000000000..0096ff1501
--- /dev/null
+++ b/contrib/python/prettytable/py2/.dist-info/METADATA
@@ -0,0 +1,619 @@
+Metadata-Version: 2.1
+Name: prettytable
+Version: 1.0.1
+Summary: A simple Python library for easily displaying tabular data in a visually appealing ASCII table format
+Home-page: https://github.com/jazzband/prettytable
+Author: Luke Maurits
+Author-email: luke@maurits.id.au
+Maintainer: Jazzband
+License: BSD (3 clause)
+Project-URL: Source, https://github.com/jazzband/prettytable
+Platform: UNKNOWN
+Classifier: Programming Language :: Python
+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: License :: OSI Approved :: BSD License
+Classifier: Topic :: Text Processing
+Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
+Description-Content-Type: text/markdown
+Requires-Dist: setuptools
+Requires-Dist: wcwidth
+Provides-Extra: tests
+Requires-Dist: pytest ; extra == 'tests'
+Requires-Dist: pytest-cov ; extra == 'tests'
+
+# PrettyTable
+
+[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
+[![PyPI version](https://img.shields.io/pypi/v/prettytable.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![Supported Python versions](https://img.shields.io/pypi/pyversions/prettytable.svg?logo=python&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![PyPI downloads](https://img.shields.io/pypi/dm/prettytable.svg)](https://pypistats.org/packages/prettytable)
+[![Travis CI Status](https://img.shields.io/travis/jazzband/prettytable/master?label=Travis%20CI&logo=travis)](https://travis-ci.org/jazzband/prettytable)
+[![GitHub Actions status](https://github.com/jazzband/prettytable/workflows/Test/badge.svg)](https://github.com/jazzband/prettytable/actions)
+[![codecov](https://codecov.io/gh/jazzband/prettytable/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/prettytable)
+[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+## Installation
+
+Install via pip:
+
+ pip install -U prettytable
+
+Install latest development version:
+
+ pip install -U git+https://github.com/jazzband/prettytable
+
+Or from `requirements.txt`:
+
+ -e git://github.com/jazzband/prettytable.git#egg=prettytable
+
+## Tutorial on how to use the PrettyTable API
+
+### Getting your data into (and out of) the table
+
+Let's suppose you have a shiny new PrettyTable:
+
+```python
+from prettytable import PrettyTable
+x = PrettyTable()
+```
+
+and you want to put some data into it. You have a few options.
+
+#### Row by row
+
+You can add data one row at a time. To do this you can set the field names
+first using the `field_names` attribute, and then add the rows one at a time
+using the `add_row` method:
+
+```python
+x.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+x.add_row(["Adelaide",1295, 1158259, 600.5])
+x.add_row(["Brisbane",5905, 1857594, 1146.4])
+x.add_row(["Darwin", 112, 120900, 1714.7])
+x.add_row(["Hobart", 1357, 205556, 619.5])
+x.add_row(["Sydney", 2058, 4336374, 1214.8])
+x.add_row(["Melbourne", 1566, 3806092, 646.9])
+x.add_row(["Perth", 5386, 1554769, 869.4])
+```
+
+#### Column by column
+
+You can add data one column at a time as well. To do this you use the
+`add_column` method, which takes two arguments - a string which is the name for
+the field the column you are adding corresponds to, and a list or tuple which
+contains the column data"
+
+```python
+x.add_column("City name",
+["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
+x.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
+x.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092,
+1554769])
+x.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9,
+869.4])
+```
+
+#### Mixing and matching
+
+If you really want to, you can even mix and match `add_row` and `add_column`
+and build some of your table in one way and some of it in the other. There's a
+unit test which makes sure that doing things this way will always work out
+nicely as if you'd done it using just one of the two approaches. Tables built
+this way are kind of confusing for other people to read, though, so don't do
+this unless you have a good reason.
+
+#### Importing data from a CSV file
+
+If you have your table data in a comma separated values file (.csv), you can
+read this data into a PrettyTable like this:
+
+```python
+from prettytable import from_csv
+fp = open("myfile.csv", "r")
+mytable = from_csv(fp)
+fp.close()
+```
+
+#### Importing data from a database cursor
+
+If you have your table data in a database which you can access using a library which confirms to the Python DB-API (e.g. an SQLite database accessible using the sqlite module), then you can build a PrettyTable using a cursor object, like this:
+
+```python
+import sqlite3
+from prettytable import from_cursor
+
+connection = sqlite3.connect("mydb.db")
+cursor = connection.cursor()
+cursor.execute("SELECT field1, field2, field3 FROM my_table")
+mytable = from_cursor(cursor)
+```
+
+#### Getting data out
+
+There are three ways to get data out of a PrettyTable, in increasing order of
+completeness:
+
+ * The `del_row` method takes an integer index of a single row to delete.
+ * The `del_column` method takes a field name of a single column to delete.
+ * The `clear_rows` method takes no arguments and deletes all the rows in the
+table - but keeps the field names as they were so you that you can repopulate
+it with the same kind of data.
+ * The `clear` method takes no arguments and deletes all rows and all field
+names. It's not quite the same as creating a fresh table instance, though -
+style related settings, discussed later, are maintained.
+
+### Displaying your table in ASCII form
+
+PrettyTable's main goal is to let you print tables in an attractive ASCII form,
+like this:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+You can print tables like this to `stdout` or get string representations of
+them.
+
+#### Printing
+
+To print a table in ASCII form, you can just do this:
+
+```python
+print x
+```
+
+in Python 2.x or:
+
+```python
+print(x)
+```
+
+in Python 3.x.
+
+The old `x.printt()` method from versions 0.5 and earlier has been removed.
+
+To pass options changing the look of the table, use the `get_string()` method
+documented below:
+
+```python
+print(x.get_string())
+```
+
+#### Stringing
+
+If you don't want to actually print your table in ASCII form but just get a
+string containing what _would_ be printed if you use `print(x)`, you can use
+the `get_string` method:
+
+```python
+mystring = x.get_string()
+```
+
+This string is guaranteed to look exactly the same as what would be printed by
+doing `print(x)`. You can now do all the usual things you can do with a
+string, like write your table to a file or insert it into a GUI.
+
+#### Controlling which data gets displayed
+
+If you like, you can restrict the output of `print(x)` or `x.get_string` to
+only the fields or rows you like.
+
+The `fields` argument to these methods takes a list of field names to be
+printed:
+
+```python
+print(x.get_string(fields=["City name", "Population"]))
+```
+
+gives:
+
+```
++-----------+------------+
+| City name | Population |
++-----------+------------+
+| Adelaide | 1158259 |
+| Brisbane | 1857594 |
+| Darwin | 120900 |
+| Hobart | 205556 |
+| Melbourne | 3806092 |
+| Perth | 1554769 |
+| Sydney | 4336374 |
++-----------+------------+
+```
+
+The `start` and `end` arguments take the index of the first and last row to
+print respectively. Note that the indexing works like Python list slicing - to
+print the 2nd, 3rd and 4th rows of the table, set `start` to 1 (the first row
+is row 0, so the second is row 1) and set `end` to 4 (the index of the 4th row,
+plus 1):
+
+```python
+print(x.get_string(start=1,end=4))
+```
+
+prints:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
++-----------+------+------------+-----------------+
+```
+
+#### Changing the alignment of columns
+
+By default, all columns in a table are centre aligned.
+
+##### All columns at once
+
+You can change the alignment of all the columns in a table at once by assigning
+a one character string to the `align` attribute. The allowed strings are "l",
+"r" and "c" for left, right and centre alignment, respectively:
+
+```python
+x.align = "r"
+print(x)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### One column at a time
+
+You can also change the alignment of individual columns based on the
+corresponding field name by treating the `align` attribute as if it were a
+dictionary.
+
+```python
+x.align["City name"] = "l"
+x.align["Area"] = "c"
+x.align["Population"] = "r"
+x.align["Annual Rainfall"] = "c"
+print(x)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### Sorting your table by a field
+
+You can make sure that your ASCII tables are produced with the data sorted by
+one particular field by giving `get_string` a `sortby` keyword argument, which
+ must be a string containing the name of one field.
+
+For example, to print the example table we built earlier of Australian capital
+city data, so that the most populated city comes last, we can do this:
+
+```python
+print(x.get_string(sortby="Population"))
+```
+
+to get
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+If we want the most populated city to come _first_, we can also give a
+`reversesort=True` argument.
+
+If you _always_ want your tables to be sorted in a certain way, you can make
+the setting long term like this:
+
+```python
+x.sortby = "Population"
+print(x)
+print(x)
+print(x)
+```
+
+All three tables printed by this code will be sorted by population (you could
+do `x.reversesort = True` as well, if you wanted). The behaviour will persist
+until you turn it off:
+
+```python
+x.sortby = None
+```
+
+If you want to specify a custom sorting function, you can use the `sort_key`
+keyword argument. Pass this a function which accepts two lists of values
+and returns a negative or positive value depending on whether the first list
+should appear before or after the second one. If your table has n columns,
+each list will have n+1 elements. Each list corresponds to one row of the
+table. The first element will be whatever data is in the relevant row, in
+the column specified by the `sort_by` argument. The remaining n elements
+are the data in each of the table's columns, in order, including a repeated
+instance of the data in the `sort_by` column.
+
+### Changing the appearance of your table - the easy way
+
+By default, PrettyTable produces ASCII tables that look like the ones used in
+SQL database shells. But if can print them in a variety of other formats as
+well. If the format you want to use is common, PrettyTable makes this very
+easy for you to do using the `set_style` method. If you want to produce an
+uncommon table, you'll have to do things slightly harder (see later).
+
+#### Setting a table style
+
+You can set the style for your table using the `set_style` method before any
+calls to `print` or `get_string`. Here's how to print a table in a format
+which works nicely with Microsoft Word's "Convert to table" feature:
+
+```python
+from prettytable import MSWORD_FRIENDLY
+x.set_style(MSWORD_FRIENDLY)
+print(x)
+```
+
+In addition to `MSWORD_FRIENDLY` there are currently two other in-built styles
+you can use for your tables:
+
+ * `DEFAULT` - The default look, used to undo any style changes you may have
+made
+ * `PLAIN_COLUMNS` - A borderless style that works well with command line
+programs for columnar data
+ * `MARKDOWN` - A style that follows Markdown syntax
+ * `ORGMODE` - A table style that fits [Org mode](https://orgmode.org/) syntax
+
+Other styles are likely to appear in future releases.
+
+### Changing the appearance of your table - the hard way
+
+If you want to display your table in a style other than one of the in-built
+styles listed above, you'll have to set things up the hard way.
+
+Don't worry, it's not really that hard!
+
+#### Style options
+
+PrettyTable has a number of style options which control various aspects of how
+tables are displayed. You have the freedom to set each of these options
+individually to whatever you prefer. The `set_style` method just does this
+automatically for you.
+
+The options are these:
+
+ * `border` - A boolean option (must be `True` or `False`). Controls whether
+ or not a border is drawn around the table.
+ * `header` - A boolean option (must be `True` or `False`). Controls whether
+ or not the first row of the table is a header showing the names of all the
+ fields.
+ * `hrules` - Controls printing of horizontal rules after rows. Allowed
+ values: FRAME, HEADER, ALL, NONE - note that these are variables defined
+ inside the `prettytable` module so make sure you import them or use
+ `prettytable.FRAME` etc.
+ * `vrules` - Controls printing of vertical rules between columns. Allowed
+ values: FRAME, ALL, NONE.
+ * `int_format` - A string which controls the way integer data is printed.
+ This works like: `print("%<int_format>d" % data)`
+ * `float_format` - A string which controls the way floating point data is
+ printed. This works like: `print("%<float_format>f" % data)`
+ * `padding_width` - Number of spaces on either side of column data (only used
+ if left and right paddings are None).
+ * `left_padding_width` - Number of spaces on left hand side of column data.
+ * `right_padding_width` - Number of spaces on right hand side of column data.
+ * `vertical_char` - Single character string used to draw vertical lines.
+ Default is `|`.
+ * `horizontal_char` - Single character string used to draw horizontal lines.
+ Default is `-`.
+ * `junction_char` - Single character string used to draw line junctions.
+ Default is `+`.
+
+You can set the style options to your own settings in two ways:
+
+#### Setting style options for the long term
+
+If you want to print your table with a different style several times, you can
+set your option for the "long term" just by changing the appropriate
+attributes. If you never want your tables to have borders you can do this:
+
+```python
+x.border = False
+print(x)
+print(x)
+print(x)
+```
+
+Neither of the 3 tables printed by this will have borders, even if you do
+things like add extra rows in between them. The lack of borders will last until
+you do:
+
+```python
+x.border = True
+```
+
+to turn them on again. This sort of long term setting is exactly how
+`set_style` works. `set_style` just sets a bunch of attributes to pre-set
+values for you.
+
+Note that if you know what style options you want at the moment you are
+creating your table, you can specify them using keyword arguments to the
+constructor. For example, the following two code blocks are equivalent:
+
+```python
+x = PrettyTable()
+x.border = False
+x.header = False
+x.padding_width = 5
+
+x = PrettyTable(border=False, header=False, padding_width=5)
+```
+
+#### Changing style options just once
+
+If you don't want to make long term style changes by changing an attribute like
+in the previous section, you can make changes that last for just one
+``get_string`` by giving those methods keyword arguments. To print two
+"normal" tables with one borderless table between them, you could do this:
+
+```python
+print(x)
+print(x.get_string(border=False))
+print(x)
+```
+
+### Displaying your table in JSON
+
+PrettyTable will also print your tables in JSON, as a list of fields and an array
+of rows. Just like in ASCII form, you can actually get a string representation - just use
+`get_json_string()`.
+
+### Displaying your table in HTML form
+
+PrettyTable will also print your tables in HTML form, as `<table>`s. Just like
+in ASCII form, you can actually get a string representation - just use
+`get_html_string()`. HTML printing supports the `fields`, `start`, `end`,
+`sortby` and `reversesort` arguments in exactly the same way as ASCII printing.
+
+#### Styling HTML tables
+
+By default, PrettyTable outputs HTML for "vanilla" tables. The HTML code is
+quite simple. It looks like this:
+
+```html
+<table>
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ <tr>
+ <td>Adelaide</td>
+ <td>1295</td>
+ <td>1158259</td>
+ <td>600.5</td>
+ <tr>
+ <td>Brisbane</td>
+ <td>5905</td>
+ <td>1857594</td>
+ <td>1146.4</td>
+ ...
+ ...
+ ...
+</table>
+```
+
+If you like, you can ask PrettyTable to do its best to mimic the style options
+that your table has set using inline CSS. This is done by giving a
+`format=True` keyword argument to `get_html_string` method. Note that if you
+_always_ want to print formatted HTML you can do:
+
+```python
+x.format = True
+```
+
+and the setting will persist until you turn it off.
+
+Just like with ASCII tables, if you want to change the table's style for just
+one `get_html_string` you can pass those methods keyword arguments - exactly
+like `print` and `get_string`.
+
+#### Setting HTML attributes
+
+You can provide a dictionary of HTML attribute name/value pairs to the
+`get_html_string` method using the `attributes` keyword argument.
+This lets you specify common HTML attributes like `name`, `id` and
+`class` that can be used for linking to your tables or customising their
+appearance using CSS. For example:
+
+```python
+print(x.get_html_string(attributes={"name":"my_table", "class":"red_table"}))
+```
+
+will print:
+
+```html
+<table name="my_table" class="red_table">
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ ...
+ ...
+ ...
+</table>
+```
+
+### Miscellaneous things
+
+#### Copying a table
+
+You can call the `copy` method on a PrettyTable object without arguments to
+return an identical independent copy of the table.
+
+If you want a copy of a PrettyTable object with just a subset of the rows,
+you can use list slicing notation:
+
+```python
+new_table = old_table[0:5]
+```
+
+## Contributing
+
+After editing files, use the [black](https://github.com/psf/black) linter to auto-format changed lines.
+
+```sh
+pip install black
+black prettytable*.py
+```
+
+
diff --git a/contrib/python/prettytable/py2/.dist-info/top_level.txt b/contrib/python/prettytable/py2/.dist-info/top_level.txt
new file mode 100644
index 0000000000..deb2d14cce
--- /dev/null
+++ b/contrib/python/prettytable/py2/.dist-info/top_level.txt
@@ -0,0 +1 @@
+prettytable
diff --git a/contrib/python/prettytable/py2/.yandex_meta/yamaker.yaml b/contrib/python/prettytable/py2/.yandex_meta/yamaker.yaml
new file mode 100644
index 0000000000..2f9202c8eb
--- /dev/null
+++ b/contrib/python/prettytable/py2/.yandex_meta/yamaker.yaml
@@ -0,0 +1,2 @@
+keep:
+ - tests/prettytable_test.py
diff --git a/contrib/python/prettytable/py2/COPYING b/contrib/python/prettytable/py2/COPYING
new file mode 100644
index 0000000000..cb6fed3eb8
--- /dev/null
+++ b/contrib/python/prettytable/py2/COPYING
@@ -0,0 +1,30 @@
+# Copyright (c) 2009-2014 Luke Maurits <luke@maurits.id.au>
+# All rights reserved.
+# With contributions from:
+# * Chris Clark
+# * Klein Stephane
+# * John Filleau
+# * Vladimir Vrzić
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
diff --git a/contrib/python/prettytable/py2/README.md b/contrib/python/prettytable/py2/README.md
new file mode 100644
index 0000000000..32c724b286
--- /dev/null
+++ b/contrib/python/prettytable/py2/README.md
@@ -0,0 +1,587 @@
+# PrettyTable
+
+[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
+[![PyPI version](https://img.shields.io/pypi/v/prettytable.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![Supported Python versions](https://img.shields.io/pypi/pyversions/prettytable.svg?logo=python&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![PyPI downloads](https://img.shields.io/pypi/dm/prettytable.svg)](https://pypistats.org/packages/prettytable)
+[![Travis CI Status](https://img.shields.io/travis/jazzband/prettytable/master?label=Travis%20CI&logo=travis)](https://travis-ci.org/jazzband/prettytable)
+[![GitHub Actions status](https://github.com/jazzband/prettytable/workflows/Test/badge.svg)](https://github.com/jazzband/prettytable/actions)
+[![codecov](https://codecov.io/gh/jazzband/prettytable/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/prettytable)
+[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+## Installation
+
+Install via pip:
+
+ pip install -U prettytable
+
+Install latest development version:
+
+ pip install -U git+https://github.com/jazzband/prettytable
+
+Or from `requirements.txt`:
+
+ -e git://github.com/jazzband/prettytable.git#egg=prettytable
+
+## Tutorial on how to use the PrettyTable API
+
+### Getting your data into (and out of) the table
+
+Let's suppose you have a shiny new PrettyTable:
+
+```python
+from prettytable import PrettyTable
+x = PrettyTable()
+```
+
+and you want to put some data into it. You have a few options.
+
+#### Row by row
+
+You can add data one row at a time. To do this you can set the field names
+first using the `field_names` attribute, and then add the rows one at a time
+using the `add_row` method:
+
+```python
+x.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+x.add_row(["Adelaide",1295, 1158259, 600.5])
+x.add_row(["Brisbane",5905, 1857594, 1146.4])
+x.add_row(["Darwin", 112, 120900, 1714.7])
+x.add_row(["Hobart", 1357, 205556, 619.5])
+x.add_row(["Sydney", 2058, 4336374, 1214.8])
+x.add_row(["Melbourne", 1566, 3806092, 646.9])
+x.add_row(["Perth", 5386, 1554769, 869.4])
+```
+
+#### Column by column
+
+You can add data one column at a time as well. To do this you use the
+`add_column` method, which takes two arguments - a string which is the name for
+the field the column you are adding corresponds to, and a list or tuple which
+contains the column data"
+
+```python
+x.add_column("City name",
+["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
+x.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
+x.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092,
+1554769])
+x.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9,
+869.4])
+```
+
+#### Mixing and matching
+
+If you really want to, you can even mix and match `add_row` and `add_column`
+and build some of your table in one way and some of it in the other. There's a
+unit test which makes sure that doing things this way will always work out
+nicely as if you'd done it using just one of the two approaches. Tables built
+this way are kind of confusing for other people to read, though, so don't do
+this unless you have a good reason.
+
+#### Importing data from a CSV file
+
+If you have your table data in a comma separated values file (.csv), you can
+read this data into a PrettyTable like this:
+
+```python
+from prettytable import from_csv
+fp = open("myfile.csv", "r")
+mytable = from_csv(fp)
+fp.close()
+```
+
+#### Importing data from a database cursor
+
+If you have your table data in a database which you can access using a library which confirms to the Python DB-API (e.g. an SQLite database accessible using the sqlite module), then you can build a PrettyTable using a cursor object, like this:
+
+```python
+import sqlite3
+from prettytable import from_cursor
+
+connection = sqlite3.connect("mydb.db")
+cursor = connection.cursor()
+cursor.execute("SELECT field1, field2, field3 FROM my_table")
+mytable = from_cursor(cursor)
+```
+
+#### Getting data out
+
+There are three ways to get data out of a PrettyTable, in increasing order of
+completeness:
+
+ * The `del_row` method takes an integer index of a single row to delete.
+ * The `del_column` method takes a field name of a single column to delete.
+ * The `clear_rows` method takes no arguments and deletes all the rows in the
+table - but keeps the field names as they were so you that you can repopulate
+it with the same kind of data.
+ * The `clear` method takes no arguments and deletes all rows and all field
+names. It's not quite the same as creating a fresh table instance, though -
+style related settings, discussed later, are maintained.
+
+### Displaying your table in ASCII form
+
+PrettyTable's main goal is to let you print tables in an attractive ASCII form,
+like this:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+You can print tables like this to `stdout` or get string representations of
+them.
+
+#### Printing
+
+To print a table in ASCII form, you can just do this:
+
+```python
+print x
+```
+
+in Python 2.x or:
+
+```python
+print(x)
+```
+
+in Python 3.x.
+
+The old `x.printt()` method from versions 0.5 and earlier has been removed.
+
+To pass options changing the look of the table, use the `get_string()` method
+documented below:
+
+```python
+print(x.get_string())
+```
+
+#### Stringing
+
+If you don't want to actually print your table in ASCII form but just get a
+string containing what _would_ be printed if you use `print(x)`, you can use
+the `get_string` method:
+
+```python
+mystring = x.get_string()
+```
+
+This string is guaranteed to look exactly the same as what would be printed by
+doing `print(x)`. You can now do all the usual things you can do with a
+string, like write your table to a file or insert it into a GUI.
+
+#### Controlling which data gets displayed
+
+If you like, you can restrict the output of `print(x)` or `x.get_string` to
+only the fields or rows you like.
+
+The `fields` argument to these methods takes a list of field names to be
+printed:
+
+```python
+print(x.get_string(fields=["City name", "Population"]))
+```
+
+gives:
+
+```
++-----------+------------+
+| City name | Population |
++-----------+------------+
+| Adelaide | 1158259 |
+| Brisbane | 1857594 |
+| Darwin | 120900 |
+| Hobart | 205556 |
+| Melbourne | 3806092 |
+| Perth | 1554769 |
+| Sydney | 4336374 |
++-----------+------------+
+```
+
+The `start` and `end` arguments take the index of the first and last row to
+print respectively. Note that the indexing works like Python list slicing - to
+print the 2nd, 3rd and 4th rows of the table, set `start` to 1 (the first row
+is row 0, so the second is row 1) and set `end` to 4 (the index of the 4th row,
+plus 1):
+
+```python
+print(x.get_string(start=1,end=4))
+```
+
+prints:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
++-----------+------+------------+-----------------+
+```
+
+#### Changing the alignment of columns
+
+By default, all columns in a table are centre aligned.
+
+##### All columns at once
+
+You can change the alignment of all the columns in a table at once by assigning
+a one character string to the `align` attribute. The allowed strings are "l",
+"r" and "c" for left, right and centre alignment, respectively:
+
+```python
+x.align = "r"
+print(x)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### One column at a time
+
+You can also change the alignment of individual columns based on the
+corresponding field name by treating the `align` attribute as if it were a
+dictionary.
+
+```python
+x.align["City name"] = "l"
+x.align["Area"] = "c"
+x.align["Population"] = "r"
+x.align["Annual Rainfall"] = "c"
+print(x)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### Sorting your table by a field
+
+You can make sure that your ASCII tables are produced with the data sorted by
+one particular field by giving `get_string` a `sortby` keyword argument, which
+ must be a string containing the name of one field.
+
+For example, to print the example table we built earlier of Australian capital
+city data, so that the most populated city comes last, we can do this:
+
+```python
+print(x.get_string(sortby="Population"))
+```
+
+to get
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+If we want the most populated city to come _first_, we can also give a
+`reversesort=True` argument.
+
+If you _always_ want your tables to be sorted in a certain way, you can make
+the setting long term like this:
+
+```python
+x.sortby = "Population"
+print(x)
+print(x)
+print(x)
+```
+
+All three tables printed by this code will be sorted by population (you could
+do `x.reversesort = True` as well, if you wanted). The behaviour will persist
+until you turn it off:
+
+```python
+x.sortby = None
+```
+
+If you want to specify a custom sorting function, you can use the `sort_key`
+keyword argument. Pass this a function which accepts two lists of values
+and returns a negative or positive value depending on whether the first list
+should appear before or after the second one. If your table has n columns,
+each list will have n+1 elements. Each list corresponds to one row of the
+table. The first element will be whatever data is in the relevant row, in
+the column specified by the `sort_by` argument. The remaining n elements
+are the data in each of the table's columns, in order, including a repeated
+instance of the data in the `sort_by` column.
+
+### Changing the appearance of your table - the easy way
+
+By default, PrettyTable produces ASCII tables that look like the ones used in
+SQL database shells. But if can print them in a variety of other formats as
+well. If the format you want to use is common, PrettyTable makes this very
+easy for you to do using the `set_style` method. If you want to produce an
+uncommon table, you'll have to do things slightly harder (see later).
+
+#### Setting a table style
+
+You can set the style for your table using the `set_style` method before any
+calls to `print` or `get_string`. Here's how to print a table in a format
+which works nicely with Microsoft Word's "Convert to table" feature:
+
+```python
+from prettytable import MSWORD_FRIENDLY
+x.set_style(MSWORD_FRIENDLY)
+print(x)
+```
+
+In addition to `MSWORD_FRIENDLY` there are currently two other in-built styles
+you can use for your tables:
+
+ * `DEFAULT` - The default look, used to undo any style changes you may have
+made
+ * `PLAIN_COLUMNS` - A borderless style that works well with command line
+programs for columnar data
+ * `MARKDOWN` - A style that follows Markdown syntax
+ * `ORGMODE` - A table style that fits [Org mode](https://orgmode.org/) syntax
+
+Other styles are likely to appear in future releases.
+
+### Changing the appearance of your table - the hard way
+
+If you want to display your table in a style other than one of the in-built
+styles listed above, you'll have to set things up the hard way.
+
+Don't worry, it's not really that hard!
+
+#### Style options
+
+PrettyTable has a number of style options which control various aspects of how
+tables are displayed. You have the freedom to set each of these options
+individually to whatever you prefer. The `set_style` method just does this
+automatically for you.
+
+The options are these:
+
+ * `border` - A boolean option (must be `True` or `False`). Controls whether
+ or not a border is drawn around the table.
+ * `header` - A boolean option (must be `True` or `False`). Controls whether
+ or not the first row of the table is a header showing the names of all the
+ fields.
+ * `hrules` - Controls printing of horizontal rules after rows. Allowed
+ values: FRAME, HEADER, ALL, NONE - note that these are variables defined
+ inside the `prettytable` module so make sure you import them or use
+ `prettytable.FRAME` etc.
+ * `vrules` - Controls printing of vertical rules between columns. Allowed
+ values: FRAME, ALL, NONE.
+ * `int_format` - A string which controls the way integer data is printed.
+ This works like: `print("%<int_format>d" % data)`
+ * `float_format` - A string which controls the way floating point data is
+ printed. This works like: `print("%<float_format>f" % data)`
+ * `padding_width` - Number of spaces on either side of column data (only used
+ if left and right paddings are None).
+ * `left_padding_width` - Number of spaces on left hand side of column data.
+ * `right_padding_width` - Number of spaces on right hand side of column data.
+ * `vertical_char` - Single character string used to draw vertical lines.
+ Default is `|`.
+ * `horizontal_char` - Single character string used to draw horizontal lines.
+ Default is `-`.
+ * `junction_char` - Single character string used to draw line junctions.
+ Default is `+`.
+
+You can set the style options to your own settings in two ways:
+
+#### Setting style options for the long term
+
+If you want to print your table with a different style several times, you can
+set your option for the "long term" just by changing the appropriate
+attributes. If you never want your tables to have borders you can do this:
+
+```python
+x.border = False
+print(x)
+print(x)
+print(x)
+```
+
+Neither of the 3 tables printed by this will have borders, even if you do
+things like add extra rows in between them. The lack of borders will last until
+you do:
+
+```python
+x.border = True
+```
+
+to turn them on again. This sort of long term setting is exactly how
+`set_style` works. `set_style` just sets a bunch of attributes to pre-set
+values for you.
+
+Note that if you know what style options you want at the moment you are
+creating your table, you can specify them using keyword arguments to the
+constructor. For example, the following two code blocks are equivalent:
+
+```python
+x = PrettyTable()
+x.border = False
+x.header = False
+x.padding_width = 5
+
+x = PrettyTable(border=False, header=False, padding_width=5)
+```
+
+#### Changing style options just once
+
+If you don't want to make long term style changes by changing an attribute like
+in the previous section, you can make changes that last for just one
+``get_string`` by giving those methods keyword arguments. To print two
+"normal" tables with one borderless table between them, you could do this:
+
+```python
+print(x)
+print(x.get_string(border=False))
+print(x)
+```
+
+### Displaying your table in JSON
+
+PrettyTable will also print your tables in JSON, as a list of fields and an array
+of rows. Just like in ASCII form, you can actually get a string representation - just use
+`get_json_string()`.
+
+### Displaying your table in HTML form
+
+PrettyTable will also print your tables in HTML form, as `<table>`s. Just like
+in ASCII form, you can actually get a string representation - just use
+`get_html_string()`. HTML printing supports the `fields`, `start`, `end`,
+`sortby` and `reversesort` arguments in exactly the same way as ASCII printing.
+
+#### Styling HTML tables
+
+By default, PrettyTable outputs HTML for "vanilla" tables. The HTML code is
+quite simple. It looks like this:
+
+```html
+<table>
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ <tr>
+ <td>Adelaide</td>
+ <td>1295</td>
+ <td>1158259</td>
+ <td>600.5</td>
+ <tr>
+ <td>Brisbane</td>
+ <td>5905</td>
+ <td>1857594</td>
+ <td>1146.4</td>
+ ...
+ ...
+ ...
+</table>
+```
+
+If you like, you can ask PrettyTable to do its best to mimic the style options
+that your table has set using inline CSS. This is done by giving a
+`format=True` keyword argument to `get_html_string` method. Note that if you
+_always_ want to print formatted HTML you can do:
+
+```python
+x.format = True
+```
+
+and the setting will persist until you turn it off.
+
+Just like with ASCII tables, if you want to change the table's style for just
+one `get_html_string` you can pass those methods keyword arguments - exactly
+like `print` and `get_string`.
+
+#### Setting HTML attributes
+
+You can provide a dictionary of HTML attribute name/value pairs to the
+`get_html_string` method using the `attributes` keyword argument.
+This lets you specify common HTML attributes like `name`, `id` and
+`class` that can be used for linking to your tables or customising their
+appearance using CSS. For example:
+
+```python
+print(x.get_html_string(attributes={"name":"my_table", "class":"red_table"}))
+```
+
+will print:
+
+```html
+<table name="my_table" class="red_table">
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ ...
+ ...
+ ...
+</table>
+```
+
+### Miscellaneous things
+
+#### Copying a table
+
+You can call the `copy` method on a PrettyTable object without arguments to
+return an identical independent copy of the table.
+
+If you want a copy of a PrettyTable object with just a subset of the rows,
+you can use list slicing notation:
+
+```python
+new_table = old_table[0:5]
+```
+
+## Contributing
+
+After editing files, use the [black](https://github.com/psf/black) linter to auto-format changed lines.
+
+```sh
+pip install black
+black prettytable*.py
+```
diff --git a/contrib/python/prettytable/py2/prettytable.py b/contrib/python/prettytable/py2/prettytable.py
new file mode 100644
index 0000000000..687b0b6083
--- /dev/null
+++ b/contrib/python/prettytable/py2/prettytable.py
@@ -0,0 +1,2010 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009-2014, Luke Maurits <luke@maurits.id.au>
+# All rights reserved.
+# With contributions from:
+# * Chris Clark
+# * Klein Stephane
+# * John Filleau
+# * Vladimir Vrzić
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import copy
+import csv
+import itertools
+import json
+import math
+import random
+import re
+import sys
+import textwrap
+
+import pkg_resources
+import wcwidth
+
+__version__ = pkg_resources.get_distribution(__name__).version
+py3k = sys.version_info[0] >= 3
+if py3k:
+ unicode = str
+ basestring = str
+ itermap = map
+ iterzip = zip
+ uni_chr = chr
+ from html import escape
+ from html.parser import HTMLParser
+ from io import StringIO
+else:
+ itermap = itertools.imap
+ iterzip = itertools.izip
+ uni_chr = unichr # noqa: F821
+ from cgi import escape
+
+ from HTMLParser import HTMLParser
+ from StringIO import StringIO
+
+# hrule styles
+FRAME = 0
+ALL = 1
+NONE = 2
+HEADER = 3
+
+# Table styles
+DEFAULT = 10
+MSWORD_FRIENDLY = 11
+PLAIN_COLUMNS = 12
+MARKDOWN = 13
+ORGMODE = 14
+RANDOM = 20
+
+_re = re.compile(r"\033\[[0-9;]*m")
+
+
+def _get_size(text):
+ lines = text.split("\n")
+ height = len(lines)
+ width = max(_str_block_width(line) for line in lines)
+ return width, height
+
+
+class PrettyTable(object):
+ def __init__(self, field_names=None, **kwargs):
+
+ """Return a new PrettyTable instance
+
+ Arguments:
+
+ encoding - Unicode encoding scheme used to decode any encoded input
+ title - optional table title
+ field_names - list or tuple of field names
+ fields - list or tuple of field names to include in displays
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ header - print a header showing field names (True or False)
+ header_style - stylisation to apply to field names in header
+ ("cap", "title", "upper", "lower" or None)
+ border - print a border around the table (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: FRAME, HEADER, ALL, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ min_table_width - minimum desired table width, in characters
+ max_table_width - maximum desired table width, in characters
+ padding_width - number of spaces on either side of column data
+ (only used if left and right paddings are None)
+ left_padding_width - number of spaces on left hand side of column data
+ right_padding_width - number of spaces on right hand side of column data
+ vertical_char - single character string used to draw vertical lines
+ horizontal_char - single character string used to draw horizontal lines
+ junction_char - single character string used to draw line junctions
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ valign - default valign for each row (None, "t", "m" or "b")
+ reversesort - True or False to sort in descending or ascending order
+ oldsortslice - Slice rows before sorting in the "old style" """
+
+ self.encoding = kwargs.get("encoding", "UTF-8")
+
+ # Data
+ self._field_names = []
+ self._rows = []
+ self.align = {}
+ self.valign = {}
+ self.max_width = {}
+ self.min_width = {}
+ self.int_format = {}
+ self.float_format = {}
+ if field_names:
+ self.field_names = field_names
+ else:
+ self._widths = []
+
+ # Options
+ self._options = (
+ "title start end fields header border sortby reversesort "
+ "sort_key attributes format hrules vrules".split()
+ )
+ self._options.extend(
+ "int_format float_format min_table_width max_table_width padding_width "
+ "left_padding_width right_padding_width".split()
+ )
+ self._options.extend(
+ "vertical_char horizontal_char junction_char header_style valign xhtml "
+ "print_empty oldsortslice".split()
+ )
+ self._options.extend("align valign max_width min_width".split())
+ for option in self._options:
+ if option in kwargs:
+ self._validate_option(option, kwargs[option])
+ else:
+ kwargs[option] = None
+
+ self._title = kwargs["title"] or None
+ self._start = kwargs["start"] or 0
+ self._end = kwargs["end"] or None
+ self._fields = kwargs["fields"] or None
+
+ if kwargs["header"] in (True, False):
+ self._header = kwargs["header"]
+ else:
+ self._header = True
+ self._header_style = kwargs["header_style"] or None
+ if kwargs["border"] in (True, False):
+ self._border = kwargs["border"]
+ else:
+ self._border = True
+ self._hrules = kwargs["hrules"] or FRAME
+ self._vrules = kwargs["vrules"] or ALL
+
+ self._sortby = kwargs["sortby"] or None
+ if kwargs["reversesort"] in (True, False):
+ self._reversesort = kwargs["reversesort"]
+ else:
+ self._reversesort = False
+ self._sort_key = kwargs["sort_key"] or (lambda x: x)
+
+ # Column specific arguments, use property.setters
+ self.align = kwargs["align"] or {}
+ self.valign = kwargs["valign"] or {}
+ self.max_width = kwargs["max_width"] or {}
+ self.min_width = kwargs["min_width"] or {}
+ self.int_format = kwargs["int_format"] or {}
+ self.float_format = kwargs["float_format"] or {}
+
+ self._min_table_width = kwargs["min_table_width"] or None
+ self._max_table_width = kwargs["max_table_width"] or None
+ self._padding_width = kwargs["padding_width"] or 1
+ self._left_padding_width = kwargs["left_padding_width"] or None
+ self._right_padding_width = kwargs["right_padding_width"] or None
+
+ self._vertical_char = kwargs["vertical_char"] or self._unicode("|")
+ self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-")
+ self._junction_char = kwargs["junction_char"] or self._unicode("+")
+
+ if kwargs["print_empty"] in (True, False):
+ self._print_empty = kwargs["print_empty"]
+ else:
+ self._print_empty = True
+ if kwargs["oldsortslice"] in (True, False):
+ self._oldsortslice = kwargs["oldsortslice"]
+ else:
+ self._oldsortslice = False
+ self._format = kwargs["format"] or False
+ self._xhtml = kwargs["xhtml"] or False
+ self._attributes = kwargs["attributes"] or {}
+
+ def _unicode(self, value):
+ if not isinstance(value, basestring):
+ value = str(value)
+ if not isinstance(value, unicode):
+ value = unicode(value, self.encoding, "strict")
+ return value
+
+ def _justify(self, text, width, align):
+ excess = width - _str_block_width(text)
+ if align == "l":
+ return text + excess * " "
+ elif align == "r":
+ return excess * " " + text
+ else:
+ if excess % 2:
+ # Uneven padding
+ # Put more space on right if text is of odd length...
+ if _str_block_width(text) % 2:
+ return (excess // 2) * " " + text + (excess // 2 + 1) * " "
+ # and more space on left if text is of even length
+ else:
+ return (excess // 2 + 1) * " " + text + (excess // 2) * " "
+ # Why distribute extra space this way? To match the behaviour of
+ # the inbuilt str.center() method.
+ else:
+ # Equal padding on either side
+ return (excess // 2) * " " + text + (excess // 2) * " "
+
+ def __getattr__(self, name):
+
+ if name == "rowcount":
+ return len(self._rows)
+ elif name == "colcount":
+ if self._field_names:
+ return len(self._field_names)
+ elif self._rows:
+ return len(self._rows[0])
+ else:
+ return 0
+ else:
+ raise AttributeError(name)
+
+ def __getitem__(self, index):
+
+ new = PrettyTable()
+ new.field_names = self.field_names
+ for attr in self._options:
+ setattr(new, "_" + attr, getattr(self, "_" + attr))
+ setattr(new, "_align", getattr(self, "_align"))
+ if isinstance(index, slice):
+ for row in self._rows[index]:
+ new.add_row(row)
+ elif isinstance(index, int):
+ new.add_row(self._rows[index])
+ else:
+ raise Exception(
+ "Index %s is invalid, must be an integer or slice" % str(index)
+ )
+ return new
+
+ if py3k:
+
+ def __str__(self):
+ return self.__unicode__()
+
+ else:
+
+ def __str__(self):
+ return self.__unicode__().encode(self.encoding)
+
+ def __unicode__(self):
+ return self.get_string()
+
+ ##############################
+ # ATTRIBUTE VALIDATORS #
+ ##############################
+
+ # The method _validate_option is all that should be used elsewhere in the code base
+ # to validate options. It will call the appropriate validation method for that
+ # option. The individual validation methods should never need to be called directly
+ # (although nothing bad will happen if they *are*).
+ # Validation happens in TWO places.
+ # Firstly, in the property setters defined in the ATTRIBUTE MANAGEMENT section.
+ # Secondly, in the _get_options method, where keyword arguments are mixed with
+ # persistent settings
+
+ def _validate_option(self, option, val):
+ if option == "field_names":
+ self._validate_field_names(val)
+ elif option in (
+ "start",
+ "end",
+ "max_width",
+ "min_width",
+ "min_table_width",
+ "max_table_width",
+ "padding_width",
+ "left_padding_width",
+ "right_padding_width",
+ "format",
+ ):
+ self._validate_nonnegative_int(option, val)
+ elif option == "sortby":
+ self._validate_field_name(option, val)
+ elif option == "sort_key":
+ self._validate_function(option, val)
+ elif option == "hrules":
+ self._validate_hrules(option, val)
+ elif option == "vrules":
+ self._validate_vrules(option, val)
+ elif option == "fields":
+ self._validate_all_field_names(option, val)
+ elif option in (
+ "header",
+ "border",
+ "reversesort",
+ "xhtml",
+ "print_empty",
+ "oldsortslice",
+ ):
+ self._validate_true_or_false(option, val)
+ elif option == "header_style":
+ self._validate_header_style(val)
+ elif option == "int_format":
+ self._validate_int_format(option, val)
+ elif option == "float_format":
+ self._validate_float_format(option, val)
+ elif option in ("vertical_char", "horizontal_char", "junction_char"):
+ self._validate_single_char(option, val)
+ elif option == "attributes":
+ self._validate_attributes(option, val)
+
+ def _validate_field_names(self, val):
+ # Check for appropriate length
+ if self._field_names:
+ try:
+ assert len(val) == len(self._field_names)
+ except AssertionError:
+ raise Exception(
+ "Field name list has incorrect number of values, "
+ "(actual) %d!=%d (expected)" % (len(val), len(self._field_names))
+ )
+ if self._rows:
+ try:
+ assert len(val) == len(self._rows[0])
+ except AssertionError:
+ raise Exception(
+ "Field name list has incorrect number of values, "
+ "(actual) %d!=%d (expected)" % (len(val), len(self._rows[0]))
+ )
+ # Check for uniqueness
+ try:
+ assert len(val) == len(set(val))
+ except AssertionError:
+ raise Exception("Field names must be unique!")
+
+ def _validate_header_style(self, val):
+ try:
+ assert val in ("cap", "title", "upper", "lower", None)
+ except AssertionError:
+ raise Exception(
+ "Invalid header style, use cap, title, upper, lower or None!"
+ )
+
+ def _validate_align(self, val):
+ try:
+ assert val in ["l", "c", "r"]
+ except AssertionError:
+ raise Exception("Alignment %s is invalid, use l, c or r!" % val)
+
+ def _validate_valign(self, val):
+ try:
+ assert val in ["t", "m", "b", None]
+ except AssertionError:
+ raise Exception("Alignment %s is invalid, use t, m, b or None!" % val)
+
+ def _validate_nonnegative_int(self, name, val):
+ try:
+ assert int(val) >= 0
+ except AssertionError:
+ raise Exception(
+ "Invalid value for {}: {}!".format(name, self._unicode(val))
+ )
+
+ def _validate_true_or_false(self, name, val):
+ try:
+ assert val in (True, False)
+ except AssertionError:
+ raise Exception("Invalid value for %s! Must be True or False." % name)
+
+ def _validate_int_format(self, name, val):
+ if val == "":
+ return
+ try:
+ assert type(val) in (str, unicode)
+ assert val.isdigit()
+ except AssertionError:
+ raise Exception(
+ "Invalid value for %s! Must be an integer format string." % name
+ )
+
+ def _validate_float_format(self, name, val):
+ if val == "":
+ return
+ try:
+ assert type(val) in (str, unicode)
+ assert "." in val
+ bits = val.split(".")
+ assert len(bits) <= 2
+ assert bits[0] == "" or bits[0].isdigit()
+ assert (
+ bits[1] == ""
+ or bits[1].isdigit()
+ or (bits[1][-1] == "f" and bits[1].rstrip("f").isdigit())
+ )
+ except AssertionError:
+ raise Exception(
+ "Invalid value for %s! Must be a float format string." % name
+ )
+
+ def _validate_function(self, name, val):
+ try:
+ assert hasattr(val, "__call__")
+ except AssertionError:
+ raise Exception("Invalid value for %s! Must be a function." % name)
+
+ def _validate_hrules(self, name, val):
+ try:
+ assert val in (ALL, FRAME, HEADER, NONE)
+ except AssertionError:
+ raise Exception(
+ "Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name
+ )
+
+ def _validate_vrules(self, name, val):
+ try:
+ assert val in (ALL, FRAME, NONE)
+ except AssertionError:
+ raise Exception(
+ "Invalid value for %s! Must be ALL, FRAME, or NONE." % name
+ )
+
+ def _validate_field_name(self, name, val):
+ try:
+ assert (val in self._field_names) or (val is None)
+ except AssertionError:
+ raise Exception("Invalid field name: %s!" % val)
+
+ def _validate_all_field_names(self, name, val):
+ try:
+ for x in val:
+ self._validate_field_name(name, x)
+ except AssertionError:
+ raise Exception("fields must be a sequence of field names!")
+
+ def _validate_single_char(self, name, val):
+ try:
+ assert _str_block_width(val) == 1
+ except AssertionError:
+ raise Exception(
+ "Invalid value for %s! Must be a string of length 1." % name
+ )
+
+ def _validate_attributes(self, name, val):
+ try:
+ assert isinstance(val, dict)
+ except AssertionError:
+ raise Exception("attributes must be a dictionary of name/value pairs!")
+
+ ##############################
+ # ATTRIBUTE MANAGEMENT #
+ ##############################
+
+ @property
+ def field_names(self):
+ """List or tuple of field names
+
+ When setting field_names, if there are already field names the new list
+ of field names must be the same length. Columns are renamed and row data
+ remains unchanged."""
+ return self._field_names
+
+ @field_names.setter
+ def field_names(self, val):
+ val = [self._unicode(x) for x in val]
+ self._validate_option("field_names", val)
+ old_names = None
+ if self._field_names:
+ old_names = self._field_names[:]
+ self._field_names = val
+ if self._align and old_names:
+ for old_name, new_name in zip(old_names, val):
+ self._align[new_name] = self._align[old_name]
+ for old_name in old_names:
+ if old_name not in self._align:
+ self._align.pop(old_name)
+ else:
+ self.align = "c"
+ if self._valign and old_names:
+ for old_name, new_name in zip(old_names, val):
+ self._valign[new_name] = self._valign[old_name]
+ for old_name in old_names:
+ if old_name not in self._valign:
+ self._valign.pop(old_name)
+ else:
+ self.valign = "t"
+
+ @property
+ def align(self):
+ """Controls alignment of fields
+ Arguments:
+
+ align - alignment, one of "l", "c", or "r" """
+ return self._align
+
+ @align.setter
+ def align(self, val):
+ if not self._field_names:
+ self._align = {}
+ elif val is None or (isinstance(val, dict) and len(val) == 0):
+ for field in self._field_names:
+ self._align[field] = "c"
+ else:
+ self._validate_align(val)
+ for field in self._field_names:
+ self._align[field] = val
+
+ @property
+ def valign(self):
+ """Controls vertical alignment of fields
+ Arguments:
+
+ valign - vertical alignment, one of "t", "m", or "b" """
+ return self._valign
+
+ @valign.setter
+ def valign(self, val):
+ if not self._field_names:
+ self._valign = {}
+ elif val is None or (isinstance(val, dict) and len(val) == 0):
+ for field in self._field_names:
+ self._valign[field] = "t"
+ else:
+ self._validate_valign(val)
+ for field in self._field_names:
+ self._valign[field] = val
+
+ @property
+ def max_width(self):
+ """Controls maximum width of fields
+ Arguments:
+
+ max_width - maximum width integer"""
+ return self._max_width
+
+ @max_width.setter
+ def max_width(self, val):
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._max_width = {}
+ else:
+ self._validate_option("max_width", val)
+ for field in self._field_names:
+ self._max_width[field] = val
+
+ @property
+ def min_width(self):
+ """Controls minimum width of fields
+ Arguments:
+
+ min_width - minimum width integer"""
+ return self._min_width
+
+ @min_width.setter
+ def min_width(self, val):
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._min_width = {}
+ else:
+ self._validate_option("min_width", val)
+ for field in self._field_names:
+ self._min_width[field] = val
+
+ @property
+ def min_table_width(self):
+ return self._min_table_width
+
+ @min_table_width.setter
+ def min_table_width(self, val):
+ self._validate_option("min_table_width", val)
+ self._min_table_width = val
+
+ @property
+ def max_table_width(self):
+ return self._max_table_width
+
+ @max_table_width.setter
+ def max_table_width(self, val):
+ self._validate_option("max_table_width", val)
+ self._max_table_width = val
+
+ @property
+ def fields(self):
+ """List or tuple of field names to include in displays"""
+ return self._fields
+
+ @fields.setter
+ def fields(self, val):
+ self._validate_option("fields", val)
+ self._fields = val
+
+ @property
+ def title(self):
+ """Optional table title
+
+ Arguments:
+
+ title - table title"""
+ return self._title
+
+ @title.setter
+ def title(self, val):
+ self._title = self._unicode(val)
+
+ @property
+ def start(self):
+ """Start index of the range of rows to print
+
+ Arguments:
+
+ start - index of first data row to include in output"""
+ return self._start
+
+ @start.setter
+ def start(self, val):
+ self._validate_option("start", val)
+ self._start = val
+
+ @property
+ def end(self):
+ """End index of the range of rows to print
+
+ Arguments:
+
+ end - index of last data row to include in output PLUS ONE (list slice style)"""
+ return self._end
+
+ @end.setter
+ def end(self, val):
+ self._validate_option("end", val)
+ self._end = val
+
+ @property
+ def sortby(self):
+ """Name of field by which to sort rows
+
+ Arguments:
+
+ sortby - field name to sort by"""
+ return self._sortby
+
+ @sortby.setter
+ def sortby(self, val):
+ self._validate_option("sortby", val)
+ self._sortby = val
+
+ @property
+ def reversesort(self):
+ """Controls direction of sorting (ascending vs descending)
+
+ Arguments:
+
+ reveresort - set to True to sort by descending order, or False to sort by
+ ascending order"""
+ return self._reversesort
+
+ @reversesort.setter
+ def reversesort(self, val):
+ self._validate_option("reversesort", val)
+ self._reversesort = val
+
+ @property
+ def sort_key(self):
+ """Sorting key function, applied to data points before sorting
+
+ Arguments:
+
+ sort_key - a function which takes one argument and returns something to be
+ sorted"""
+ return self._sort_key
+
+ @sort_key.setter
+ def sort_key(self, val):
+ self._validate_option("sort_key", val)
+ self._sort_key = val
+
+ @property
+ def header(self):
+ """Controls printing of table header with field names
+
+ Arguments:
+
+ header - print a header showing field names (True or False)"""
+ return self._header
+
+ @header.setter
+ def header(self, val):
+ self._validate_option("header", val)
+ self._header = val
+
+ @property
+ def header_style(self):
+ """Controls stylisation applied to field names in header
+
+ Arguments:
+
+ header_style - stylisation to apply to field names in header
+ ("cap", "title", "upper", "lower" or None)"""
+ return self._header_style
+
+ @header_style.setter
+ def header_style(self, val):
+ self._validate_header_style(val)
+ self._header_style = val
+
+ @property
+ def border(self):
+ """Controls printing of border around table
+
+ Arguments:
+
+ border - print a border around the table (True or False)"""
+ return self._border
+
+ @border.setter
+ def border(self, val):
+ self._validate_option("border", val)
+ self._border = val
+
+ @property
+ def hrules(self):
+ """Controls printing of horizontal rules after rows
+
+ Arguments:
+
+ hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE"""
+ return self._hrules
+
+ @hrules.setter
+ def hrules(self, val):
+ self._validate_option("hrules", val)
+ self._hrules = val
+
+ @property
+ def vrules(self):
+ """Controls printing of vertical rules between columns
+
+ Arguments:
+
+ vrules - vertical rules style. Allowed values: FRAME, ALL, NONE"""
+ return self._vrules
+
+ @vrules.setter
+ def vrules(self, val):
+ self._validate_option("vrules", val)
+ self._vrules = val
+
+ @property
+ def int_format(self):
+ """Controls formatting of integer data
+ Arguments:
+
+ int_format - integer format string"""
+ return self._int_format
+
+ @int_format.setter
+ def int_format(self, val):
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._int_format = {}
+ else:
+ self._validate_option("int_format", val)
+ for field in self._field_names:
+ self._int_format[field] = val
+
+ @property
+ def float_format(self):
+ """Controls formatting of floating point data
+ Arguments:
+
+ float_format - floating point format string"""
+ return self._float_format
+
+ @float_format.setter
+ def float_format(self, val):
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._float_format = {}
+ else:
+ self._validate_option("float_format", val)
+ for field in self._field_names:
+ self._float_format[field] = val
+
+ @property
+ def padding_width(self):
+ """The number of empty spaces between a column's edge and its content
+
+ Arguments:
+
+ padding_width - number of spaces, must be a positive integer"""
+ return self._padding_width
+
+ @padding_width.setter
+ def padding_width(self, val):
+ self._validate_option("padding_width", val)
+ self._padding_width = val
+
+ @property
+ def left_padding_width(self):
+ """The number of empty spaces between a column's left edge and its content
+
+ Arguments:
+
+ left_padding - number of spaces, must be a positive integer"""
+ return self._left_padding_width
+
+ @left_padding_width.setter
+ def left_padding_width(self, val):
+ self._validate_option("left_padding_width", val)
+ self._left_padding_width = val
+
+ @property
+ def right_padding_width(self):
+ """The number of empty spaces between a column's right edge and its content
+
+ Arguments:
+
+ right_padding - number of spaces, must be a positive integer"""
+ return self._right_padding_width
+
+ @right_padding_width.setter
+ def right_padding_width(self, val):
+ self._validate_option("right_padding_width", val)
+ self._right_padding_width = val
+
+ @property
+ def vertical_char(self):
+ """The character used when printing table borders to draw vertical lines
+
+ Arguments:
+
+ vertical_char - single character string used to draw vertical lines"""
+ return self._vertical_char
+
+ @vertical_char.setter
+ def vertical_char(self, val):
+ val = self._unicode(val)
+ self._validate_option("vertical_char", val)
+ self._vertical_char = val
+
+ @property
+ def horizontal_char(self):
+ """The character used when printing table borders to draw horizontal lines
+
+ Arguments:
+
+ horizontal_char - single character string used to draw horizontal lines"""
+ return self._horizontal_char
+
+ @horizontal_char.setter
+ def horizontal_char(self, val):
+ val = self._unicode(val)
+ self._validate_option("horizontal_char", val)
+ self._horizontal_char = val
+
+ @property
+ def junction_char(self):
+ """The character used when printing table borders to draw line junctions
+
+ Arguments:
+
+ junction_char - single character string used to draw line junctions"""
+ return self._junction_char
+
+ @junction_char.setter
+ def junction_char(self, val):
+ val = self._unicode(val)
+ self._validate_option("vertical_char", val)
+ self._junction_char = val
+
+ @property
+ def format(self):
+ """Controls whether or not HTML tables are formatted to match styling options
+
+ Arguments:
+
+ format - True or False"""
+ return self._format
+
+ @format.setter
+ def format(self, val):
+ self._validate_option("format", val)
+ self._format = val
+
+ @property
+ def print_empty(self):
+ """Controls whether or not empty tables produce a header and frame or just an
+ empty string
+
+ Arguments:
+
+ print_empty - True or False"""
+ return self._print_empty
+
+ @print_empty.setter
+ def print_empty(self, val):
+ self._validate_option("print_empty", val)
+ self._print_empty = val
+
+ @property
+ def attributes(self):
+ """A dictionary of HTML attribute name/value pairs to be included in the
+ <table> tag when printing HTML
+
+ Arguments:
+
+ attributes - dictionary of attributes"""
+ return self._attributes
+
+ @attributes.setter
+ def attributes(self, val):
+ self._validate_option("attributes", val)
+ self._attributes = val
+
+ @property
+ def oldsortslice(self):
+ """ oldsortslice - Slice rows before sorting in the "old style" """
+ return self._oldsortslice
+
+ @oldsortslice.setter
+ def oldsortslice(self, val):
+ self._validate_option("oldsortslice", val)
+ self._oldsortslice = val
+
+ ##############################
+ # OPTION MIXER #
+ ##############################
+
+ def _get_options(self, kwargs):
+
+ options = {}
+ for option in self._options:
+ if option in kwargs:
+ self._validate_option(option, kwargs[option])
+ options[option] = kwargs[option]
+ else:
+ options[option] = getattr(self, "_" + option)
+ return options
+
+ ##############################
+ # PRESET STYLE LOGIC #
+ ##############################
+
+ def set_style(self, style):
+
+ if style == DEFAULT:
+ self._set_default_style()
+ elif style == MSWORD_FRIENDLY:
+ self._set_msword_style()
+ elif style == PLAIN_COLUMNS:
+ self._set_columns_style()
+ elif style == MARKDOWN:
+ self._set_markdown_style()
+ elif style == ORGMODE:
+ self._set_orgmode_style()
+ elif style == RANDOM:
+ self._set_random_style()
+ else:
+ raise Exception("Invalid pre-set style!")
+
+ def _set_orgmode_style(self):
+ self._set_default_style()
+ self.orgmode = True
+
+ def _set_markdown_style(self):
+ self.header = True
+ self.border = True
+ self._hrules = None
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+ self.junction_char = "|"
+
+ def _set_default_style(self):
+
+ self.header = True
+ self.border = True
+ self._hrules = FRAME
+ self._vrules = ALL
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+ self.horizontal_char = "-"
+ self.junction_char = "+"
+
+ def _set_msword_style(self):
+
+ self.header = True
+ self.border = True
+ self._hrules = NONE
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+
+ def _set_columns_style(self):
+
+ self.header = True
+ self.border = False
+ self.padding_width = 1
+ self.left_padding_width = 0
+ self.right_padding_width = 8
+
+ def _set_random_style(self):
+
+ # Just for fun!
+ self.header = random.choice((True, False))
+ self.border = random.choice((True, False))
+ self._hrules = random.choice((ALL, FRAME, HEADER, NONE))
+ self._vrules = random.choice((ALL, FRAME, NONE))
+ self.left_padding_width = random.randint(0, 5)
+ self.right_padding_width = random.randint(0, 5)
+ self.vertical_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
+ self.horizontal_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
+ self.junction_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
+
+ ##############################
+ # DATA INPUT METHODS #
+ ##############################
+
+ def add_row(self, row):
+
+ """Add a row to the table
+
+ Arguments:
+
+ row - row of data, should be a list with as many elements as the table
+ has fields"""
+
+ if self._field_names and len(row) != len(self._field_names):
+ raise Exception(
+ "Row has incorrect number of values, (actual) %d!=%d (expected)"
+ % (len(row), len(self._field_names))
+ )
+ if not self._field_names:
+ self.field_names = [("Field %d" % (n + 1)) for n in range(0, len(row))]
+ self._rows.append(list(row))
+
+ def del_row(self, row_index):
+
+ """Delete a row from the table
+
+ Arguments:
+
+ row_index - The index of the row you want to delete. Indexing starts at 0."""
+
+ if row_index > len(self._rows) - 1:
+ raise Exception(
+ "Can't delete row at index %d, table only has %d rows!"
+ % (row_index, len(self._rows))
+ )
+ del self._rows[row_index]
+
+ def add_column(self, fieldname, column, align="c", valign="t"):
+
+ """Add a column to the table.
+
+ Arguments:
+
+ fieldname - name of the field to contain the new column of data
+ column - column of data, should be a list with as many elements as the
+ table has rows
+ align - desired alignment for this column - "l" for left, "c" for centre and
+ "r" for right
+ valign - desired vertical alignment for new columns - "t" for top,
+ "m" for middle and "b" for bottom"""
+
+ if len(self._rows) in (0, len(column)):
+ self._validate_align(align)
+ self._validate_valign(valign)
+ self._field_names.append(fieldname)
+ self._align[fieldname] = align
+ self._valign[fieldname] = valign
+ for i in range(0, len(column)):
+ if len(self._rows) < i + 1:
+ self._rows.append([])
+ self._rows[i].append(column[i])
+ else:
+ raise Exception(
+ "Column length %d does not match number of rows %d!"
+ % (len(column), len(self._rows))
+ )
+
+ def del_column(self, fieldname):
+
+ """Delete a column from the table
+
+ Arguments:
+
+ fieldname - The field name of the column you want to delete."""
+
+ if fieldname not in self._field_names:
+ raise Exception(
+ "Can't delete column %r which is not a field name of this table."
+ " Field names are: %s"
+ % (fieldname, ", ".join(map(repr, self._field_names)))
+ )
+
+ col_index = self._field_names.index(fieldname)
+ del self._field_names[col_index]
+ for row in self._rows:
+ del row[col_index]
+
+ def clear_rows(self):
+
+ """Delete all rows from the table but keep the current field names"""
+
+ self._rows = []
+
+ def clear(self):
+
+ """Delete all rows and field names from the table, maintaining nothing but
+ styling options"""
+
+ self._rows = []
+ self._field_names = []
+ self._widths = []
+
+ ##############################
+ # MISC PUBLIC METHODS #
+ ##############################
+
+ def copy(self):
+ return copy.deepcopy(self)
+
+ ##############################
+ # MISC PRIVATE METHODS #
+ ##############################
+
+ def _format_value(self, field, value):
+ if isinstance(value, int) and field in self._int_format:
+ value = self._unicode(("%%%sd" % self._int_format[field]) % value)
+ elif isinstance(value, float) and field in self._float_format:
+ value = self._unicode(("%%%sf" % self._float_format[field]) % value)
+ return self._unicode(value)
+
+ def _compute_table_width(self, options):
+ table_width = 2 if options["vrules"] in (FRAME, ALL) else 0
+ per_col_padding = sum(self._get_padding_widths(options))
+ for index, fieldname in enumerate(self.field_names):
+ if not options["fields"] or (
+ options["fields"] and fieldname in options["fields"]
+ ):
+ table_width += self._widths[index] + per_col_padding
+ return table_width
+
+ def _compute_widths(self, rows, options):
+ if options["header"]:
+ widths = [_get_size(field)[0] for field in self._field_names]
+ else:
+ widths = len(self.field_names) * [0]
+
+ for row in rows:
+ for index, value in enumerate(row):
+ fieldname = self.field_names[index]
+ if fieldname in self.max_width:
+ widths[index] = max(
+ widths[index],
+ min(_get_size(value)[0], self.max_width[fieldname]),
+ )
+ else:
+ widths[index] = max(widths[index], _get_size(value)[0])
+ if fieldname in self.min_width:
+ widths[index] = max(widths[index], self.min_width[fieldname])
+ self._widths = widths
+
+ # Are we exceeding max_table_width?
+ if self._max_table_width:
+ table_width = self._compute_table_width(options)
+ if table_width > self._max_table_width:
+ # Shrink widths in proportion
+ scale = 1.0 * self._max_table_width / table_width
+ widths = [int(math.floor(w * scale)) for w in widths]
+ self._widths = widths
+
+ # Are we under min_table_width or title width?
+ if self._min_table_width or options["title"]:
+ if options["title"]:
+ title_width = len(options["title"]) + sum(
+ self._get_padding_widths(options)
+ )
+ if options["vrules"] in (FRAME, ALL):
+ title_width += 2
+ else:
+ title_width = 0
+ min_table_width = self.min_table_width or 0
+ min_width = max(title_width, min_table_width)
+ table_width = self._compute_table_width(options)
+ if table_width < min_width:
+ # Grow widths in proportion
+ scale = 1.0 * min_width / table_width
+ widths = [int(math.ceil(w * scale)) for w in widths]
+ self._widths = widths
+
+ def _get_padding_widths(self, options):
+
+ if options["left_padding_width"] is not None:
+ lpad = options["left_padding_width"]
+ else:
+ lpad = options["padding_width"]
+ if options["right_padding_width"] is not None:
+ rpad = options["right_padding_width"]
+ else:
+ rpad = options["padding_width"]
+ return lpad, rpad
+
+ def _get_rows(self, options):
+ """Return only those data rows that should be printed, based on slicing and
+ sorting.
+
+ Arguments:
+
+ options - dictionary of option settings."""
+
+ if options["oldsortslice"]:
+ rows = copy.deepcopy(self._rows[options["start"] : options["end"]])
+ else:
+ rows = copy.deepcopy(self._rows)
+
+ # Sort
+ if options["sortby"]:
+ sortindex = self._field_names.index(options["sortby"])
+ # Decorate
+ rows = [[row[sortindex]] + row for row in rows]
+ # Sort
+ rows.sort(reverse=options["reversesort"], key=options["sort_key"])
+ # Undecorate
+ rows = [row[1:] for row in rows]
+
+ # Slice if necessary
+ if not options["oldsortslice"]:
+ rows = rows[options["start"] : options["end"]]
+
+ return rows
+
+ def _format_row(self, row, options):
+ return [
+ self._format_value(field, value)
+ for (field, value) in zip(self._field_names, row)
+ ]
+
+ def _format_rows(self, rows, options):
+ return [self._format_row(row, options) for row in rows]
+
+ ##############################
+ # PLAIN TEXT STRING METHODS #
+ ##############################
+
+ def get_string(self, **kwargs):
+
+ """Return string representation of table in current state.
+
+ Arguments:
+
+ title - optional table title
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ fields - names of fields (columns) to include
+ header - print a header showing field names (True or False)
+ border - print a border around the table (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: ALL, FRAME, HEADER, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ padding_width - number of spaces on either side of column data (only used if
+ left and right paddings are None)
+ left_padding_width - number of spaces on left hand side of column data
+ right_padding_width - number of spaces on right hand side of column data
+ vertical_char - single character string used to draw vertical lines
+ horizontal_char - single character string used to draw horizontal lines
+ junction_char - single character string used to draw line junctions
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ reversesort - True or False to sort in descending or ascending order
+ print empty - if True, stringify just the header for an empty table,
+ if False return an empty string"""
+
+ options = self._get_options(kwargs)
+
+ lines = []
+
+ # Don't think too hard about an empty table
+ # Is this the desired behaviour? Maybe we should still print the header?
+ if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
+ return ""
+
+ # Get the rows we need to print, taking into account slicing, sorting, etc.
+ rows = self._get_rows(options)
+
+ # Turn all data in all rows into Unicode, formatted as desired
+ formatted_rows = self._format_rows(rows, options)
+
+ # Compute column widths
+ self._compute_widths(formatted_rows, options)
+ self._hrule = self._stringify_hrule(options)
+
+ # Add title
+ title = options["title"] or self._title
+ if title:
+ lines.append(self._stringify_title(title, options))
+
+ # Add header or top of border
+ if options["header"]:
+ lines.append(self._stringify_header(options))
+ elif options["border"] and options["hrules"] in (ALL, FRAME):
+ lines.append(self._hrule)
+
+ # Add rows
+ for row in formatted_rows:
+ lines.append(self._stringify_row(row, options))
+
+ # Add bottom of border
+ if options["border"] and options["hrules"] == FRAME:
+ lines.append(self._hrule)
+
+ if "orgmode" in self.__dict__ and self.orgmode is True:
+ tmp = list()
+ for line in lines:
+ tmp.extend(line.split("\n"))
+ lines = ["|" + line[1:-1] + "|" for line in tmp]
+
+ return self._unicode("\n").join(lines)
+
+ def _stringify_hrule(self, options):
+
+ if not options["border"]:
+ return ""
+ lpad, rpad = self._get_padding_widths(options)
+ if options["vrules"] in (ALL, FRAME):
+ bits = [options["junction_char"]]
+ else:
+ bits = [options["horizontal_char"]]
+ # For tables with no data or fieldnames
+ if not self._field_names:
+ bits.append(options["junction_char"])
+ return "".join(bits)
+ for field, width in zip(self._field_names, self._widths):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ bits.append((width + lpad + rpad) * options["horizontal_char"])
+ if options["vrules"] == ALL:
+ bits.append(options["junction_char"])
+ else:
+ bits.append(options["horizontal_char"])
+ if options["vrules"] == FRAME:
+ bits.pop()
+ bits.append(options["junction_char"])
+ return "".join(bits)
+
+ def _stringify_title(self, title, options):
+
+ lines = []
+ lpad, rpad = self._get_padding_widths(options)
+ if options["border"]:
+ if options["vrules"] == ALL:
+ options["vrules"] = FRAME
+ lines.append(self._stringify_hrule(options))
+ options["vrules"] = ALL
+ elif options["vrules"] == FRAME:
+ lines.append(self._stringify_hrule(options))
+ bits = []
+ endpoint = (
+ options["vertical_char"] if options["vrules"] in (ALL, FRAME) else " "
+ )
+ bits.append(endpoint)
+ title = " " * lpad + title + " " * rpad
+ bits.append(self._justify(title, len(self._hrule) - 2, "c"))
+ bits.append(endpoint)
+ lines.append("".join(bits))
+ return "\n".join(lines)
+
+ def _stringify_header(self, options):
+
+ bits = []
+ lpad, rpad = self._get_padding_widths(options)
+ if options["border"]:
+ if options["hrules"] in (ALL, FRAME):
+ bits.append(self._hrule)
+ bits.append("\n")
+ if options["vrules"] in (ALL, FRAME):
+ bits.append(options["vertical_char"])
+ else:
+ bits.append(" ")
+ # For tables with no data or field names
+ if not self._field_names:
+ if options["vrules"] in (ALL, FRAME):
+ bits.append(options["vertical_char"])
+ else:
+ bits.append(" ")
+ for (field, width) in zip(self._field_names, self._widths):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ if self._header_style == "cap":
+ fieldname = field.capitalize()
+ elif self._header_style == "title":
+ fieldname = field.title()
+ elif self._header_style == "upper":
+ fieldname = field.upper()
+ elif self._header_style == "lower":
+ fieldname = field.lower()
+ else:
+ fieldname = field
+ bits.append(
+ " " * lpad
+ + self._justify(fieldname, width, self._align[field])
+ + " " * rpad
+ )
+ if options["border"]:
+ if options["vrules"] == ALL:
+ bits.append(options["vertical_char"])
+ else:
+ bits.append(" ")
+ # If vrules is FRAME, then we just appended a space at the end
+ # of the last field, when we really want a vertical character
+ if options["border"] and options["vrules"] == FRAME:
+ bits.pop()
+ bits.append(options["vertical_char"])
+ if options["border"] and options["hrules"] != NONE:
+ bits.append("\n")
+ bits.append(self._hrule)
+ return "".join(bits)
+
+ def _stringify_row(self, row, options):
+
+ for (index, field, value, width) in zip(
+ range(0, len(row)), self._field_names, row, self._widths
+ ):
+ # Enforce max widths
+ lines = value.split("\n")
+ new_lines = []
+ for line in lines:
+ if _str_block_width(line) > width:
+ line = textwrap.fill(line, width)
+ new_lines.append(line)
+ lines = new_lines
+ value = "\n".join(lines)
+ row[index] = value
+
+ row_height = 0
+ for c in row:
+ h = _get_size(c)[1]
+ if h > row_height:
+ row_height = h
+
+ bits = []
+ lpad, rpad = self._get_padding_widths(options)
+ for y in range(0, row_height):
+ bits.append([])
+ if options["border"]:
+ if options["vrules"] in (ALL, FRAME):
+ bits[y].append(self.vertical_char)
+ else:
+ bits[y].append(" ")
+
+ for (field, value, width) in zip(self._field_names, row, self._widths):
+
+ valign = self._valign[field]
+ lines = value.split("\n")
+ d_height = row_height - len(lines)
+ if d_height:
+ if valign == "m":
+ lines = (
+ [""] * int(d_height / 2)
+ + lines
+ + [""] * (d_height - int(d_height / 2))
+ )
+ elif valign == "b":
+ lines = [""] * d_height + lines
+ else:
+ lines = lines + [""] * d_height
+
+ y = 0
+ for line in lines:
+ if options["fields"] and field not in options["fields"]:
+ continue
+
+ bits[y].append(
+ " " * lpad
+ + self._justify(line, width, self._align[field])
+ + " " * rpad
+ )
+ if options["border"]:
+ if options["vrules"] == ALL:
+ bits[y].append(self.vertical_char)
+ else:
+ bits[y].append(" ")
+ y += 1
+
+ # If vrules is FRAME, then we just appended a space at the end
+ # of the last field, when we really want a vertical character
+ for y in range(0, row_height):
+ if options["border"] and options["vrules"] == FRAME:
+ bits[y].pop()
+ bits[y].append(options["vertical_char"])
+
+ if options["border"] and options["hrules"] == ALL:
+ bits[row_height - 1].append("\n")
+ bits[row_height - 1].append(self._hrule)
+
+ for y in range(0, row_height):
+ bits[y] = "".join(bits[y])
+
+ return "\n".join(bits)
+
+ def paginate(self, page_length=58, **kwargs):
+
+ pages = []
+ kwargs["start"] = kwargs.get("start", 0)
+ true_end = kwargs.get("end", self.rowcount)
+ while True:
+ kwargs["end"] = min(kwargs["start"] + page_length, true_end)
+ pages.append(self.get_string(**kwargs))
+ if kwargs["end"] == true_end:
+ break
+ kwargs["start"] += page_length
+ return "\f".join(pages)
+
+ ##############################
+ # CSV STRING METHODS #
+ ##############################
+ def get_csv_string(self, **kwargs):
+
+ """Return string representation of CSV formatted table in the current state
+
+ Keyword arguments are first interpreted as table formatting options, and
+ then any unused keyword arguments are passed to csv.writer(). For
+ example, get_csv_string(header=False, delimiter='\t') would use
+ header as a PrettyTable formatting option (skip the header row) and
+ delimiter as a csv.writer keyword argument.
+ """
+
+ options = self._get_options(kwargs)
+ csv_options = {
+ key: value for key, value in kwargs.items() if key not in options
+ }
+ csv_buffer = StringIO()
+ csv_writer = csv.writer(csv_buffer, **csv_options)
+
+ if options.get("header"):
+ csv_writer.writerow(self._field_names)
+ for row in self._get_rows(options):
+ csv_writer.writerow(row)
+
+ return csv_buffer.getvalue()
+
+ ##############################
+ # JSON STRING METHODS #
+ ##############################
+ def get_json_string(self, **kwargs):
+
+ """Return string representation of JSON formatted table in the current state
+
+ Arguments:
+
+ none yet"""
+
+ options = self._get_options(kwargs)
+
+ objects = [self.field_names]
+ for row in self._get_rows(options):
+ objects.append(dict(zip(self._field_names, row)))
+
+ return json.dumps(objects, indent=4, separators=(",", ": "), sort_keys=True)
+
+ ##############################
+ # HTML STRING METHODS #
+ ##############################
+
+ def get_html_string(self, **kwargs):
+ """Return string representation of HTML formatted version of table in current
+ state.
+
+ Arguments:
+
+ title - optional table title
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ fields - names of fields (columns) to include
+ header - print a header showing field names (True or False)
+ border - print a border around the table (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: ALL, FRAME, HEADER, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ padding_width - number of spaces on either side of column data (only used if
+ left and right paddings are None)
+ left_padding_width - number of spaces on left hand side of column data
+ right_padding_width - number of spaces on right hand side of column data
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ attributes - dictionary of name/value pairs to include as HTML attributes in the
+ <table> tag
+ xhtml - print <br/> tags if True, <br> tags if false"""
+
+ options = self._get_options(kwargs)
+
+ if options["format"]:
+ string = self._get_formatted_html_string(options)
+ else:
+ string = self._get_simple_html_string(options)
+
+ return string
+
+ def _get_simple_html_string(self, options):
+
+ lines = []
+ if options["xhtml"]:
+ linebreak = "<br/>"
+ else:
+ linebreak = "<br>"
+
+ open_tag = ["<table"]
+ if options["attributes"]:
+ for attr_name in options["attributes"]:
+ open_tag.append(
+ ' {}="{}"'.format(attr_name, options["attributes"][attr_name])
+ )
+ open_tag.append(">")
+ lines.append("".join(open_tag))
+
+ # Title
+ title = options["title"] or self._title
+ if title:
+ cols = (
+ len(options["fields"]) if options["fields"] else len(self.field_names)
+ )
+ lines.append(" <tr>")
+ lines.append(" <td colspan=%d>%s</td>" % (cols, title))
+ lines.append(" </tr>")
+
+ # Headers
+ if options["header"]:
+ lines.append(" <tr>")
+ for field in self._field_names:
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ " <th>%s</th>" % escape(field).replace("\n", linebreak)
+ )
+ lines.append(" </tr>")
+
+ # Data
+ rows = self._get_rows(options)
+ formatted_rows = self._format_rows(rows, options)
+ for row in formatted_rows:
+ lines.append(" <tr>")
+ for field, datum in zip(self._field_names, row):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ " <td>%s</td>" % escape(datum).replace("\n", linebreak)
+ )
+ lines.append(" </tr>")
+
+ lines.append("</table>")
+
+ return self._unicode("\n").join(lines)
+
+ def _get_formatted_html_string(self, options):
+
+ lines = []
+ lpad, rpad = self._get_padding_widths(options)
+ if options["xhtml"]:
+ linebreak = "<br/>"
+ else:
+ linebreak = "<br>"
+
+ open_tag = ["<table"]
+ if options["border"]:
+ if options["hrules"] == ALL and options["vrules"] == ALL:
+ open_tag.append(' frame="box" rules="all"')
+ elif options["hrules"] == FRAME and options["vrules"] == FRAME:
+ open_tag.append(' frame="box"')
+ elif options["hrules"] == FRAME and options["vrules"] == ALL:
+ open_tag.append(' frame="box" rules="cols"')
+ elif options["hrules"] == FRAME:
+ open_tag.append(' frame="hsides"')
+ elif options["hrules"] == ALL:
+ open_tag.append(' frame="hsides" rules="rows"')
+ elif options["vrules"] == FRAME:
+ open_tag.append(' frame="vsides"')
+ elif options["vrules"] == ALL:
+ open_tag.append(' frame="vsides" rules="cols"')
+ if options["attributes"]:
+ for attr_name in options["attributes"]:
+ open_tag.append(
+ ' {}="{}"'.format(attr_name, options["attributes"][attr_name])
+ )
+ open_tag.append(">")
+ lines.append("".join(open_tag))
+
+ # Title
+ title = options["title"] or self._title
+ if title:
+ cols = (
+ len(options["fields"]) if options["fields"] else len(self.field_names)
+ )
+ lines.append(" <tr>")
+ lines.append(" <td colspan=%d>%s</td>" % (cols, title))
+ lines.append(" </tr>")
+
+ # Headers
+ if options["header"]:
+ lines.append(" <tr>")
+ for field in self._field_names:
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ ' <th style="padding-left: %dem; padding-right: %dem; text-align: center">%s</th>' # noqa: E501
+ % (lpad, rpad, escape(field).replace("\n", linebreak))
+ )
+ lines.append(" </tr>")
+
+ # Data
+ rows = self._get_rows(options)
+ formatted_rows = self._format_rows(rows, options)
+ aligns = []
+ valigns = []
+ for field in self._field_names:
+ aligns.append(
+ {"l": "left", "r": "right", "c": "center"}[self._align[field]]
+ )
+ valigns.append(
+ {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
+ )
+ for row in formatted_rows:
+ lines.append(" <tr>")
+ for field, datum, align, valign in zip(
+ self._field_names, row, aligns, valigns
+ ):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ ' <td style="padding-left: %dem; padding-right: %dem; text-align: %s; vertical-align: %s">%s</td>' # noqa: E501
+ % (
+ lpad,
+ rpad,
+ align,
+ valign,
+ escape(datum).replace("\n", linebreak),
+ )
+ )
+ lines.append(" </tr>")
+ lines.append("</table>")
+
+ return self._unicode("\n").join(lines)
+
+
+##############################
+# UNICODE WIDTH FUNCTION #
+##############################
+
+
+def _str_block_width(val):
+ return wcwidth.wcswidth(_re.sub("", val))
+
+
+##############################
+# TABLE FACTORIES #
+##############################
+
+
+def from_csv(fp, field_names=None, **kwargs):
+ fmtparams = {}
+ for param in [
+ "delimiter",
+ "doublequote",
+ "escapechar",
+ "lineterminator",
+ "quotechar",
+ "quoting",
+ "skipinitialspace",
+ "strict",
+ ]:
+ if param in kwargs:
+ fmtparams[param] = kwargs.pop(param)
+ if fmtparams:
+ reader = csv.reader(fp, **fmtparams)
+ else:
+ dialect = csv.Sniffer().sniff(fp.read(1024))
+ fp.seek(0)
+ reader = csv.reader(fp, dialect)
+
+ table = PrettyTable(**kwargs)
+ if field_names:
+ table.field_names = field_names
+ else:
+ if py3k:
+ table.field_names = [x.strip() for x in next(reader)]
+ else:
+ table.field_names = [x.strip() for x in reader.next()]
+
+ for row in reader:
+ table.add_row([x.strip() for x in row])
+
+ return table
+
+
+def from_db_cursor(cursor, **kwargs):
+ if cursor.description:
+ table = PrettyTable(**kwargs)
+ table.field_names = [col[0] for col in cursor.description]
+ for row in cursor.fetchall():
+ table.add_row(row)
+ return table
+
+
+def from_json(json_string, **kwargs):
+ table = PrettyTable(**kwargs)
+ objects = json.loads(json_string)
+ table.field_names = objects[0]
+ for obj in objects[1:]:
+ row = [obj[key] for key in table.field_names]
+ table.add_row(row)
+ return table
+
+
+class TableHandler(HTMLParser):
+ def __init__(self, **kwargs):
+ HTMLParser.__init__(self)
+ self.kwargs = kwargs
+ self.tables = []
+ self.last_row = []
+ self.rows = []
+ self.max_row_width = 0
+ self.active = None
+ self.last_content = ""
+ self.is_last_row_header = False
+ self.colspan = 0
+
+ def handle_starttag(self, tag, attrs):
+ self.active = tag
+ if tag == "th":
+ self.is_last_row_header = True
+ for (key, value) in attrs:
+ if key == "colspan":
+ self.colspan = int(value)
+
+ def handle_endtag(self, tag):
+ if tag in ["th", "td"]:
+ stripped_content = self.last_content.strip()
+ self.last_row.append(stripped_content)
+ if self.colspan:
+ for i in range(1, self.colspan):
+ self.last_row.append("")
+ self.colspan = 0
+
+ if tag == "tr":
+ self.rows.append((self.last_row, self.is_last_row_header))
+ self.max_row_width = max(self.max_row_width, len(self.last_row))
+ self.last_row = []
+ self.is_last_row_header = False
+ if tag == "table":
+ table = self.generate_table(self.rows)
+ self.tables.append(table)
+ self.rows = []
+ self.last_content = " "
+ self.active = None
+
+ def handle_data(self, data):
+ self.last_content += data
+
+ def generate_table(self, rows):
+ """
+ Generates from a list of rows a PrettyTable object.
+ """
+ table = PrettyTable(**self.kwargs)
+ for row in self.rows:
+ if len(row[0]) < self.max_row_width:
+ appends = self.max_row_width - len(row[0])
+ for i in range(1, appends):
+ row[0].append("-")
+
+ if row[1]:
+ self.make_fields_unique(row[0])
+ table.field_names = row[0]
+ else:
+ table.add_row(row[0])
+ return table
+
+ def make_fields_unique(self, fields):
+ """
+ iterates over the row and make each field unique
+ """
+ for i in range(0, len(fields)):
+ for j in range(i + 1, len(fields)):
+ if fields[i] == fields[j]:
+ fields[j] += "'"
+
+
+def from_html(html_code, **kwargs):
+ """
+ Generates a list of PrettyTables from a string of HTML code. Each <table> in
+ the HTML becomes one PrettyTable object.
+ """
+
+ parser = TableHandler(**kwargs)
+ parser.feed(html_code)
+ return parser.tables
+
+
+def from_html_one(html_code, **kwargs):
+ """
+ Generates a PrettyTables from a string of HTML code which contains only a
+ single <table>
+ """
+
+ tables = from_html(html_code, **kwargs)
+ try:
+ assert len(tables) == 1
+ except AssertionError:
+ raise Exception(
+ "More than one <table> in provided HTML code! Use from_html instead."
+ )
+ return tables[0]
+
+
+##############################
+# MAIN (TEST FUNCTION) #
+##############################
+
+
+def main():
+ print("Generated using setters:")
+ x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"])
+ x.title = "Australian capital cities"
+ x.sortby = "Population"
+ x.reversesort = True
+ x.int_format["Area"] = "04"
+ x.float_format = "6.1"
+ x.align["City name"] = "l" # Left align city names
+ x.add_row(["Adelaide", 1295, 1158259, 600.5])
+ x.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ x.add_row(["Darwin", 112, 120900, 1714.7])
+ x.add_row(["Hobart", 1357, 205556, 619.5])
+ x.add_row(["Sydney", 2058, 4336374, 1214.8])
+ x.add_row(["Melbourne", 1566, 3806092, 646.9])
+ x.add_row(["Perth", 5386, 1554769, 869.4])
+ print(x)
+
+ print()
+
+ print("Generated using constructor arguments:")
+
+ y = PrettyTable(
+ ["City name", "Area", "Population", "Annual Rainfall"],
+ title="Australian capital cities",
+ sortby="Population",
+ reversesort=True,
+ int_format="04",
+ float_format="6.1",
+ max_width=12,
+ min_width=4,
+ align="c",
+ valign="t",
+ )
+ y.align["City name"] = "l" # Left align city names
+ y.add_row(["Adelaide", 1295, 1158259, 600.5])
+ y.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ y.add_row(["Darwin", 112, 120900, 1714.7])
+ y.add_row(["Hobart", 1357, 205556, 619.5])
+ y.add_row(["Sydney", 2058, 4336374, 1214.8])
+ y.add_row(["Melbourne", 1566, 3806092, 646.9])
+ y.add_row(["Perth", 5386, 1554769, 869.4])
+ print(y)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/contrib/python/prettytable/py2/tests/prettytable_test.py b/contrib/python/prettytable/py2/tests/prettytable_test.py
new file mode 100644
index 0000000000..1ea534af00
--- /dev/null
+++ b/contrib/python/prettytable/py2/tests/prettytable_test.py
@@ -0,0 +1,859 @@
+#!/usr/bin/env python
+# coding=UTF-8
+
+import sys
+import unittest
+from math import e, pi, sqrt
+
+from prettytable import (
+ ALL,
+ MARKDOWN,
+ MSWORD_FRIENDLY,
+ NONE,
+ ORGMODE,
+ PrettyTable,
+ from_csv,
+ from_db_cursor,
+ from_html,
+ from_html_one,
+ from_json,
+)
+
+py3k = sys.version_info[0] >= 3
+try:
+ import sqlite3
+
+ _have_sqlite = True
+except ImportError:
+ _have_sqlite = False
+if py3k:
+ import io as StringIO
+else:
+ import StringIO
+
+
+class BuildEquivalenceTest(unittest.TestCase):
+ """Make sure that building a table row-by-row and column-by-column yield the same
+ results"""
+
+ def setUp(self):
+
+ # Row by row...
+ self.row = PrettyTable()
+ self.row.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ self.row.add_row(["Adelaide", 1295, 1158259, 600.5])
+ self.row.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ self.row.add_row(["Darwin", 112, 120900, 1714.7])
+ self.row.add_row(["Hobart", 1357, 205556, 619.5])
+ self.row.add_row(["Sydney", 2058, 4336374, 1214.8])
+ self.row.add_row(["Melbourne", 1566, 3806092, 646.9])
+ self.row.add_row(["Perth", 5386, 1554769, 869.4])
+
+ # Column by column...
+ self.col = PrettyTable()
+ self.col.add_column(
+ "City name",
+ [
+ "Adelaide",
+ "Brisbane",
+ "Darwin",
+ "Hobart",
+ "Sydney",
+ "Melbourne",
+ "Perth",
+ ],
+ )
+ self.col.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
+ self.col.add_column(
+ "Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769]
+ )
+ self.col.add_column(
+ "Annual Rainfall", [600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4]
+ )
+
+ # A mix of both!
+ self.mix = PrettyTable()
+ self.mix.field_names = ["City name", "Area"]
+ self.mix.add_row(["Adelaide", 1295])
+ self.mix.add_row(["Brisbane", 5905])
+ self.mix.add_row(["Darwin", 112])
+ self.mix.add_row(["Hobart", 1357])
+ self.mix.add_row(["Sydney", 2058])
+ self.mix.add_row(["Melbourne", 1566])
+ self.mix.add_row(["Perth", 5386])
+ self.mix.add_column(
+ "Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769]
+ )
+ self.mix.add_column(
+ "Annual Rainfall", [600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4]
+ )
+
+ def testRowColEquivalenceASCII(self):
+
+ self.assertEqual(self.row.get_string(), self.col.get_string())
+
+ def testRowMixEquivalenceASCII(self):
+
+ self.assertEqual(self.row.get_string(), self.mix.get_string())
+
+ def testRowColEquivalenceHTML(self):
+
+ self.assertEqual(self.row.get_html_string(), self.col.get_html_string())
+
+ def testRowMixEquivalenceHTML(self):
+
+ self.assertEqual(self.row.get_html_string(), self.mix.get_html_string())
+
+
+class DeleteColumnTest(unittest.TestCase):
+ def testDeleteColumn(self):
+ with_del = PrettyTable()
+ with_del.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
+ with_del.add_column("Area", [1295, 5905, 112])
+ with_del.add_column("Population", [1158259, 1857594, 120900])
+ with_del.del_column("Area")
+
+ without_row = PrettyTable()
+ without_row.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
+ without_row.add_column("Population", [1158259, 1857594, 120900])
+
+ self.assertEqual(with_del.get_string(), without_row.get_string())
+
+ def testDeleteIllegalColumnRaisesException(self):
+ table = PrettyTable()
+ table.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
+
+ with self.assertRaises(Exception):
+ table.del_column("City not-a-name")
+
+
+# class FieldnamelessTableTest(unittest.TestCase):
+#
+# """Make sure that building and stringing a table with no fieldnames works fine"""
+#
+# def setUp(self):
+# self.x = PrettyTable()
+# self.x.add_row(["Adelaide",1295, 1158259, 600.5])
+# self.x.add_row(["Brisbane",5905, 1857594, 1146.4])
+# self.x.add_row(["Darwin", 112, 120900, 1714.7])
+# self.x.add_row(["Hobart", 1357, 205556, 619.5])
+# self.x.add_row(["Sydney", 2058, 4336374, 1214.8])
+# self.x.add_row(["Melbourne", 1566, 3806092, 646.9])
+# self.x.add_row(["Perth", 5386, 1554769, 869.4])
+#
+# def testCanStringASCII(self):
+# self.x.get_string()
+#
+# def testCanStringHTML(self):
+# self.x.get_html_string()
+#
+# def testAddFieldnamesLater(self):
+# self.x.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+# self.x.get_string()
+
+
+class CityDataTest(unittest.TestCase):
+
+ """Just build the Australian capital city data example table."""
+
+ def setUp(self):
+
+ self.x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"])
+ self.x.add_row(["Adelaide", 1295, 1158259, 600.5])
+ self.x.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ self.x.add_row(["Darwin", 112, 120900, 1714.7])
+ self.x.add_row(["Hobart", 1357, 205556, 619.5])
+ self.x.add_row(["Sydney", 2058, 4336374, 1214.8])
+ self.x.add_row(["Melbourne", 1566, 3806092, 646.9])
+ self.x.add_row(["Perth", 5386, 1554769, 869.4])
+
+
+class OptionOverrideTests(CityDataTest):
+
+ """Make sure all options are properly overwritten by get_string."""
+
+ def testBorder(self):
+ default = self.x.get_string()
+ override = self.x.get_string(border=False)
+ self.assertNotEqual(default, override)
+
+ def testHeader(self):
+ default = self.x.get_string()
+ override = self.x.get_string(header=False)
+ self.assertNotEqual(default, override)
+
+ def testHrulesAll(self):
+ default = self.x.get_string()
+ override = self.x.get_string(hrules=ALL)
+ self.assertNotEqual(default, override)
+
+ def testHrulesNone(self):
+
+ default = self.x.get_string()
+ override = self.x.get_string(hrules=NONE)
+ self.assertNotEqual(default, override)
+
+
+class OptionAttributeTests(CityDataTest):
+
+ """Make sure all options which have an attribute interface work as they should.
+ Also make sure option settings are copied correctly when a table is cloned by
+ slicing."""
+
+ def testSetForAllColumns(self):
+ self.x.field_names = sorted(self.x.field_names)
+ self.x.align = "l"
+ self.x.max_width = 10
+ self.x.start = 2
+ self.x.end = 4
+ self.x.sortby = "Area"
+ self.x.reversesort = True
+ self.x.header = True
+ self.x.border = False
+ self.x.hrule = True
+ self.x.int_format = "4"
+ self.x.float_format = "2.2"
+ self.x.padding_width = 2
+ self.x.left_padding_width = 2
+ self.x.right_padding_width = 2
+ self.x.vertical_char = "!"
+ self.x.horizontal_char = "~"
+ self.x.junction_char = "*"
+ self.x.format = True
+ self.x.attributes = {"class": "prettytable"}
+ assert self.x.get_string() == self.x[:].get_string()
+
+ def testSetForOneColumn(self):
+ self.x.align["Rainfall"] = "l"
+ self.x.max_width["Name"] = 10
+ self.x.int_format["Population"] = "4"
+ self.x.float_format["Area"] = "2.2"
+ assert self.x.get_string() == self.x[:].get_string()
+
+
+class BasicTests(CityDataTest):
+
+ """Some very basic tests."""
+
+ def testNoBlankLines(self):
+
+ """No table should ever have blank lines in it."""
+
+ string = self.x.get_string()
+ lines = string.split("\n")
+ self.assertNotIn("", lines)
+
+ def testAllLengthsEqual(self):
+
+ """All lines in a table should be of the same length."""
+
+ string = self.x.get_string()
+ lines = string.split("\n")
+ lengths = [len(line) for line in lines]
+ lengths = set(lengths)
+ self.assertEqual(len(lengths), 1)
+
+
+class TitleBasicTests(BasicTests):
+
+ """Run the basic tests with a title"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.title = "My table"
+
+
+class NoBorderBasicTests(BasicTests):
+
+ """Run the basic tests with border = False"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.border = False
+
+
+class NoHeaderBasicTests(BasicTests):
+
+ """Run the basic tests with header = False"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.header = False
+
+
+class HrulesNoneBasicTests(BasicTests):
+
+ """Run the basic tests with hrules = NONE"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.hrules = NONE
+
+
+class HrulesAllBasicTests(BasicTests):
+
+ """Run the basic tests with hrules = ALL"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.hrules = ALL
+
+
+class EmptyTableTests(CityDataTest):
+
+ """Make sure the print_empty option works"""
+
+ def setUp(self):
+ CityDataTest.setUp(self)
+ self.y = PrettyTable()
+ self.y.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+
+ def testPrintEmptyTrue(self):
+ assert self.y.get_string(print_empty=True) != ""
+ assert self.x.get_string(print_empty=True) != self.y.get_string(
+ print_empty=True
+ )
+
+ def testPrintEmptyFalse(self):
+ assert self.y.get_string(print_empty=False) == ""
+ assert self.y.get_string(print_empty=False) != self.x.get_string(
+ print_empty=False
+ )
+
+ def testInteractionWithBorder(self):
+ assert self.y.get_string(border=False, print_empty=True) == ""
+
+
+class PresetBasicTests(BasicTests):
+
+ """Run the basic tests after using set_style"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.set_style(MSWORD_FRIENDLY)
+
+
+class SlicingTests(CityDataTest):
+ def setUp(self):
+ CityDataTest.setUp(self)
+
+ def testSliceAll(self):
+ y = self.x[:]
+ assert self.x.get_string() == y.get_string()
+
+ def testSliceFirstTwoRows(self):
+ y = self.x[0:2]
+ string = y.get_string()
+ assert len(string.split("\n")) == 6
+ assert "Adelaide" in string
+ assert "Brisbane" in string
+ assert "Melbourne" not in string
+ assert "Perth" not in string
+
+ def testSliceLastTwoRows(self):
+ y = self.x[-2:]
+ string = y.get_string()
+ assert len(string.split("\n")) == 6
+ assert "Adelaide" not in string
+ assert "Brisbane" not in string
+ assert "Melbourne" in string
+ assert "Perth" in string
+
+
+class SortingTests(CityDataTest):
+ def setUp(self):
+ CityDataTest.setUp(self)
+
+ def testSortBy(self):
+ self.x.sortby = self.x.field_names[0]
+ old = self.x.get_string()
+ for field in self.x.field_names[1:]:
+ self.x.sortby = field
+ new = self.x.get_string()
+ assert new != old
+
+ def testReverseSort(self):
+ for field in self.x.field_names:
+ self.x.sortby = field
+ self.x.reversesort = False
+ forward = self.x.get_string()
+ self.x.reversesort = True
+ backward = self.x.get_string()
+ forward_lines = forward.split("\n")[2:] # Discard header lines
+ backward_lines = backward.split("\n")[2:]
+ backward_lines.reverse()
+ assert forward_lines == backward_lines
+
+ def testSortKey(self):
+ # Test sorting by length of city name
+ def key(vals):
+ vals[0] = len(vals[0])
+ return vals
+
+ self.x.sortby = "City name"
+ self.x.sort_key = key
+ assert (
+ self.x.get_string().strip()
+ == """+-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Perth | 5386 | 1554769 | 869.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Melbourne | 1566 | 3806092 | 646.9 |
++-----------+------+------------+-----------------+
+""".strip()
+ )
+
+ def testSortSlice(self):
+ """Make sure sorting and slicing interact in the expected way"""
+ x = PrettyTable(["Foo"])
+ for i in range(20, 0, -1):
+ x.add_row([i])
+ newstyle = x.get_string(sortby="Foo", end=10)
+ assert "10" in newstyle
+ assert "20" not in newstyle
+ oldstyle = x.get_string(sortby="Foo", end=10, oldsortslice=True)
+ assert "10" not in oldstyle
+ assert "20" in oldstyle
+
+
+class IntegerFormatBasicTests(BasicTests):
+
+ """Run the basic tests after setting an integer format string"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.int_format = "04"
+
+
+class FloatFormatBasicTests(BasicTests):
+
+ """Run the basic tests after setting a float format string"""
+
+ def setUp(self):
+ BasicTests.setUp(self)
+ self.x.float_format = "6.2f"
+
+
+class FloatFormatTests(unittest.TestCase):
+ def setUp(self):
+ self.x = PrettyTable(["Constant", "Value"])
+ self.x.add_row(["Pi", pi])
+ self.x.add_row(["e", e])
+ self.x.add_row(["sqrt(2)", sqrt(2)])
+
+ def testNoDecimals(self):
+ self.x.float_format = ".0f"
+ self.x.caching = False
+ assert "." not in self.x.get_string()
+
+ def testRoundTo5DP(self):
+ self.x.float_format = ".5f"
+ string = self.x.get_string()
+ assert "3.14159" in string
+ assert "3.141592" not in string
+ assert "2.71828" in string
+ assert "2.718281" not in string
+ assert "2.718282" not in string
+ assert "1.41421" in string
+ assert "1.414213" not in string
+
+ def testPadWith2Zeroes(self):
+ self.x.float_format = "06.2f"
+ string = self.x.get_string()
+ assert "003.14" in string
+ assert "002.72" in string
+ assert "001.41" in string
+
+
+class BreakLineTests(unittest.TestCase):
+ def testAsciiBreakLine(self):
+ t = PrettyTable(["Field 1", "Field 2"])
+ t.add_row(["value 1", "value2\nsecond line"])
+ t.add_row(["value 3", "value4"])
+ result = t.get_string(hrules=ALL)
+ assert (
+ result.strip()
+ == """
++---------+-------------+
+| Field 1 | Field 2 |
++---------+-------------+
+| value 1 | value2 |
+| | second line |
++---------+-------------+
+| value 3 | value4 |
++---------+-------------+
+""".strip()
+ )
+
+ t = PrettyTable(["Field 1", "Field 2"])
+ t.add_row(["value 1", "value2\nsecond line"])
+ t.add_row(["value 3\n\nother line", "value4\n\n\nvalue5"])
+ result = t.get_string(hrules=ALL)
+ assert (
+ result.strip()
+ == """
++------------+-------------+
+| Field 1 | Field 2 |
++------------+-------------+
+| value 1 | value2 |
+| | second line |
++------------+-------------+
+| value 3 | value4 |
+| | |
+| other line | |
+| | value5 |
++------------+-------------+
+""".strip()
+ )
+
+ t = PrettyTable(["Field 1", "Field 2"])
+ t.add_row(["value 1", "value2\nsecond line"])
+ t.add_row(["value 3\n\nother line", "value4\n\n\nvalue5"])
+ result = t.get_string()
+ assert (
+ result.strip()
+ == """
++------------+-------------+
+| Field 1 | Field 2 |
++------------+-------------+
+| value 1 | value2 |
+| | second line |
+| value 3 | value4 |
+| | |
+| other line | |
+| | value5 |
++------------+-------------+
+""".strip()
+ )
+
+ def testHtmlBreakLine(self):
+ t = PrettyTable(["Field 1", "Field 2"])
+ t.add_row(["value 1", "value2\nsecond line"])
+ t.add_row(["value 3", "value4"])
+ result = t.get_html_string(hrules=ALL)
+ assert (
+ result.strip()
+ == """
+<table>
+ <tr>
+ <th>Field 1</th>
+ <th>Field 2</th>
+ </tr>
+ <tr>
+ <td>value 1</td>
+ <td>value2<br>second line</td>
+ </tr>
+ <tr>
+ <td>value 3</td>
+ <td>value4</td>
+ </tr>
+</table>
+""".strip()
+ )
+
+
+class JSONOutputTests(unittest.TestCase):
+ def testJSONOutput(self):
+ t = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ t.add_row(["value 1", "value2", "value3"])
+ t.add_row(["value 4", "value5", "value6"])
+ t.add_row(["value 7", "value8", "value9"])
+ result = t.get_json_string()
+ assert (
+ result.strip()
+ == """[
+ [
+ "Field 1",
+ "Field 2",
+ "Field 3"
+ ],
+ {
+ "Field 1": "value 1",
+ "Field 2": "value2",
+ "Field 3": "value3"
+ },
+ {
+ "Field 1": "value 4",
+ "Field 2": "value5",
+ "Field 3": "value6"
+ },
+ {
+ "Field 1": "value 7",
+ "Field 2": "value8",
+ "Field 3": "value9"
+ }
+]""".strip()
+ )
+
+
+class HtmlOutputTests(unittest.TestCase):
+ def testHtmlOutput(self):
+ t = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ t.add_row(["value 1", "value2", "value3"])
+ t.add_row(["value 4", "value5", "value6"])
+ t.add_row(["value 7", "value8", "value9"])
+ result = t.get_html_string()
+ assert (
+ result.strip()
+ == """
+<table>
+ <tr>
+ <th>Field 1</th>
+ <th>Field 2</th>
+ <th>Field 3</th>
+ </tr>
+ <tr>
+ <td>value 1</td>
+ <td>value2</td>
+ <td>value3</td>
+ </tr>
+ <tr>
+ <td>value 4</td>
+ <td>value5</td>
+ <td>value6</td>
+ </tr>
+ <tr>
+ <td>value 7</td>
+ <td>value8</td>
+ <td>value9</td>
+ </tr>
+</table>
+""".strip()
+ )
+
+ def testHtmlOutputFormated(self):
+ t = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ t.add_row(["value 1", "value2", "value3"])
+ t.add_row(["value 4", "value5", "value6"])
+ t.add_row(["value 7", "value8", "value9"])
+ result = t.get_html_string(format=True)
+ assert (
+ result.strip()
+ == """
+<table frame="box" rules="cols">
+ <tr>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 1</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 2</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 3</th>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 1</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value2</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value3</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 4</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value5</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value6</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 7</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value8</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value9</td>
+ </tr>
+</table>
+""".strip() # noqa: E501
+ )
+
+
+class MarkdownStyleTest(BasicTests):
+ def testMarkdownStyle(self):
+ t = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ t.add_row(["value 1", "value2", "value3"])
+ t.add_row(["value 4", "value5", "value6"])
+ t.add_row(["value 7", "value8", "value9"])
+ t.set_style(MARKDOWN)
+ result = t.get_string()
+ assert (
+ result.strip()
+ == """
+| Field 1 | Field 2 | Field 3 |
+|---------|---------|---------|
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+""".strip()
+ )
+
+
+class OrgmodeStyleTest(BasicTests):
+ def testOrgmodeStyle(self):
+ t = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ t.add_row(["value 1", "value2", "value3"])
+ t.add_row(["value 4", "value5", "value6"])
+ t.add_row(["value 7", "value8", "value9"])
+ t.set_style(ORGMODE)
+ result = t.get_string()
+ assert (
+ result.strip()
+ == """
+|---------+---------+---------|
+| Field 1 | Field 2 | Field 3 |
+|---------+---------+---------|
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+|---------+---------+---------|
+""".strip()
+ )
+
+
+class CsvConstructorTest(BasicTests):
+ def setUp(self):
+
+ csv_string = """City name, Area , Population , Annual Rainfall
+ Sydney, 2058 , 4336374 , 1214.8
+ Melbourne, 1566 , 3806092 , 646.9
+ Brisbane, 5905 , 1857594 , 1146.4
+ Perth, 5386 , 1554769 , 869.4
+ Adelaide, 1295 , 1158259 , 600.5
+ Hobart, 1357 , 205556 , 619.5
+ Darwin, 0112 , 120900 , 1714.7"""
+ csv_fp = StringIO.StringIO(csv_string)
+ self.x = from_csv(csv_fp)
+
+
+class CsvOutputTests(unittest.TestCase):
+ def testCsvOutput(self):
+ t = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ t.add_row(["value 1", "value2", "value3"])
+ t.add_row(["value 4", "value5", "value6"])
+ t.add_row(["value 7", "value8", "value9"])
+ self.assertEqual(
+ t.get_csv_string(delimiter="\t", header=False),
+ "value 1\tvalue2\tvalue3\r\n"
+ "value 4\tvalue5\tvalue6\r\n"
+ "value 7\tvalue8\tvalue9\r\n",
+ )
+ self.assertEqual(
+ t.get_csv_string(),
+ "Field 1,Field 2,Field 3\r\n"
+ "value 1,value2,value3\r\n"
+ "value 4,value5,value6\r\n"
+ "value 7,value8,value9\r\n",
+ )
+
+
+if _have_sqlite:
+
+ class DatabaseConstructorTest(BasicTests):
+ def setUp(self):
+ self.conn = sqlite3.connect(":memory:")
+ self.cur = self.conn.cursor()
+ self.cur.execute(
+ "CREATE TABLE cities "
+ "(name TEXT, area INTEGER, population INTEGER, rainfall REAL)"
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Adelaide", 1295, 1158259, 600.5)'
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Brisbane", 5905, 1857594, 1146.4)'
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Darwin", 112, 120900, 1714.7)'
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Hobart", 1357, 205556, 619.5)'
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Sydney", 2058, 4336374, 1214.8)'
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Melbourne", 1566, 3806092, 646.9)'
+ )
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Perth", 5386, 1554769, 869.4)'
+ )
+ self.cur.execute("SELECT * FROM cities")
+ self.x = from_db_cursor(self.cur)
+
+ def testNonSelectCursor(self):
+ self.cur.execute(
+ 'INSERT INTO cities VALUES ("Adelaide", 1295, 1158259, 600.5)'
+ )
+ assert from_db_cursor(self.cur) is None
+
+
+class JSONConstructorTest(CityDataTest):
+ def testJSONAndBack(self):
+ json_string = self.x.get_json_string()
+ new_table = from_json(json_string)
+ assert new_table.get_string() == self.x.get_string()
+
+
+class HtmlConstructorTest(CityDataTest):
+ def testHtmlAndBack(self):
+ html_string = self.x.get_html_string()
+ new_table = from_html(html_string)[0]
+ assert new_table.get_string() == self.x.get_string()
+
+ def testHtmlOneAndBack(self):
+ html_string = self.x.get_html_string()
+ new_table = from_html_one(html_string)
+ assert new_table.get_string() == self.x.get_string()
+
+ def testHtmlOneFailOnMany(self):
+ html_string = self.x.get_html_string()
+ html_string += self.x.get_html_string()
+ self.assertRaises(Exception, from_html_one, html_string)
+
+
+class PrintEnglishTest(CityDataTest):
+ def testPrint(self):
+ print()
+ print(self.x)
+
+
+class PrintJapaneseTest(unittest.TestCase):
+ def setUp(self):
+
+ self.x = PrettyTable(["Kanji", "Hiragana", "English"])
+ self.x.add_row(["神戸", "こうべ", "Kobe"])
+ self.x.add_row(["京都", "きょうと", "Kyoto"])
+ self.x.add_row(["長崎", "ながさき", "Nagasaki"])
+ self.x.add_row(["名古屋", "なごや", "Nagoya"])
+ self.x.add_row(["大阪", "おおさか", "Osaka"])
+ self.x.add_row(["札幌", "さっぽろ", "Sapporo"])
+ self.x.add_row(["東京", "とうきょう", "Tokyo"])
+ self.x.add_row(["横浜", "よこはま", "Yokohama"])
+
+ def testPrint(self):
+ print()
+ print(self.x)
+
+
+class PrintEmojiTest(unittest.TestCase):
+ def setUp(self):
+ thunder1 = [
+ '\033[38;5;226m _`/""\033[38;5;250m.-. \033[0m',
+ "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m",
+ "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m",
+ "\033[38;5;228;5m ⚡\033[38;5;111;25mʻ ʻ\033[38;5;228;5m"
+ "⚡\033[38;5;111;25mʻ ʻ \033[0m",
+ "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m",
+ ]
+ thunder2 = [
+ "\033[38;5;240;1m .-. \033[0m",
+ "\033[38;5;240;1m ( ). \033[0m",
+ "\033[38;5;240;1m (___(__) \033[0m",
+ "\033[38;5;21;1m ‚ʻ\033[38;5;228;5m⚡\033[38;5;21;25mʻ‚\033[38;5;228;5m"
+ "⚡\033[38;5;21;25m‚ʻ \033[0m",
+ "\033[38;5;21;1m ‚ʻ‚ʻ\033[38;5;228;5m⚡\033[38;5;21;25mʻ‚ʻ \033[0m",
+ ]
+ self.x = PrettyTable(["Thunderbolt", "Lightning"])
+ for i in range(len(thunder1)):
+ self.x.add_row([thunder1[i], thunder2[i]])
+
+ def testPrint(self):
+ print()
+ print(self.x)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/contrib/python/prettytable/py2/tests/ya.make b/contrib/python/prettytable/py2/tests/ya.make
new file mode 100644
index 0000000000..1f31bbb75c
--- /dev/null
+++ b/contrib/python/prettytable/py2/tests/ya.make
@@ -0,0 +1,15 @@
+PY2TEST()
+
+SUBSCRIBER(g:python-contrib)
+
+PEERDIR(
+ contrib/python/prettytable
+)
+
+TEST_SRCS(
+ prettytable_test.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/prettytable/py2/ya.make b/contrib/python/prettytable/py2/ya.make
new file mode 100644
index 0000000000..c1d48de277
--- /dev/null
+++ b/contrib/python/prettytable/py2/ya.make
@@ -0,0 +1,33 @@
+# Generated by devtools/yamaker (pypi).
+
+PY2_LIBRARY()
+
+SUBSCRIBER(manushkin g:python-contrib)
+
+VERSION(1.0.1)
+
+LICENSE(BSD-3-Clause)
+
+PEERDIR(
+ contrib/python/setuptools
+ contrib/python/wcwidth
+)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ prettytable.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/prettytable/py2/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)
diff --git a/contrib/python/prettytable/py3/.dist-info/METADATA b/contrib/python/prettytable/py3/.dist-info/METADATA
new file mode 100644
index 0000000000..6b15eb6252
--- /dev/null
+++ b/contrib/python/prettytable/py3/.dist-info/METADATA
@@ -0,0 +1,732 @@
+Metadata-Version: 2.1
+Name: prettytable
+Version: 3.10.0
+Summary: A simple Python library for easily displaying tabular data in a visually appealing ASCII table format
+Project-URL: Changelog, https://github.com/jazzband/prettytable/releases
+Project-URL: Homepage, https://github.com/jazzband/prettytable
+Project-URL: Source, https://github.com/jazzband/prettytable
+Author-email: Luke Maurits <luke@maurits.id.au>
+Maintainer: Jazzband
+License: BSD (3 clause)
+License-File: LICENSE
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3 :: Only
+Classifier: Programming Language :: Python :: 3.8
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Topic :: Text Processing
+Classifier: Typing :: Typed
+Requires-Python: >=3.8
+Requires-Dist: wcwidth
+Provides-Extra: tests
+Requires-Dist: pytest; extra == 'tests'
+Requires-Dist: pytest-cov; extra == 'tests'
+Requires-Dist: pytest-lazy-fixtures; extra == 'tests'
+Description-Content-Type: text/markdown
+
+# PrettyTable
+
+[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
+[![PyPI version](https://img.shields.io/pypi/v/prettytable.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![Supported Python versions](https://img.shields.io/pypi/pyversions/prettytable.svg?logo=python&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![PyPI downloads](https://img.shields.io/pypi/dm/prettytable.svg)](https://pypistats.org/packages/prettytable)
+[![GitHub Actions status](https://github.com/jazzband/prettytable/workflows/Test/badge.svg)](https://github.com/jazzband/prettytable/actions)
+[![codecov](https://codecov.io/gh/jazzband/prettytable/branch/main/graph/badge.svg)](https://codecov.io/gh/jazzband/prettytable)
+[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+PrettyTable lets you print tables in an attractive ASCII form:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+## Installation
+
+Install via pip:
+
+ python -m pip install -U prettytable
+
+Install latest development version:
+
+ python -m pip install -U git+https://github.com/jazzband/prettytable
+
+Or from `requirements.txt`:
+
+ -e git://github.com/jazzband/prettytable.git#egg=prettytable
+
+## Tutorial on how to use the PrettyTable API
+
+### Getting your data into (and out of) the table
+
+Let's suppose you have a shiny new PrettyTable:
+
+```python
+from prettytable import PrettyTable
+table = PrettyTable()
+```
+
+and you want to put some data into it. You have a few options.
+
+#### Row by row
+
+You can add data one row at a time. To do this you can set the field names first using
+the `field_names` attribute, and then add the rows one at a time using the `add_row`
+method:
+
+```python
+table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+table.add_row(["Adelaide", 1295, 1158259, 600.5])
+table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+table.add_row(["Darwin", 112, 120900, 1714.7])
+table.add_row(["Hobart", 1357, 205556, 619.5])
+table.add_row(["Sydney", 2058, 4336374, 1214.8])
+table.add_row(["Melbourne", 1566, 3806092, 646.9])
+table.add_row(["Perth", 5386, 1554769, 869.4])
+```
+
+#### All rows at once
+
+When you have a list of rows, you can add them in one go with `add_rows`:
+
+```python
+table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+table.add_rows(
+ [
+ ["Adelaide", 1295, 1158259, 600.5],
+ ["Brisbane", 5905, 1857594, 1146.4],
+ ["Darwin", 112, 120900, 1714.7],
+ ["Hobart", 1357, 205556, 619.5],
+ ["Sydney", 2058, 4336374, 1214.8],
+ ["Melbourne", 1566, 3806092, 646.9],
+ ["Perth", 5386, 1554769, 869.4],
+ ]
+)
+```
+
+#### Column by column
+
+You can add data one column at a time as well. To do this you use the `add_column`
+method, which takes two arguments - a string which is the name for the field the column
+you are adding corresponds to, and a list or tuple which contains the column data:
+
+```python
+table.add_column("City name",
+["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
+table.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
+table.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092,
+1554769])
+table.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9,
+869.4])
+```
+
+#### Mixing and matching
+
+If you really want to, you can even mix and match `add_row` and `add_column` and build
+some of your table in one way and some of it in the other. Tables built this way are
+kind of confusing for other people to read, though, so don't do this unless you have a
+good reason.
+
+#### Importing data from a CSV file
+
+If you have your table data in a comma-separated values file (.csv), you can read this
+data into a PrettyTable like this:
+
+```python
+from prettytable import from_csv
+with open("myfile.csv") as fp:
+ mytable = from_csv(fp)
+```
+
+#### Importing data from a database cursor
+
+If you have your table data in a database which you can access using a library which
+confirms to the Python DB-API (e.g. an SQLite database accessible using the `sqlite`
+module), then you can build a PrettyTable using a cursor object, like this:
+
+```python
+import sqlite3
+from prettytable import from_db_cursor
+
+connection = sqlite3.connect("mydb.db")
+cursor = connection.cursor()
+cursor.execute("SELECT field1, field2, field3 FROM my_table")
+mytable = from_db_cursor(cursor)
+```
+
+#### Getting data out
+
+There are three ways to get data out of a PrettyTable, in increasing order of
+completeness:
+
+- The `del_row` method takes an integer index of a single row to delete.
+- The `del_column` method takes a field name of a single column to delete.
+- The `clear_rows` method takes no arguments and deletes all the rows in the table - but
+ keeps the field names as they were so you that you can repopulate it with the same
+ kind of data.
+- The `clear` method takes no arguments and deletes all rows and all field names. It's
+ not quite the same as creating a fresh table instance, though - style related
+ settings, discussed later, are maintained.
+
+### Displaying your table in ASCII form
+
+PrettyTable's main goal is to let you print tables in an attractive ASCII form, like
+this:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+You can print tables like this to `stdout` or get string representations of them.
+
+#### Printing
+
+To print a table in ASCII form, you can just do this:
+
+```python
+print(table)
+```
+
+The old `table.printt()` method from versions 0.5 and earlier has been removed.
+
+To pass options changing the look of the table, use the `get_string()` method documented
+below:
+
+```python
+print(table.get_string())
+```
+
+#### Stringing
+
+If you don't want to actually print your table in ASCII form but just get a string
+containing what _would_ be printed if you use `print(table)`, you can use the
+`get_string` method:
+
+```python
+mystring = table.get_string()
+```
+
+This string is guaranteed to look exactly the same as what would be printed by doing
+`print(table)`. You can now do all the usual things you can do with a string, like write
+your table to a file or insert it into a GUI.
+
+The table can be displayed in several different formats using `get_formatted_string` by
+changing the `out_format=<text|html|json|csv|latex>`. This function passes through
+arguments to the functions that render the table, so additional arguments can be given.
+This provides a way to let a user choose the output formatting.
+
+```python
+def my_cli_function(table_format: str = 'text'):
+ ...
+ print(table.get_formatted_string(table_format))
+```
+
+#### Controlling which data gets displayed
+
+If you like, you can restrict the output of `print(table)` or `table.get_string` to only
+the fields or rows you like.
+
+The `fields` argument to these methods takes a list of field names to be printed:
+
+```python
+print(table.get_string(fields=["City name", "Population"]))
+```
+
+gives:
+
+```
++-----------+------------+
+| City name | Population |
++-----------+------------+
+| Adelaide | 1158259 |
+| Brisbane | 1857594 |
+| Darwin | 120900 |
+| Hobart | 205556 |
+| Melbourne | 3806092 |
+| Perth | 1554769 |
+| Sydney | 4336374 |
++-----------+------------+
+```
+
+The `start` and `end` arguments take the index of the first and last row to print
+respectively. Note that the indexing works like Python list slicing - to print the 2nd,
+3rd and 4th rows of the table, set `start` to 1 (the first row is row 0, so the second
+is row 1) and set `end` to 4 (the index of the 4th row, plus 1):
+
+```python
+print(table.get_string(start=1, end=4))
+```
+
+prints:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
++-----------+------+------------+-----------------+
+```
+
+#### Changing the alignment of columns
+
+By default, all columns in a table are centre aligned.
+
+##### All columns at once
+
+You can change the alignment of all the columns in a table at once by assigning a one
+character string to the `align` attribute. The allowed strings are `"l"`, `"r"` and
+`"c"` for left, right and centre alignment, respectively:
+
+```python
+table.align = "r"
+print(table)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### One column at a time
+
+You can also change the alignment of individual columns based on the corresponding field
+name by treating the `align` attribute as if it were a dictionary.
+
+```python
+table.align["City name"] = "l"
+table.align["Area"] = "c"
+table.align["Population"] = "r"
+table.align["Annual Rainfall"] = "c"
+print(table)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### Sorting your table by a field
+
+You can make sure that your ASCII tables are produced with the data sorted by one
+particular field by giving `get_string` a `sortby` keyword argument, which must be a
+string containing the name of one field.
+
+For example, to print the example table we built earlier of Australian capital city
+data, so that the most populated city comes last, we can do this:
+
+```python
+print(table.get_string(sortby="Population"))
+```
+
+to get:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+If we want the most populated city to come _first_, we can also give a
+`reversesort=True` argument.
+
+If you _always_ want your tables to be sorted in a certain way, you can make the setting
+long-term like this:
+
+```python
+table.sortby = "Population"
+print(table)
+print(table)
+print(table)
+```
+
+All three tables printed by this code will be sorted by population (you could do
+`table.reversesort = True` as well, if you wanted). The behaviour will persist until you
+turn it off:
+
+```python
+table.sortby = None
+```
+
+If you want to specify a custom sorting function, you can use the `sort_key` keyword
+argument. Pass this a function which accepts two lists of values and returns a negative
+or positive value depending on whether the first list should appear before or after the
+second one. If your table has n columns, each list will have n+1 elements. Each list
+corresponds to one row of the table. The first element will be whatever data is in the
+relevant row, in the column specified by the `sort_by` argument. The remaining n
+elements are the data in each of the table's columns, in order, including a repeated
+instance of the data in the `sort_by` column.
+
+#### Adding sections to a table
+
+You can divide your table into different sections using the `divider` argument. This
+will add a dividing line into the table under the row who has this field set. So we can
+set up a table like this:
+
+```python
+table = PrettyTable()
+table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+table.add_row(["Adelaide", 1295, 1158259, 600.5])
+table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+table.add_row(["Darwin", 112, 120900, 1714.7])
+table.add_row(["Hobart", 1357, 205556, 619.5], divider=True)
+table.add_row(["Melbourne", 1566, 3806092, 646.9])
+table.add_row(["Perth", 5386, 1554769, 869.4])
+table.add_row(["Sydney", 2058, 4336374, 1214.8])
+```
+
+to get a table like this:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
++-----------+------+------------+-----------------+
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+Any added dividers will be removed if a table is sorted.
+
+### Changing the appearance of your table - the easy way
+
+By default, PrettyTable produces ASCII tables that look like the ones used in SQL
+database shells. But it can print them in a variety of other formats as well. If the
+format you want to use is common, PrettyTable makes this easy for you to do using the
+`set_style` method. If you want to produce an uncommon table, you'll have to do things
+slightly harder (see later).
+
+#### Setting a table style
+
+You can set the style for your table using the `set_style` method before any calls to
+`print` or `get_string`. Here's how to print a table in Markdown format:
+
+```python
+from prettytable import MARKDOWN
+table.set_style(MARKDOWN)
+print(table)
+```
+
+In addition to `MARKDOWN` you can use these in-built styles:
+
+- `DEFAULT` - The default look, used to undo any style changes you may have made
+- `PLAIN_COLUMNS` - A borderless style that works well with command line programs for
+ columnar data
+- `MSWORD_FRIENDLY` - A format which works nicely with Microsoft Word's "Convert to
+ table" feature
+- `ORGMODE` - A table style that fits [Org mode](https://orgmode.org/) syntax
+- `SINGLE_BORDER` and `DOUBLE_BORDER` - Styles that use continuous single/double border
+ lines with Box drawing characters for a fancier display on terminal
+
+Other styles are likely to appear in future releases.
+
+### Changing the appearance of your table - the hard way
+
+If you want to display your table in a style other than one of the in-built styles
+listed above, you'll have to set things up the hard way.
+
+Don't worry, it's not really that hard!
+
+#### Style options
+
+PrettyTable has a number of style options which control various aspects of how tables
+are displayed. You have the freedom to set each of these options individually to
+whatever you prefer. The `set_style` method just does this automatically for you.
+
+The options are:
+
+| Option | Details |
+| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `border` | A Boolean option (must be `True` or `False`). Controls whether a border is drawn inside and around the table. |
+| `preserve_internal_border` | A Boolean option (must be `True` or `False`). Controls whether borders are still drawn within the table even when `border=False`. |
+| `header` | A Boolean option (must be `True` or `False`). Controls whether the first row of the table is a header showing the names of all the fields. |
+| `hrules` | Controls printing of horizontal rules after rows. Allowed values: `FRAME`, `HEADER`, `ALL`, `NONE`. |
+| `HEADER`, `ALL`, `NONE` | These are variables defined inside the `prettytable` module so make sure you import them or use `prettytable.FRAME` etc. |
+| `vrules` | Controls printing of vertical rules between columns. Allowed values: `FRAME`, `ALL`, `NONE`. |
+| `int_format` | A string which controls the way integer data is printed. This works like: `print("%<int_format>d" % data)`. |
+| `float_format` | A string which controls the way floating point data is printed. This works like: `print("%<float_format>f" % data)`. |
+| `custom_format` | A dictionary of field and callable. This allows you to set any format you want `pf.custom_format["my_col_int"] = ()lambda f, v: f"{v:,}"`. The type of the callable if `callable[[str, Any], str]` |
+| `padding_width` | Number of spaces on either side of column data (only used if left and right paddings are `None`). |
+| `left_padding_width` | Number of spaces on left-hand side of column data. |
+| `right_padding_width` | Number of spaces on right-hand side of column data. |
+| `vertical_char` | Single character string used to draw vertical lines. Default: `\|`. |
+| `horizontal_char` | Single character string used to draw horizontal lines. Default: `-`. |
+| `_horizontal_align_char` | Single character string used to indicate column alignment in horizontal lines. Default: `:` for Markdown, otherwise `None`. |
+| `junction_char` | Single character string used to draw line junctions. Default: `+`. |
+| `top_junction_char` | Single character string used to draw top line junctions. Default: `junction_char`. |
+| `bottom_junction_char` | single character string used to draw bottom line junctions. Default: `junction_char`. |
+| `right_junction_char` | Single character string used to draw right line junctions. Default: `junction_char`. |
+| `left_junction_char` | Single character string used to draw left line junctions. Default: `junction_char`. |
+| `top_right_junction_char` | Single character string used to draw top-right line junctions. Default: `junction_char`. |
+| `top_left_junction_char` | Single character string used to draw top-left line junctions. Default: `junction_char`. |
+| `bottom_right_junction_char` | Single character string used to draw bottom-right line junctions. Default: `junction_char`. |
+| `bottom_left_junction_char` | Single character string used to draw bottom-left line junctions. Default: `junction_char`. |
+
+You can set the style options to your own settings in two ways:
+
+#### Setting style options for the long term
+
+If you want to print your table with a different style several times, you can set your
+option for the long term just by changing the appropriate attributes. If you never want
+your tables to have borders you can do this:
+
+```python
+table.border = False
+print(table)
+print(table)
+print(table)
+```
+
+Neither of the 3 tables printed by this will have borders, even if you do things like
+add extra rows in between them. The lack of borders will last until you do:
+
+```python
+table.border = True
+```
+
+to turn them on again. This sort of long-term setting is exactly how `set_style` works.
+`set_style` just sets a bunch of attributes to pre-set values for you.
+
+Note that if you know what style options you want at the moment you are creating your
+table, you can specify them using keyword arguments to the constructor. For example, the
+following two code blocks are equivalent:
+
+```python
+table = PrettyTable()
+table.border = False
+table.header = False
+table.padding_width = 5
+
+table = PrettyTable(border=False, header=False, padding_width=5)
+```
+
+#### Changing style options just once
+
+If you don't want to make long-term style changes by changing an attribute like in the
+previous section, you can make changes that last for just one `get_string` by giving
+those methods keyword arguments. To print two "normal" tables with one borderless table
+between them, you could do this:
+
+```python
+print(table)
+print(table.get_string(border=False))
+print(table)
+```
+
+### Changing the appearance of your table - with _colors_!
+
+PrettyTable has the functionality of printing your table with ANSI color codes. This
+includes support for most Windows versions through
+[Colorama](https://pypi.org/project/colorama/). To get started, import the `ColorTable`
+class instead of `PrettyTable`.
+
+```diff
+-from prettytable import PrettyTable
++from prettytable.colortable import ColorTable
+```
+
+The `ColorTable` class can be used the same as `PrettyTable`, but it adds an extra
+property. You can now specify a custom _theme_ that will format your table with colors.
+
+```python
+from prettytable.colortable import ColorTable, Themes
+
+table = ColorTable(theme=Themes.OCEAN)
+
+print(table)
+```
+
+#### Creating a custom theme
+
+The `Theme` class allows you to customize both the characters and colors used in your
+table.
+
+| Argument | Description |
+| ---------------------------------------------------------- | --------------------------------------------------------- |
+| `default_color` | The color to use as default |
+| `vertical_char`, `horizontal_char`, and `junction_char` | The characters used for creating the outline of the table |
+| `vertical_color`, `horizontal_color`, and `junction_color` | The colors used to style each character. |
+
+> **Note:** Colors are formatted with the `Theme.format_code(s: str)` function. It
+> accepts a string. If the string starts with an escape code (like `\x1b`) then it will
+> return the given string. If the string is just whitespace, it will return `""`. If the
+> string is a number (like `"34"`), it will automatically format it into an escape code.
+> I recommend you look into the source code for more information.
+
+### Displaying your table in JSON
+
+PrettyTable will also print your tables in JSON, as a list of fields and an array of
+rows. Just like in ASCII form, you can actually get a string representation - just use
+`get_json_string()`.
+
+### Displaying your table in HTML form
+
+PrettyTable will also print your tables in HTML form, as `<table>`s. Just like in ASCII
+form, you can actually get a string representation - just use `get_html_string()`. HTML
+printing supports the `fields`, `start`, `end`, `sortby` and `reversesort` arguments in
+exactly the same way as ASCII printing.
+
+#### Styling HTML tables
+
+By default, PrettyTable outputs HTML for "vanilla" tables. The HTML code is quite
+simple. It looks like this:
+
+```html
+<table>
+ <thead>
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Adelaide</td>
+ <td>1295</td>
+ <td>1158259</td>
+ <td>600.5</td>
+ </tr>
+ <tr>
+ <td>Brisbane</td>
+ <td>5905</td>
+ <td>1857594</td>
+ <td>1146.4</td>
+ ...
+ </tr>
+ </tbody>
+</table>
+```
+
+If you like, you can ask PrettyTable to do its best to mimic the style options that your
+table has set using inline CSS. This is done by giving a `format=True` keyword argument
+to `get_html_string` method. Note that if you _always_ want to print formatted HTML you
+can do:
+
+```python
+table.format = True
+```
+
+and the setting will persist until you turn it off.
+
+Just like with ASCII tables, if you want to change the table's style for just one
+`get_html_string` you can pass those methods' keyword arguments - exactly like `print`
+and `get_string`.
+
+#### Setting HTML attributes
+
+You can provide a dictionary of HTML attribute name/value pairs to the `get_html_string`
+method using the `attributes` keyword argument. This lets you specify common HTML
+attributes like `id` and `class` that can be used for linking to your tables or
+customising their appearance using CSS. For example:
+
+```python
+print(table.get_html_string(attributes={"id":"my_table", "class":"red_table"}))
+```
+
+will print:
+
+```html
+<table id="my_table" class="red_table">
+ <thead>
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ ... ... ...
+ </tr>
+ </tbody>
+</table>
+```
+
+### Miscellaneous things
+
+#### Copying a table
+
+You can call the `copy` method on a PrettyTable object without arguments to return an
+identical independent copy of the table.
+
+If you want a copy of a PrettyTable object with just a subset of the rows, you can use
+list slicing notation:
+
+```python
+new_table = old_table[0:5]
+```
+
+## Contributing
+
+After editing files, use the [Black](https://github.com/psf/black) linter to auto-format
+changed lines.
+
+```sh
+python -m pip install black
+black prettytable*.py
+```
diff --git a/contrib/python/prettytable/py3/.dist-info/top_level.txt b/contrib/python/prettytable/py3/.dist-info/top_level.txt
new file mode 100644
index 0000000000..deb2d14cce
--- /dev/null
+++ b/contrib/python/prettytable/py3/.dist-info/top_level.txt
@@ -0,0 +1 @@
+prettytable
diff --git a/contrib/python/prettytable/py3/LICENSE b/contrib/python/prettytable/py3/LICENSE
new file mode 100644
index 0000000000..cb6fed3eb8
--- /dev/null
+++ b/contrib/python/prettytable/py3/LICENSE
@@ -0,0 +1,30 @@
+# Copyright (c) 2009-2014 Luke Maurits <luke@maurits.id.au>
+# All rights reserved.
+# With contributions from:
+# * Chris Clark
+# * Klein Stephane
+# * John Filleau
+# * Vladimir Vrzić
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
diff --git a/contrib/python/prettytable/py3/README.md b/contrib/python/prettytable/py3/README.md
new file mode 100644
index 0000000000..cc32e6e2e4
--- /dev/null
+++ b/contrib/python/prettytable/py3/README.md
@@ -0,0 +1,700 @@
+# PrettyTable
+
+[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/)
+[![PyPI version](https://img.shields.io/pypi/v/prettytable.svg?logo=pypi&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![Supported Python versions](https://img.shields.io/pypi/pyversions/prettytable.svg?logo=python&logoColor=FFE873)](https://pypi.org/project/prettytable/)
+[![PyPI downloads](https://img.shields.io/pypi/dm/prettytable.svg)](https://pypistats.org/packages/prettytable)
+[![GitHub Actions status](https://github.com/jazzband/prettytable/workflows/Test/badge.svg)](https://github.com/jazzband/prettytable/actions)
+[![codecov](https://codecov.io/gh/jazzband/prettytable/branch/main/graph/badge.svg)](https://codecov.io/gh/jazzband/prettytable)
+[![Code style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
+
+PrettyTable lets you print tables in an attractive ASCII form:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+## Installation
+
+Install via pip:
+
+ python -m pip install -U prettytable
+
+Install latest development version:
+
+ python -m pip install -U git+https://github.com/jazzband/prettytable
+
+Or from `requirements.txt`:
+
+ -e git://github.com/jazzband/prettytable.git#egg=prettytable
+
+## Tutorial on how to use the PrettyTable API
+
+### Getting your data into (and out of) the table
+
+Let's suppose you have a shiny new PrettyTable:
+
+```python
+from prettytable import PrettyTable
+table = PrettyTable()
+```
+
+and you want to put some data into it. You have a few options.
+
+#### Row by row
+
+You can add data one row at a time. To do this you can set the field names first using
+the `field_names` attribute, and then add the rows one at a time using the `add_row`
+method:
+
+```python
+table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+table.add_row(["Adelaide", 1295, 1158259, 600.5])
+table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+table.add_row(["Darwin", 112, 120900, 1714.7])
+table.add_row(["Hobart", 1357, 205556, 619.5])
+table.add_row(["Sydney", 2058, 4336374, 1214.8])
+table.add_row(["Melbourne", 1566, 3806092, 646.9])
+table.add_row(["Perth", 5386, 1554769, 869.4])
+```
+
+#### All rows at once
+
+When you have a list of rows, you can add them in one go with `add_rows`:
+
+```python
+table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+table.add_rows(
+ [
+ ["Adelaide", 1295, 1158259, 600.5],
+ ["Brisbane", 5905, 1857594, 1146.4],
+ ["Darwin", 112, 120900, 1714.7],
+ ["Hobart", 1357, 205556, 619.5],
+ ["Sydney", 2058, 4336374, 1214.8],
+ ["Melbourne", 1566, 3806092, 646.9],
+ ["Perth", 5386, 1554769, 869.4],
+ ]
+)
+```
+
+#### Column by column
+
+You can add data one column at a time as well. To do this you use the `add_column`
+method, which takes two arguments - a string which is the name for the field the column
+you are adding corresponds to, and a list or tuple which contains the column data:
+
+```python
+table.add_column("City name",
+["Adelaide","Brisbane","Darwin","Hobart","Sydney","Melbourne","Perth"])
+table.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
+table.add_column("Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092,
+1554769])
+table.add_column("Annual Rainfall",[600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9,
+869.4])
+```
+
+#### Mixing and matching
+
+If you really want to, you can even mix and match `add_row` and `add_column` and build
+some of your table in one way and some of it in the other. Tables built this way are
+kind of confusing for other people to read, though, so don't do this unless you have a
+good reason.
+
+#### Importing data from a CSV file
+
+If you have your table data in a comma-separated values file (.csv), you can read this
+data into a PrettyTable like this:
+
+```python
+from prettytable import from_csv
+with open("myfile.csv") as fp:
+ mytable = from_csv(fp)
+```
+
+#### Importing data from a database cursor
+
+If you have your table data in a database which you can access using a library which
+confirms to the Python DB-API (e.g. an SQLite database accessible using the `sqlite`
+module), then you can build a PrettyTable using a cursor object, like this:
+
+```python
+import sqlite3
+from prettytable import from_db_cursor
+
+connection = sqlite3.connect("mydb.db")
+cursor = connection.cursor()
+cursor.execute("SELECT field1, field2, field3 FROM my_table")
+mytable = from_db_cursor(cursor)
+```
+
+#### Getting data out
+
+There are three ways to get data out of a PrettyTable, in increasing order of
+completeness:
+
+- The `del_row` method takes an integer index of a single row to delete.
+- The `del_column` method takes a field name of a single column to delete.
+- The `clear_rows` method takes no arguments and deletes all the rows in the table - but
+ keeps the field names as they were so you that you can repopulate it with the same
+ kind of data.
+- The `clear` method takes no arguments and deletes all rows and all field names. It's
+ not quite the same as creating a fresh table instance, though - style related
+ settings, discussed later, are maintained.
+
+### Displaying your table in ASCII form
+
+PrettyTable's main goal is to let you print tables in an attractive ASCII form, like
+this:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+You can print tables like this to `stdout` or get string representations of them.
+
+#### Printing
+
+To print a table in ASCII form, you can just do this:
+
+```python
+print(table)
+```
+
+The old `table.printt()` method from versions 0.5 and earlier has been removed.
+
+To pass options changing the look of the table, use the `get_string()` method documented
+below:
+
+```python
+print(table.get_string())
+```
+
+#### Stringing
+
+If you don't want to actually print your table in ASCII form but just get a string
+containing what _would_ be printed if you use `print(table)`, you can use the
+`get_string` method:
+
+```python
+mystring = table.get_string()
+```
+
+This string is guaranteed to look exactly the same as what would be printed by doing
+`print(table)`. You can now do all the usual things you can do with a string, like write
+your table to a file or insert it into a GUI.
+
+The table can be displayed in several different formats using `get_formatted_string` by
+changing the `out_format=<text|html|json|csv|latex>`. This function passes through
+arguments to the functions that render the table, so additional arguments can be given.
+This provides a way to let a user choose the output formatting.
+
+```python
+def my_cli_function(table_format: str = 'text'):
+ ...
+ print(table.get_formatted_string(table_format))
+```
+
+#### Controlling which data gets displayed
+
+If you like, you can restrict the output of `print(table)` or `table.get_string` to only
+the fields or rows you like.
+
+The `fields` argument to these methods takes a list of field names to be printed:
+
+```python
+print(table.get_string(fields=["City name", "Population"]))
+```
+
+gives:
+
+```
++-----------+------------+
+| City name | Population |
++-----------+------------+
+| Adelaide | 1158259 |
+| Brisbane | 1857594 |
+| Darwin | 120900 |
+| Hobart | 205556 |
+| Melbourne | 3806092 |
+| Perth | 1554769 |
+| Sydney | 4336374 |
++-----------+------------+
+```
+
+The `start` and `end` arguments take the index of the first and last row to print
+respectively. Note that the indexing works like Python list slicing - to print the 2nd,
+3rd and 4th rows of the table, set `start` to 1 (the first row is row 0, so the second
+is row 1) and set `end` to 4 (the index of the 4th row, plus 1):
+
+```python
+print(table.get_string(start=1, end=4))
+```
+
+prints:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
++-----------+------+------------+-----------------+
+```
+
+#### Changing the alignment of columns
+
+By default, all columns in a table are centre aligned.
+
+##### All columns at once
+
+You can change the alignment of all the columns in a table at once by assigning a one
+character string to the `align` attribute. The allowed strings are `"l"`, `"r"` and
+`"c"` for left, right and centre alignment, respectively:
+
+```python
+table.align = "r"
+print(table)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### One column at a time
+
+You can also change the alignment of individual columns based on the corresponding field
+name by treating the `align` attribute as if it were a dictionary.
+
+```python
+table.align["City name"] = "l"
+table.align["Area"] = "c"
+table.align["Population"] = "r"
+table.align["Annual Rainfall"] = "c"
+print(table)
+```
+
+gives:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+##### Sorting your table by a field
+
+You can make sure that your ASCII tables are produced with the data sorted by one
+particular field by giving `get_string` a `sortby` keyword argument, which must be a
+string containing the name of one field.
+
+For example, to print the example table we built earlier of Australian capital city
+data, so that the most populated city comes last, we can do this:
+
+```python
+print(table.get_string(sortby="Population"))
+```
+
+to get:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+If we want the most populated city to come _first_, we can also give a
+`reversesort=True` argument.
+
+If you _always_ want your tables to be sorted in a certain way, you can make the setting
+long-term like this:
+
+```python
+table.sortby = "Population"
+print(table)
+print(table)
+print(table)
+```
+
+All three tables printed by this code will be sorted by population (you could do
+`table.reversesort = True` as well, if you wanted). The behaviour will persist until you
+turn it off:
+
+```python
+table.sortby = None
+```
+
+If you want to specify a custom sorting function, you can use the `sort_key` keyword
+argument. Pass this a function which accepts two lists of values and returns a negative
+or positive value depending on whether the first list should appear before or after the
+second one. If your table has n columns, each list will have n+1 elements. Each list
+corresponds to one row of the table. The first element will be whatever data is in the
+relevant row, in the column specified by the `sort_by` argument. The remaining n
+elements are the data in each of the table's columns, in order, including a repeated
+instance of the data in the `sort_by` column.
+
+#### Adding sections to a table
+
+You can divide your table into different sections using the `divider` argument. This
+will add a dividing line into the table under the row who has this field set. So we can
+set up a table like this:
+
+```python
+table = PrettyTable()
+table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+table.add_row(["Adelaide", 1295, 1158259, 600.5])
+table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+table.add_row(["Darwin", 112, 120900, 1714.7])
+table.add_row(["Hobart", 1357, 205556, 619.5], divider=True)
+table.add_row(["Melbourne", 1566, 3806092, 646.9])
+table.add_row(["Perth", 5386, 1554769, 869.4])
+table.add_row(["Sydney", 2058, 4336374, 1214.8])
+```
+
+to get a table like this:
+
+```
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
++-----------+------+------------+-----------------+
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
+| Sydney | 2058 | 4336374 | 1214.8 |
++-----------+------+------------+-----------------+
+```
+
+Any added dividers will be removed if a table is sorted.
+
+### Changing the appearance of your table - the easy way
+
+By default, PrettyTable produces ASCII tables that look like the ones used in SQL
+database shells. But it can print them in a variety of other formats as well. If the
+format you want to use is common, PrettyTable makes this easy for you to do using the
+`set_style` method. If you want to produce an uncommon table, you'll have to do things
+slightly harder (see later).
+
+#### Setting a table style
+
+You can set the style for your table using the `set_style` method before any calls to
+`print` or `get_string`. Here's how to print a table in Markdown format:
+
+```python
+from prettytable import MARKDOWN
+table.set_style(MARKDOWN)
+print(table)
+```
+
+In addition to `MARKDOWN` you can use these in-built styles:
+
+- `DEFAULT` - The default look, used to undo any style changes you may have made
+- `PLAIN_COLUMNS` - A borderless style that works well with command line programs for
+ columnar data
+- `MSWORD_FRIENDLY` - A format which works nicely with Microsoft Word's "Convert to
+ table" feature
+- `ORGMODE` - A table style that fits [Org mode](https://orgmode.org/) syntax
+- `SINGLE_BORDER` and `DOUBLE_BORDER` - Styles that use continuous single/double border
+ lines with Box drawing characters for a fancier display on terminal
+
+Other styles are likely to appear in future releases.
+
+### Changing the appearance of your table - the hard way
+
+If you want to display your table in a style other than one of the in-built styles
+listed above, you'll have to set things up the hard way.
+
+Don't worry, it's not really that hard!
+
+#### Style options
+
+PrettyTable has a number of style options which control various aspects of how tables
+are displayed. You have the freedom to set each of these options individually to
+whatever you prefer. The `set_style` method just does this automatically for you.
+
+The options are:
+
+| Option | Details |
+| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `border` | A Boolean option (must be `True` or `False`). Controls whether a border is drawn inside and around the table. |
+| `preserve_internal_border` | A Boolean option (must be `True` or `False`). Controls whether borders are still drawn within the table even when `border=False`. |
+| `header` | A Boolean option (must be `True` or `False`). Controls whether the first row of the table is a header showing the names of all the fields. |
+| `hrules` | Controls printing of horizontal rules after rows. Allowed values: `FRAME`, `HEADER`, `ALL`, `NONE`. |
+| `HEADER`, `ALL`, `NONE` | These are variables defined inside the `prettytable` module so make sure you import them or use `prettytable.FRAME` etc. |
+| `vrules` | Controls printing of vertical rules between columns. Allowed values: `FRAME`, `ALL`, `NONE`. |
+| `int_format` | A string which controls the way integer data is printed. This works like: `print("%<int_format>d" % data)`. |
+| `float_format` | A string which controls the way floating point data is printed. This works like: `print("%<float_format>f" % data)`. |
+| `custom_format` | A dictionary of field and callable. This allows you to set any format you want `pf.custom_format["my_col_int"] = ()lambda f, v: f"{v:,}"`. The type of the callable if `callable[[str, Any], str]` |
+| `padding_width` | Number of spaces on either side of column data (only used if left and right paddings are `None`). |
+| `left_padding_width` | Number of spaces on left-hand side of column data. |
+| `right_padding_width` | Number of spaces on right-hand side of column data. |
+| `vertical_char` | Single character string used to draw vertical lines. Default: `\|`. |
+| `horizontal_char` | Single character string used to draw horizontal lines. Default: `-`. |
+| `_horizontal_align_char` | Single character string used to indicate column alignment in horizontal lines. Default: `:` for Markdown, otherwise `None`. |
+| `junction_char` | Single character string used to draw line junctions. Default: `+`. |
+| `top_junction_char` | Single character string used to draw top line junctions. Default: `junction_char`. |
+| `bottom_junction_char` | single character string used to draw bottom line junctions. Default: `junction_char`. |
+| `right_junction_char` | Single character string used to draw right line junctions. Default: `junction_char`. |
+| `left_junction_char` | Single character string used to draw left line junctions. Default: `junction_char`. |
+| `top_right_junction_char` | Single character string used to draw top-right line junctions. Default: `junction_char`. |
+| `top_left_junction_char` | Single character string used to draw top-left line junctions. Default: `junction_char`. |
+| `bottom_right_junction_char` | Single character string used to draw bottom-right line junctions. Default: `junction_char`. |
+| `bottom_left_junction_char` | Single character string used to draw bottom-left line junctions. Default: `junction_char`. |
+
+You can set the style options to your own settings in two ways:
+
+#### Setting style options for the long term
+
+If you want to print your table with a different style several times, you can set your
+option for the long term just by changing the appropriate attributes. If you never want
+your tables to have borders you can do this:
+
+```python
+table.border = False
+print(table)
+print(table)
+print(table)
+```
+
+Neither of the 3 tables printed by this will have borders, even if you do things like
+add extra rows in between them. The lack of borders will last until you do:
+
+```python
+table.border = True
+```
+
+to turn them on again. This sort of long-term setting is exactly how `set_style` works.
+`set_style` just sets a bunch of attributes to pre-set values for you.
+
+Note that if you know what style options you want at the moment you are creating your
+table, you can specify them using keyword arguments to the constructor. For example, the
+following two code blocks are equivalent:
+
+```python
+table = PrettyTable()
+table.border = False
+table.header = False
+table.padding_width = 5
+
+table = PrettyTable(border=False, header=False, padding_width=5)
+```
+
+#### Changing style options just once
+
+If you don't want to make long-term style changes by changing an attribute like in the
+previous section, you can make changes that last for just one `get_string` by giving
+those methods keyword arguments. To print two "normal" tables with one borderless table
+between them, you could do this:
+
+```python
+print(table)
+print(table.get_string(border=False))
+print(table)
+```
+
+### Changing the appearance of your table - with _colors_!
+
+PrettyTable has the functionality of printing your table with ANSI color codes. This
+includes support for most Windows versions through
+[Colorama](https://pypi.org/project/colorama/). To get started, import the `ColorTable`
+class instead of `PrettyTable`.
+
+```diff
+-from prettytable import PrettyTable
++from prettytable.colortable import ColorTable
+```
+
+The `ColorTable` class can be used the same as `PrettyTable`, but it adds an extra
+property. You can now specify a custom _theme_ that will format your table with colors.
+
+```python
+from prettytable.colortable import ColorTable, Themes
+
+table = ColorTable(theme=Themes.OCEAN)
+
+print(table)
+```
+
+#### Creating a custom theme
+
+The `Theme` class allows you to customize both the characters and colors used in your
+table.
+
+| Argument | Description |
+| ---------------------------------------------------------- | --------------------------------------------------------- |
+| `default_color` | The color to use as default |
+| `vertical_char`, `horizontal_char`, and `junction_char` | The characters used for creating the outline of the table |
+| `vertical_color`, `horizontal_color`, and `junction_color` | The colors used to style each character. |
+
+> **Note:** Colors are formatted with the `Theme.format_code(s: str)` function. It
+> accepts a string. If the string starts with an escape code (like `\x1b`) then it will
+> return the given string. If the string is just whitespace, it will return `""`. If the
+> string is a number (like `"34"`), it will automatically format it into an escape code.
+> I recommend you look into the source code for more information.
+
+### Displaying your table in JSON
+
+PrettyTable will also print your tables in JSON, as a list of fields and an array of
+rows. Just like in ASCII form, you can actually get a string representation - just use
+`get_json_string()`.
+
+### Displaying your table in HTML form
+
+PrettyTable will also print your tables in HTML form, as `<table>`s. Just like in ASCII
+form, you can actually get a string representation - just use `get_html_string()`. HTML
+printing supports the `fields`, `start`, `end`, `sortby` and `reversesort` arguments in
+exactly the same way as ASCII printing.
+
+#### Styling HTML tables
+
+By default, PrettyTable outputs HTML for "vanilla" tables. The HTML code is quite
+simple. It looks like this:
+
+```html
+<table>
+ <thead>
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Adelaide</td>
+ <td>1295</td>
+ <td>1158259</td>
+ <td>600.5</td>
+ </tr>
+ <tr>
+ <td>Brisbane</td>
+ <td>5905</td>
+ <td>1857594</td>
+ <td>1146.4</td>
+ ...
+ </tr>
+ </tbody>
+</table>
+```
+
+If you like, you can ask PrettyTable to do its best to mimic the style options that your
+table has set using inline CSS. This is done by giving a `format=True` keyword argument
+to `get_html_string` method. Note that if you _always_ want to print formatted HTML you
+can do:
+
+```python
+table.format = True
+```
+
+and the setting will persist until you turn it off.
+
+Just like with ASCII tables, if you want to change the table's style for just one
+`get_html_string` you can pass those methods' keyword arguments - exactly like `print`
+and `get_string`.
+
+#### Setting HTML attributes
+
+You can provide a dictionary of HTML attribute name/value pairs to the `get_html_string`
+method using the `attributes` keyword argument. This lets you specify common HTML
+attributes like `id` and `class` that can be used for linking to your tables or
+customising their appearance using CSS. For example:
+
+```python
+print(table.get_html_string(attributes={"id":"my_table", "class":"red_table"}))
+```
+
+will print:
+
+```html
+<table id="my_table" class="red_table">
+ <thead>
+ <tr>
+ <th>City name</th>
+ <th>Area</th>
+ <th>Population</th>
+ <th>Annual Rainfall</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ ... ... ...
+ </tr>
+ </tbody>
+</table>
+```
+
+### Miscellaneous things
+
+#### Copying a table
+
+You can call the `copy` method on a PrettyTable object without arguments to return an
+identical independent copy of the table.
+
+If you want a copy of a PrettyTable object with just a subset of the rows, you can use
+list slicing notation:
+
+```python
+new_table = old_table[0:5]
+```
+
+## Contributing
+
+After editing files, use the [Black](https://github.com/psf/black) linter to auto-format
+changed lines.
+
+```sh
+python -m pip install black
+black prettytable*.py
+```
diff --git a/contrib/python/prettytable/py3/prettytable/__init__.py b/contrib/python/prettytable/py3/prettytable/__init__.py
new file mode 100644
index 0000000000..7f9bbe27eb
--- /dev/null
+++ b/contrib/python/prettytable/py3/prettytable/__init__.py
@@ -0,0 +1,57 @@
+from __future__ import annotations
+
+from typing import Any
+
+from .prettytable import (
+ ALL,
+ DEFAULT,
+ DOUBLE_BORDER,
+ FRAME,
+ HEADER,
+ MARKDOWN,
+ MSWORD_FRIENDLY,
+ NONE,
+ ORGMODE,
+ PLAIN_COLUMNS,
+ RANDOM,
+ SINGLE_BORDER,
+ PrettyTable,
+ TableHandler,
+ from_csv,
+ from_db_cursor,
+ from_html,
+ from_html_one,
+ from_json,
+)
+
+__all__ = [
+ "ALL",
+ "DEFAULT",
+ "DOUBLE_BORDER",
+ "SINGLE_BORDER",
+ "FRAME",
+ "HEADER",
+ "MARKDOWN",
+ "MSWORD_FRIENDLY",
+ "NONE",
+ "ORGMODE",
+ "PLAIN_COLUMNS",
+ "RANDOM",
+ "PrettyTable",
+ "TableHandler",
+ "from_csv",
+ "from_db_cursor",
+ "from_html",
+ "from_html_one",
+ "from_json",
+]
+
+
+def __getattr__(name: str) -> Any:
+ if name == "__version__":
+ import importlib.metadata
+
+ return importlib.metadata.version(__name__)
+
+ msg = f"module '{__name__}' has no attribute '{name}'"
+ raise AttributeError(msg)
diff --git a/contrib/python/prettytable/py3/prettytable/colortable.py b/contrib/python/prettytable/py3/prettytable/colortable.py
new file mode 100644
index 0000000000..3df50c63d8
--- /dev/null
+++ b/contrib/python/prettytable/py3/prettytable/colortable.py
@@ -0,0 +1,97 @@
+from __future__ import annotations
+
+from .prettytable import PrettyTable
+
+try:
+ from colorama import init
+
+ init()
+except ImportError:
+ pass
+
+
+RESET_CODE = "\x1b[0m"
+
+
+class Theme:
+ def __init__(
+ self,
+ default_color: str = "",
+ vertical_char: str = "|",
+ vertical_color: str = "",
+ horizontal_char: str = "-",
+ horizontal_color: str = "",
+ junction_char: str = "+",
+ junction_color: str = "",
+ ) -> None:
+ self.default_color = Theme.format_code(default_color)
+ self.vertical_char = vertical_char
+ self.vertical_color = Theme.format_code(vertical_color)
+ self.horizontal_char = horizontal_char
+ self.horizontal_color = Theme.format_code(horizontal_color)
+ self.junction_char = junction_char
+ self.junction_color = Theme.format_code(junction_color)
+
+ @staticmethod
+ def format_code(s: str) -> str:
+ """Takes string and intelligently puts it into an ANSI escape sequence"""
+ if s.strip() == "":
+ return ""
+ elif s.startswith("\x1b["):
+ return s
+ else:
+ return f"\x1b[{s}m"
+
+
+class Themes:
+ DEFAULT = Theme()
+ OCEAN = Theme(
+ default_color="96",
+ vertical_color="34",
+ horizontal_color="34",
+ junction_color="36",
+ )
+
+
+class ColorTable(PrettyTable):
+ def __init__(self, field_names=None, **kwargs) -> None:
+ super().__init__(field_names=field_names, **kwargs)
+ # TODO: Validate option
+
+ self.theme = kwargs.get("theme") or Themes.DEFAULT
+
+ @property
+ def theme(self) -> Theme:
+ return self._theme
+
+ @theme.setter
+ def theme(self, value: Theme) -> None:
+ self._theme = value
+ self.update_theme()
+
+ def update_theme(self) -> None:
+ theme = self._theme
+
+ self._vertical_char = (
+ theme.vertical_color
+ + theme.vertical_char
+ + RESET_CODE
+ + theme.default_color
+ )
+
+ self._horizontal_char = (
+ theme.horizontal_color
+ + theme.horizontal_char
+ + RESET_CODE
+ + theme.default_color
+ )
+
+ self._junction_char = (
+ theme.junction_color
+ + theme.junction_char
+ + RESET_CODE
+ + theme.default_color
+ )
+
+ def get_string(self, **kwargs) -> str:
+ return super().get_string(**kwargs) + RESET_CODE
diff --git a/contrib/python/prettytable/py3/prettytable/prettytable.py b/contrib/python/prettytable/py3/prettytable/prettytable.py
new file mode 100644
index 0000000000..df1cb86ab3
--- /dev/null
+++ b/contrib/python/prettytable/py3/prettytable/prettytable.py
@@ -0,0 +1,2587 @@
+#!/usr/bin/env python
+#
+# Copyright (c) 2009-2014, Luke Maurits <luke@maurits.id.au>
+# All rights reserved.
+# With contributions from:
+# * Chris Clark
+# * Klein Stephane
+# * John Filleau
+# * Vladimir Vrzić
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import annotations
+
+import io
+import re
+from html.parser import HTMLParser
+from typing import Any
+
+# hrule styles
+FRAME = 0
+ALL = 1
+NONE = 2
+HEADER = 3
+
+# Table styles
+DEFAULT = 10
+MSWORD_FRIENDLY = 11
+PLAIN_COLUMNS = 12
+MARKDOWN = 13
+ORGMODE = 14
+DOUBLE_BORDER = 15
+SINGLE_BORDER = 16
+RANDOM = 20
+BASE_ALIGN_VALUE = "base_align_value"
+
+_re = re.compile(r"\033\[[0-9;]*m|\033\(B")
+
+
+def _get_size(text):
+ lines = text.split("\n")
+ height = len(lines)
+ width = max(_str_block_width(line) for line in lines)
+ return width, height
+
+
+class PrettyTable:
+ def __init__(self, field_names=None, **kwargs) -> None:
+ """Return a new PrettyTable instance
+
+ Arguments:
+
+ encoding - Unicode encoding scheme used to decode any encoded input
+ title - optional table title
+ field_names - list or tuple of field names
+ fields - list or tuple of field names to include in displays
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ header - print a header showing field names (True or False)
+ header_style - stylisation to apply to field names in header
+ ("cap", "title", "upper", "lower" or None)
+ border - print a border around the table (True or False)
+ preserve_internal_border - print a border inside the table even if
+ border is disabled (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: FRAME, HEADER, ALL, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ custom_format - controls formatting of any column using callable
+ min_table_width - minimum desired table width, in characters
+ max_table_width - maximum desired table width, in characters
+ min_width - minimum desired field width, in characters
+ max_width - maximum desired field width, in characters
+ padding_width - number of spaces on either side of column data
+ (only used if left and right paddings are None)
+ left_padding_width - number of spaces on left hand side of column data
+ right_padding_width - number of spaces on right hand side of column data
+ vertical_char - single character string used to draw vertical lines
+ horizontal_char - single character string used to draw horizontal lines
+ horizontal_align_char - single character string used to indicate alignment
+ junction_char - single character string used to draw line junctions
+ top_junction_char - single character string used to draw top line junctions
+ bottom_junction_char -
+ single character string used to draw bottom line junctions
+ right_junction_char - single character string used to draw right line junctions
+ left_junction_char - single character string used to draw left line junctions
+ top_right_junction_char -
+ single character string used to draw top-right line junctions
+ top_left_junction_char -
+ single character string used to draw top-left line junctions
+ bottom_right_junction_char -
+ single character string used to draw bottom-right line junctions
+ bottom_left_junction_char -
+ single character string used to draw bottom-left line junctions
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ align - default align for each column (None, "l", "c" or "r")
+ valign - default valign for each row (None, "t", "m" or "b")
+ reversesort - True or False to sort in descending or ascending order
+ oldsortslice - Slice rows before sorting in the "old style" """
+
+ self.encoding = kwargs.get("encoding", "UTF-8")
+
+ # Data
+ self._field_names: list[str] = []
+ self._rows: list[list] = []
+ self._dividers: list[bool] = []
+ self.align = {}
+ self.valign = {}
+ self.max_width = {}
+ self.min_width = {}
+ self.int_format = {}
+ self.float_format = {}
+ self.custom_format = {}
+
+ if field_names:
+ self.field_names = field_names
+ else:
+ self._widths: list[int] = []
+
+ # Options
+ self._options = [
+ "title",
+ "start",
+ "end",
+ "fields",
+ "header",
+ "border",
+ "preserve_internal_border",
+ "sortby",
+ "reversesort",
+ "sort_key",
+ "attributes",
+ "format",
+ "hrules",
+ "vrules",
+ "int_format",
+ "float_format",
+ "custom_format",
+ "min_table_width",
+ "max_table_width",
+ "padding_width",
+ "left_padding_width",
+ "right_padding_width",
+ "vertical_char",
+ "horizontal_char",
+ "horizontal_align_char",
+ "junction_char",
+ "header_style",
+ "valign",
+ "xhtml",
+ "print_empty",
+ "oldsortslice",
+ "top_junction_char",
+ "bottom_junction_char",
+ "right_junction_char",
+ "left_junction_char",
+ "top_right_junction_char",
+ "top_left_junction_char",
+ "bottom_right_junction_char",
+ "bottom_left_junction_char",
+ "align",
+ "valign",
+ "max_width",
+ "min_width",
+ "none_format",
+ ]
+ for option in self._options:
+ if option in kwargs:
+ self._validate_option(option, kwargs[option])
+ else:
+ kwargs[option] = None
+
+ self._title = kwargs["title"] or None
+ self._start = kwargs["start"] or 0
+ self._end = kwargs["end"] or None
+ self._fields = kwargs["fields"] or None
+ self._none_format: dict[None, None] = {}
+
+ if kwargs["header"] in (True, False):
+ self._header = kwargs["header"]
+ else:
+ self._header = True
+ self._header_style = kwargs["header_style"] or None
+ if kwargs["border"] in (True, False):
+ self._border = kwargs["border"]
+ else:
+ self._border = True
+ if kwargs["preserve_internal_border"] in (True, False):
+ self._preserve_internal_border = kwargs["preserve_internal_border"]
+ else:
+ self._preserve_internal_border = False
+ self._hrules = kwargs["hrules"] or FRAME
+ self._vrules = kwargs["vrules"] or ALL
+
+ self._sortby = kwargs["sortby"] or None
+ if kwargs["reversesort"] in (True, False):
+ self._reversesort = kwargs["reversesort"]
+ else:
+ self._reversesort = False
+ self._sort_key = kwargs["sort_key"] or (lambda x: x)
+
+ # Column specific arguments, use property.setters
+ self.align = kwargs["align"] or {}
+ self.valign = kwargs["valign"] or {}
+ self.max_width = kwargs["max_width"] or {}
+ self.min_width = kwargs["min_width"] or {}
+ self.int_format = kwargs["int_format"] or {}
+ self.float_format = kwargs["float_format"] or {}
+ self.custom_format = kwargs["custom_format"] or {}
+ self.none_format = kwargs["none_format"] or {}
+
+ self._min_table_width = kwargs["min_table_width"] or None
+ self._max_table_width = kwargs["max_table_width"] or None
+ if kwargs["padding_width"] is None:
+ self._padding_width = 1
+ else:
+ self._padding_width = kwargs["padding_width"]
+ self._left_padding_width = kwargs["left_padding_width"] or None
+ self._right_padding_width = kwargs["right_padding_width"] or None
+
+ self._vertical_char = kwargs["vertical_char"] or "|"
+ self._horizontal_char = kwargs["horizontal_char"] or "-"
+ self._horizontal_align_char = kwargs["horizontal_align_char"]
+ self._junction_char = kwargs["junction_char"] or "+"
+ self._top_junction_char = kwargs["top_junction_char"]
+ self._bottom_junction_char = kwargs["bottom_junction_char"]
+ self._right_junction_char = kwargs["right_junction_char"]
+ self._left_junction_char = kwargs["left_junction_char"]
+ self._top_right_junction_char = kwargs["top_right_junction_char"]
+ self._top_left_junction_char = kwargs["top_left_junction_char"]
+ self._bottom_right_junction_char = kwargs["bottom_right_junction_char"]
+ self._bottom_left_junction_char = kwargs["bottom_left_junction_char"]
+
+ if kwargs["print_empty"] in (True, False):
+ self._print_empty = kwargs["print_empty"]
+ else:
+ self._print_empty = True
+ if kwargs["oldsortslice"] in (True, False):
+ self._oldsortslice = kwargs["oldsortslice"]
+ else:
+ self._oldsortslice = False
+ self._format = kwargs["format"] or False
+ self._xhtml = kwargs["xhtml"] or False
+ self._attributes = kwargs["attributes"] or {}
+
+ def _justify(self, text, width, align):
+ excess = width - _str_block_width(text)
+ if align == "l":
+ return text + excess * " "
+ elif align == "r":
+ return excess * " " + text
+ else:
+ if excess % 2:
+ # Uneven padding
+ # Put more space on right if text is of odd length...
+ if _str_block_width(text) % 2:
+ return (excess // 2) * " " + text + (excess // 2 + 1) * " "
+ # and more space on left if text is of even length
+ else:
+ return (excess // 2 + 1) * " " + text + (excess // 2) * " "
+ # Why distribute extra space this way? To match the behaviour of
+ # the inbuilt str.center() method.
+ else:
+ # Equal padding on either side
+ return (excess // 2) * " " + text + (excess // 2) * " "
+
+ def __getattr__(self, name):
+ if name == "rowcount":
+ return len(self._rows)
+ elif name == "colcount":
+ if self._field_names:
+ return len(self._field_names)
+ elif self._rows:
+ return len(self._rows[0])
+ else:
+ return 0
+ else:
+ raise AttributeError(name)
+
+ def __getitem__(self, index):
+ new = PrettyTable()
+ new.field_names = self.field_names
+ for attr in self._options:
+ setattr(new, "_" + attr, getattr(self, "_" + attr))
+ setattr(new, "_align", getattr(self, "_align"))
+ if isinstance(index, slice):
+ for row in self._rows[index]:
+ new.add_row(row)
+ elif isinstance(index, int):
+ new.add_row(self._rows[index])
+ else:
+ msg = f"Index {index} is invalid, must be an integer or slice"
+ raise IndexError(msg)
+ return new
+
+ def __str__(self) -> str:
+ return self.get_string()
+
+ def __repr__(self) -> str:
+ return self.get_string()
+
+ def _repr_html_(self):
+ """
+ Returns get_html_string value by default
+ as the repr call in Jupyter notebook environment
+ """
+ return self.get_html_string()
+
+ ##############################
+ # ATTRIBUTE VALIDATORS #
+ ##############################
+
+ # The method _validate_option is all that should be used elsewhere in the code base
+ # to validate options. It will call the appropriate validation method for that
+ # option. The individual validation methods should never need to be called directly
+ # (although nothing bad will happen if they *are*).
+ # Validation happens in TWO places.
+ # Firstly, in the property setters defined in the ATTRIBUTE MANAGEMENT section.
+ # Secondly, in the _get_options method, where keyword arguments are mixed with
+ # persistent settings
+
+ def _validate_option(self, option, val) -> None:
+ if option == "field_names":
+ self._validate_field_names(val)
+ elif option == "none_format":
+ self._validate_none_format(val)
+ elif option in (
+ "start",
+ "end",
+ "max_width",
+ "min_width",
+ "min_table_width",
+ "max_table_width",
+ "padding_width",
+ "left_padding_width",
+ "right_padding_width",
+ "format",
+ ):
+ self._validate_nonnegative_int(option, val)
+ elif option == "sortby":
+ self._validate_field_name(option, val)
+ elif option == "sort_key":
+ self._validate_function(option, val)
+ elif option == "hrules":
+ self._validate_hrules(option, val)
+ elif option == "vrules":
+ self._validate_vrules(option, val)
+ elif option == "fields":
+ self._validate_all_field_names(option, val)
+ elif option in (
+ "header",
+ "border",
+ "preserve_internal_border",
+ "reversesort",
+ "xhtml",
+ "print_empty",
+ "oldsortslice",
+ ):
+ self._validate_true_or_false(option, val)
+ elif option == "header_style":
+ self._validate_header_style(val)
+ elif option == "int_format":
+ self._validate_int_format(option, val)
+ elif option == "float_format":
+ self._validate_float_format(option, val)
+ elif option == "custom_format":
+ for k, formatter in val.items():
+ self._validate_function(f"{option}.{k}", formatter)
+ elif option in (
+ "vertical_char",
+ "horizontal_char",
+ "horizontal_align_char",
+ "junction_char",
+ "top_junction_char",
+ "bottom_junction_char",
+ "right_junction_char",
+ "left_junction_char",
+ "top_right_junction_char",
+ "top_left_junction_char",
+ "bottom_right_junction_char",
+ "bottom_left_junction_char",
+ ):
+ self._validate_single_char(option, val)
+ elif option == "attributes":
+ self._validate_attributes(option, val)
+
+ def _validate_field_names(self, val):
+ # Check for appropriate length
+ if self._field_names:
+ try:
+ assert len(val) == len(self._field_names)
+ except AssertionError:
+ msg = (
+ "Field name list has incorrect number of values, "
+ f"(actual) {len(val)}!={len(self._field_names)} (expected)"
+ )
+ raise ValueError(msg)
+ if self._rows:
+ try:
+ assert len(val) == len(self._rows[0])
+ except AssertionError:
+ msg = (
+ "Field name list has incorrect number of values, "
+ f"(actual) {len(val)}!={len(self._rows[0])} (expected)"
+ )
+ raise ValueError(msg)
+ # Check for uniqueness
+ try:
+ assert len(val) == len(set(val))
+ except AssertionError:
+ msg = "Field names must be unique"
+ raise ValueError(msg)
+
+ def _validate_none_format(self, val):
+ try:
+ if val is not None:
+ assert isinstance(val, str)
+ except AssertionError:
+ msg = "Replacement for None value must be a string if being supplied."
+ raise TypeError(msg)
+
+ def _validate_header_style(self, val):
+ try:
+ assert val in ("cap", "title", "upper", "lower", None)
+ except AssertionError:
+ msg = "Invalid header style, use cap, title, upper, lower or None"
+ raise ValueError(msg)
+
+ def _validate_align(self, val):
+ try:
+ assert val in ["l", "c", "r"]
+ except AssertionError:
+ msg = f"Alignment {val} is invalid, use l, c or r"
+ raise ValueError(msg)
+
+ def _validate_valign(self, val):
+ try:
+ assert val in ["t", "m", "b", None]
+ except AssertionError:
+ msg = f"Alignment {val} is invalid, use t, m, b or None"
+ raise ValueError(msg)
+
+ def _validate_nonnegative_int(self, name, val):
+ try:
+ assert int(val) >= 0
+ except AssertionError:
+ msg = f"Invalid value for {name}: {val}"
+ raise ValueError(msg)
+
+ def _validate_true_or_false(self, name, val):
+ try:
+ assert val in (True, False)
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be True or False."
+ raise ValueError(msg)
+
+ def _validate_int_format(self, name, val):
+ if val == "":
+ return
+ try:
+ assert isinstance(val, str)
+ assert val.isdigit()
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be an integer format string."
+ raise ValueError(msg)
+
+ def _validate_float_format(self, name, val):
+ if val == "":
+ return
+ try:
+ assert isinstance(val, str)
+ assert "." in val
+ bits = val.split(".")
+ assert len(bits) <= 2
+ assert bits[0] == "" or bits[0].isdigit()
+ assert (
+ bits[1] == ""
+ or bits[1].isdigit()
+ or (bits[1][-1] == "f" and bits[1].rstrip("f").isdigit())
+ )
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be a float format string."
+ raise ValueError(msg)
+
+ def _validate_function(self, name, val):
+ try:
+ assert hasattr(val, "__call__")
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be a function."
+ raise ValueError(msg)
+
+ def _validate_hrules(self, name, val):
+ try:
+ assert val in (ALL, FRAME, HEADER, NONE)
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be ALL, FRAME, HEADER or NONE."
+ raise ValueError(msg)
+
+ def _validate_vrules(self, name, val):
+ try:
+ assert val in (ALL, FRAME, NONE)
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be ALL, FRAME, or NONE."
+ raise ValueError(msg)
+
+ def _validate_field_name(self, name, val):
+ try:
+ assert (val in self._field_names) or (val is None)
+ except AssertionError:
+ msg = f"Invalid field name: {val}"
+ raise ValueError(msg)
+
+ def _validate_all_field_names(self, name, val):
+ try:
+ for x in val:
+ self._validate_field_name(name, x)
+ except AssertionError:
+ msg = "Fields must be a sequence of field names"
+ raise ValueError(msg)
+
+ def _validate_single_char(self, name, val):
+ try:
+ assert _str_block_width(val) == 1
+ except AssertionError:
+ msg = f"Invalid value for {name}. Must be a string of length 1."
+ raise ValueError(msg)
+
+ def _validate_attributes(self, name, val):
+ try:
+ assert isinstance(val, dict)
+ except AssertionError:
+ msg = "Attributes must be a dictionary of name/value pairs"
+ raise TypeError(msg)
+
+ ##############################
+ # ATTRIBUTE MANAGEMENT #
+ ##############################
+ @property
+ def rows(self) -> list[Any]:
+ return self._rows[:]
+
+ @property
+ def dividers(self) -> list[bool]:
+ return self._dividers[:]
+
+ @property
+ def xhtml(self) -> bool:
+ """Print <br/> tags if True, <br> tags if False"""
+ return self._xhtml
+
+ @xhtml.setter
+ def xhtml(self, val) -> None:
+ self._validate_option("xhtml", val)
+ self._xhtml = val
+
+ @property
+ def none_format(self):
+ return self._none_format
+
+ @none_format.setter
+ def none_format(self, val):
+ if not self._field_names:
+ self._none_format = {}
+ elif val is None or (isinstance(val, dict) and len(val) == 0):
+ for field in self._field_names:
+ self._none_format[field] = None
+ else:
+ self._validate_none_format(val)
+ for field in self._field_names:
+ self._none_format[field] = val
+
+ @property
+ def field_names(self):
+ """List or tuple of field names
+
+ When setting field_names, if there are already field names the new list
+ of field names must be the same length. Columns are renamed and row data
+ remains unchanged."""
+ return self._field_names
+
+ @field_names.setter
+ def field_names(self, val) -> None:
+ val = [str(x) for x in val]
+ self._validate_option("field_names", val)
+ old_names = None
+ if self._field_names:
+ old_names = self._field_names[:]
+ self._field_names = val
+ if self._align and old_names:
+ for old_name, new_name in zip(old_names, val):
+ self._align[new_name] = self._align[old_name]
+ for old_name in old_names:
+ if old_name not in self._align:
+ self._align.pop(old_name)
+ elif self._align:
+ for field_name in self._field_names:
+ self._align[field_name] = self._align[BASE_ALIGN_VALUE]
+ else:
+ self.align = "c"
+ if self._valign and old_names:
+ for old_name, new_name in zip(old_names, val):
+ self._valign[new_name] = self._valign[old_name]
+ for old_name in old_names:
+ if old_name not in self._valign:
+ self._valign.pop(old_name)
+ else:
+ self.valign = "t"
+
+ @property
+ def align(self):
+ """Controls alignment of fields
+ Arguments:
+
+ align - alignment, one of "l", "c", or "r" """
+ return self._align
+
+ @align.setter
+ def align(self, val) -> None:
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ if not self._field_names:
+ self._align = {BASE_ALIGN_VALUE: "c"}
+ else:
+ for field in self._field_names:
+ self._align[field] = "c"
+ else:
+ self._validate_align(val)
+ if not self._field_names:
+ self._align = {BASE_ALIGN_VALUE: val}
+ else:
+ for field in self._field_names:
+ self._align[field] = val
+
+ @property
+ def valign(self):
+ """Controls vertical alignment of fields
+ Arguments:
+
+ valign - vertical alignment, one of "t", "m", or "b" """
+ return self._valign
+
+ @valign.setter
+ def valign(self, val) -> None:
+ if not self._field_names:
+ self._valign = {}
+ elif val is None or (isinstance(val, dict) and len(val) == 0):
+ for field in self._field_names:
+ self._valign[field] = "t"
+ else:
+ self._validate_valign(val)
+ for field in self._field_names:
+ self._valign[field] = val
+
+ @property
+ def max_width(self):
+ """Controls maximum width of fields
+ Arguments:
+
+ max_width - maximum width integer"""
+ return self._max_width
+
+ @max_width.setter
+ def max_width(self, val) -> None:
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._max_width = {}
+ else:
+ self._validate_option("max_width", val)
+ for field in self._field_names:
+ self._max_width[field] = val
+
+ @property
+ def min_width(self):
+ """Controls minimum width of fields
+ Arguments:
+
+ min_width - minimum width integer"""
+ return self._min_width
+
+ @min_width.setter
+ def min_width(self, val) -> None:
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._min_width = {}
+ else:
+ self._validate_option("min_width", val)
+ for field in self._field_names:
+ self._min_width[field] = val
+
+ @property
+ def min_table_width(self):
+ return self._min_table_width
+
+ @min_table_width.setter
+ def min_table_width(self, val) -> None:
+ self._validate_option("min_table_width", val)
+ self._min_table_width = val
+
+ @property
+ def max_table_width(self):
+ return self._max_table_width
+
+ @max_table_width.setter
+ def max_table_width(self, val) -> None:
+ self._validate_option("max_table_width", val)
+ self._max_table_width = val
+
+ @property
+ def fields(self):
+ """List or tuple of field names to include in displays"""
+ return self._fields
+
+ @fields.setter
+ def fields(self, val) -> None:
+ self._validate_option("fields", val)
+ self._fields = val
+
+ @property
+ def title(self):
+ """Optional table title
+
+ Arguments:
+
+ title - table title"""
+ return self._title
+
+ @title.setter
+ def title(self, val) -> None:
+ self._title = str(val)
+
+ @property
+ def start(self):
+ """Start index of the range of rows to print
+
+ Arguments:
+
+ start - index of first data row to include in output"""
+ return self._start
+
+ @start.setter
+ def start(self, val) -> None:
+ self._validate_option("start", val)
+ self._start = val
+
+ @property
+ def end(self):
+ """End index of the range of rows to print
+
+ Arguments:
+
+ end - index of last data row to include in output PLUS ONE (list slice style)"""
+ return self._end
+
+ @end.setter
+ def end(self, val) -> None:
+ self._validate_option("end", val)
+ self._end = val
+
+ @property
+ def sortby(self):
+ """Name of field by which to sort rows
+
+ Arguments:
+
+ sortby - field name to sort by"""
+ return self._sortby
+
+ @sortby.setter
+ def sortby(self, val) -> None:
+ self._validate_option("sortby", val)
+ self._sortby = val
+
+ @property
+ def reversesort(self):
+ """Controls direction of sorting (ascending vs descending)
+
+ Arguments:
+
+ reveresort - set to True to sort by descending order, or False to sort by
+ ascending order"""
+ return self._reversesort
+
+ @reversesort.setter
+ def reversesort(self, val) -> None:
+ self._validate_option("reversesort", val)
+ self._reversesort = val
+
+ @property
+ def sort_key(self):
+ """Sorting key function, applied to data points before sorting
+
+ Arguments:
+
+ sort_key - a function which takes one argument and returns something to be
+ sorted"""
+ return self._sort_key
+
+ @sort_key.setter
+ def sort_key(self, val) -> None:
+ self._validate_option("sort_key", val)
+ self._sort_key = val
+
+ @property
+ def header(self):
+ """Controls printing of table header with field names
+
+ Arguments:
+
+ header - print a header showing field names (True or False)"""
+ return self._header
+
+ @header.setter
+ def header(self, val) -> None:
+ self._validate_option("header", val)
+ self._header = val
+
+ @property
+ def header_style(self):
+ """Controls stylisation applied to field names in header
+
+ Arguments:
+
+ header_style - stylisation to apply to field names in header
+ ("cap", "title", "upper", "lower" or None)"""
+ return self._header_style
+
+ @header_style.setter
+ def header_style(self, val) -> None:
+ self._validate_header_style(val)
+ self._header_style = val
+
+ @property
+ def border(self):
+ """Controls printing of border around table
+
+ Arguments:
+
+ border - print a border around the table (True or False)"""
+ return self._border
+
+ @border.setter
+ def border(self, val) -> None:
+ self._validate_option("border", val)
+ self._border = val
+
+ @property
+ def preserve_internal_border(self):
+ """Controls printing of border inside table
+
+ Arguments:
+
+ preserve_internal_border - print a border inside the table even if
+ border is disabled (True or False)"""
+ return self._preserve_internal_border
+
+ @preserve_internal_border.setter
+ def preserve_internal_border(self, val) -> None:
+ self._validate_option("preserve_internal_border", val)
+ self._preserve_internal_border = val
+
+ @property
+ def hrules(self):
+ """Controls printing of horizontal rules after rows
+
+ Arguments:
+
+ hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE"""
+ return self._hrules
+
+ @hrules.setter
+ def hrules(self, val) -> None:
+ self._validate_option("hrules", val)
+ self._hrules = val
+
+ @property
+ def vrules(self):
+ """Controls printing of vertical rules between columns
+
+ Arguments:
+
+ vrules - vertical rules style. Allowed values: FRAME, ALL, NONE"""
+ return self._vrules
+
+ @vrules.setter
+ def vrules(self, val) -> None:
+ self._validate_option("vrules", val)
+ self._vrules = val
+
+ @property
+ def int_format(self):
+ """Controls formatting of integer data
+ Arguments:
+
+ int_format - integer format string"""
+ return self._int_format
+
+ @int_format.setter
+ def int_format(self, val) -> None:
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._int_format = {}
+ else:
+ self._validate_option("int_format", val)
+ for field in self._field_names:
+ self._int_format[field] = val
+
+ @property
+ def float_format(self):
+ """Controls formatting of floating point data
+ Arguments:
+
+ float_format - floating point format string"""
+ return self._float_format
+
+ @float_format.setter
+ def float_format(self, val) -> None:
+ if val is None or (isinstance(val, dict) and len(val) == 0):
+ self._float_format = {}
+ else:
+ self._validate_option("float_format", val)
+ for field in self._field_names:
+ self._float_format[field] = val
+
+ @property
+ def custom_format(self):
+ """Controls formatting of any column using callable
+ Arguments:
+
+ custom_format - Dictionary of field_name and callable"""
+ return self._custom_format
+
+ @custom_format.setter
+ def custom_format(self, val):
+ if val is None:
+ self._custom_format = {}
+ elif isinstance(val, dict):
+ for k, v in val.items():
+ self._validate_function(f"custom_value.{k}", v)
+ self._custom_format = val
+ elif hasattr(val, "__call__"):
+ self._validate_function("custom_value", val)
+ for field in self._field_names:
+ self._custom_format[field] = val
+ else:
+ msg = "The custom_format property need to be a dictionary or callable"
+ raise TypeError(msg)
+
+ @property
+ def padding_width(self):
+ """The number of empty spaces between a column's edge and its content
+
+ Arguments:
+
+ padding_width - number of spaces, must be a positive integer"""
+ return self._padding_width
+
+ @padding_width.setter
+ def padding_width(self, val) -> None:
+ self._validate_option("padding_width", val)
+ self._padding_width = val
+
+ @property
+ def left_padding_width(self):
+ """The number of empty spaces between a column's left edge and its content
+
+ Arguments:
+
+ left_padding - number of spaces, must be a positive integer"""
+ return self._left_padding_width
+
+ @left_padding_width.setter
+ def left_padding_width(self, val) -> None:
+ self._validate_option("left_padding_width", val)
+ self._left_padding_width = val
+
+ @property
+ def right_padding_width(self):
+ """The number of empty spaces between a column's right edge and its content
+
+ Arguments:
+
+ right_padding - number of spaces, must be a positive integer"""
+ return self._right_padding_width
+
+ @right_padding_width.setter
+ def right_padding_width(self, val) -> None:
+ self._validate_option("right_padding_width", val)
+ self._right_padding_width = val
+
+ @property
+ def vertical_char(self):
+ """The character used when printing table borders to draw vertical lines
+
+ Arguments:
+
+ vertical_char - single character string used to draw vertical lines"""
+ return self._vertical_char
+
+ @vertical_char.setter
+ def vertical_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("vertical_char", val)
+ self._vertical_char = val
+
+ @property
+ def horizontal_char(self):
+ """The character used when printing table borders to draw horizontal lines
+
+ Arguments:
+
+ horizontal_char - single character string used to draw horizontal lines"""
+ return self._horizontal_char
+
+ @horizontal_char.setter
+ def horizontal_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("horizontal_char", val)
+ self._horizontal_char = val
+
+ @property
+ def horizontal_align_char(self):
+ """The character used to indicate column alignment in horizontal lines
+
+ Arguments:
+
+ horizontal_align_char - single character string used to indicate alignment"""
+ return self._bottom_left_junction_char or self.junction_char
+
+ @horizontal_align_char.setter
+ def horizontal_align_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("horizontal_align_char", val)
+ self._horizontal_align_char = val
+
+ @property
+ def junction_char(self):
+ """The character used when printing table borders to draw line junctions
+
+ Arguments:
+
+ junction_char - single character string used to draw line junctions"""
+ return self._junction_char
+
+ @junction_char.setter
+ def junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("junction_char", val)
+ self._junction_char = val
+
+ @property
+ def top_junction_char(self):
+ """The character used when printing table borders to draw top line junctions
+
+ Arguments:
+
+ top_junction_char - single character string used to draw top line junctions"""
+ return self._top_junction_char or self.junction_char
+
+ @top_junction_char.setter
+ def top_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("top_junction_char", val)
+ self._top_junction_char = val
+
+ @property
+ def bottom_junction_char(self):
+ """The character used when printing table borders to draw bottom line junctions
+
+ Arguments:
+
+ bottom_junction_char -
+ single character string used to draw bottom line junctions"""
+ return self._bottom_junction_char or self.junction_char
+
+ @bottom_junction_char.setter
+ def bottom_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("bottom_junction_char", val)
+ self._bottom_junction_char = val
+
+ @property
+ def right_junction_char(self):
+ """The character used when printing table borders to draw right line junctions
+
+ Arguments:
+
+ right_junction_char -
+ single character string used to draw right line junctions"""
+ return self._right_junction_char or self.junction_char
+
+ @right_junction_char.setter
+ def right_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("right_junction_char", val)
+ self._right_junction_char = val
+
+ @property
+ def left_junction_char(self):
+ """The character used when printing table borders to draw left line junctions
+
+ Arguments:
+
+ left_junction_char - single character string used to draw left line junctions"""
+ return self._left_junction_char or self.junction_char
+
+ @left_junction_char.setter
+ def left_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("left_junction_char", val)
+ self._left_junction_char = val
+
+ @property
+ def top_right_junction_char(self):
+ """
+ The character used when printing table borders to draw top-right line junctions
+
+ Arguments:
+
+ top_right_junction_char -
+ single character string used to draw top-right line junctions"""
+ return self._top_right_junction_char or self.junction_char
+
+ @top_right_junction_char.setter
+ def top_right_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("top_right_junction_char", val)
+ self._top_right_junction_char = val
+
+ @property
+ def top_left_junction_char(self):
+ """
+ The character used when printing table borders to draw top-left line junctions
+
+ Arguments:
+
+ top_left_junction_char -
+ single character string used to draw top-left line junctions"""
+ return self._top_left_junction_char or self.junction_char
+
+ @top_left_junction_char.setter
+ def top_left_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("top_left_junction_char", val)
+ self._top_left_junction_char = val
+
+ @property
+ def bottom_right_junction_char(self):
+ """The character used when printing table borders
+ to draw bottom-right line junctions
+
+ Arguments:
+
+ bottom_right_junction_char -
+ single character string used to draw bottom-right line junctions"""
+ return self._bottom_right_junction_char or self.junction_char
+
+ @bottom_right_junction_char.setter
+ def bottom_right_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("bottom_right_junction_char", val)
+ self._bottom_right_junction_char = val
+
+ @property
+ def bottom_left_junction_char(self):
+ """The character used when printing table borders
+ to draw bottom-left line junctions
+
+ Arguments:
+
+ bottom_left_junction_char -
+ single character string used to draw bottom-left line junctions"""
+ return self._bottom_left_junction_char or self.junction_char
+
+ @bottom_left_junction_char.setter
+ def bottom_left_junction_char(self, val) -> None:
+ val = str(val)
+ self._validate_option("bottom_left_junction_char", val)
+ self._bottom_left_junction_char = val
+
+ @property
+ def format(self):
+ """Controls whether or not HTML tables are formatted to match styling options
+
+ Arguments:
+
+ format - True or False"""
+ return self._format
+
+ @format.setter
+ def format(self, val) -> None:
+ self._validate_option("format", val)
+ self._format = val
+
+ @property
+ def print_empty(self):
+ """Controls whether or not empty tables produce a header and frame or just an
+ empty string
+
+ Arguments:
+
+ print_empty - True or False"""
+ return self._print_empty
+
+ @print_empty.setter
+ def print_empty(self, val) -> None:
+ self._validate_option("print_empty", val)
+ self._print_empty = val
+
+ @property
+ def attributes(self):
+ """A dictionary of HTML attribute name/value pairs to be included in the
+ <table> tag when printing HTML
+
+ Arguments:
+
+ attributes - dictionary of attributes"""
+ return self._attributes
+
+ @attributes.setter
+ def attributes(self, val) -> None:
+ self._validate_option("attributes", val)
+ self._attributes = val
+
+ @property
+ def oldsortslice(self):
+ """oldsortslice - Slice rows before sorting in the "old style" """
+ return self._oldsortslice
+
+ @oldsortslice.setter
+ def oldsortslice(self, val) -> None:
+ self._validate_option("oldsortslice", val)
+ self._oldsortslice = val
+
+ ##############################
+ # OPTION MIXER #
+ ##############################
+
+ def _get_options(self, kwargs):
+ options = {}
+ for option in self._options:
+ if option in kwargs:
+ self._validate_option(option, kwargs[option])
+ options[option] = kwargs[option]
+ else:
+ options[option] = getattr(self, option)
+ return options
+
+ ##############################
+ # PRESET STYLE LOGIC #
+ ##############################
+
+ def set_style(self, style) -> None:
+ if style == DEFAULT:
+ self._set_default_style()
+ elif style == MSWORD_FRIENDLY:
+ self._set_msword_style()
+ elif style == PLAIN_COLUMNS:
+ self._set_columns_style()
+ elif style == MARKDOWN:
+ self._set_markdown_style()
+ elif style == ORGMODE:
+ self._set_orgmode_style()
+ elif style == DOUBLE_BORDER:
+ self._set_double_border_style()
+ elif style == SINGLE_BORDER:
+ self._set_single_border_style()
+ elif style == RANDOM:
+ self._set_random_style()
+ else:
+ msg = "Invalid pre-set style"
+ raise ValueError(msg)
+
+ def _set_orgmode_style(self) -> None:
+ self._set_default_style()
+ self.orgmode = True
+
+ def _set_markdown_style(self) -> None:
+ self.header = True
+ self.border = True
+ self._hrules = None
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+ self.junction_char = "|"
+ self._horizontal_align_char = ":"
+
+ def _set_default_style(self) -> None:
+ self.header = True
+ self.border = True
+ self._hrules = FRAME
+ self._vrules = ALL
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+ self.horizontal_char = "-"
+ self._horizontal_align_char = None
+ self.junction_char = "+"
+ self._top_junction_char = None
+ self._bottom_junction_char = None
+ self._right_junction_char = None
+ self._left_junction_char = None
+ self._top_right_junction_char = None
+ self._top_left_junction_char = None
+ self._bottom_right_junction_char = None
+ self._bottom_left_junction_char = None
+
+ def _set_msword_style(self) -> None:
+ self.header = True
+ self.border = True
+ self._hrules = NONE
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+
+ def _set_columns_style(self) -> None:
+ self.header = True
+ self.border = False
+ self.padding_width = 1
+ self.left_padding_width = 0
+ self.right_padding_width = 8
+
+ def _set_double_border_style(self) -> None:
+ self.horizontal_char = "═"
+ self.vertical_char = "║"
+ self.junction_char = "╬"
+ self.top_junction_char = "╦"
+ self.bottom_junction_char = "╩"
+ self.right_junction_char = "╣"
+ self.left_junction_char = "╠"
+ self.top_right_junction_char = "╗"
+ self.top_left_junction_char = "╔"
+ self.bottom_right_junction_char = "╝"
+ self.bottom_left_junction_char = "╚"
+
+ def _set_single_border_style(self) -> None:
+ self.horizontal_char = "─"
+ self.vertical_char = "│"
+ self.junction_char = "┼"
+ self.top_junction_char = "┬"
+ self.bottom_junction_char = "┴"
+ self.right_junction_char = "┤"
+ self.left_junction_char = "├"
+ self.top_right_junction_char = "┐"
+ self.top_left_junction_char = "┌"
+ self.bottom_right_junction_char = "┘"
+ self.bottom_left_junction_char = "└"
+
+ def _set_random_style(self) -> None:
+ # Just for fun!
+ import random
+
+ self.header = random.choice((True, False))
+ self.border = random.choice((True, False))
+ self._hrules = random.choice((ALL, FRAME, HEADER, NONE))
+ self._vrules = random.choice((ALL, FRAME, NONE))
+ self.left_padding_width = random.randint(0, 5)
+ self.right_padding_width = random.randint(0, 5)
+ self.vertical_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
+ self.horizontal_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
+ self.junction_char = random.choice(r"~!@#$%^&*()_+|-=\{}[];':\",./;<>?")
+ self.preserve_internal_border = random.choice((True, False))
+
+ ##############################
+ # DATA INPUT METHODS #
+ ##############################
+
+ def add_rows(self, rows) -> None:
+ """Add rows to the table
+
+ Arguments:
+
+ rows - rows of data, should be an iterable of lists, each list with as many
+ elements as the table has fields"""
+ for row in rows:
+ self.add_row(row)
+
+ def add_row(self, row, *, divider: bool = False) -> None:
+ """Add a row to the table
+
+ Arguments:
+
+ row - row of data, should be a list with as many elements as the table
+ has fields"""
+
+ if self._field_names and len(row) != len(self._field_names):
+ msg = (
+ "Row has incorrect number of values, "
+ f"(actual) {len(row)}!={len(self._field_names)} (expected)"
+ )
+ raise ValueError(msg)
+ if not self._field_names:
+ self.field_names = [f"Field {n + 1}" for n in range(0, len(row))]
+ self._rows.append(list(row))
+ self._dividers.append(divider)
+
+ def del_row(self, row_index) -> None:
+ """Delete a row from the table
+
+ Arguments:
+
+ row_index - The index of the row you want to delete. Indexing starts at 0."""
+
+ if row_index > len(self._rows) - 1:
+ msg = (
+ f"Can't delete row at index {row_index}, "
+ f"table only has {len(self._rows)} rows"
+ )
+ raise IndexError(msg)
+ del self._rows[row_index]
+ del self._dividers[row_index]
+
+ def add_column(
+ self, fieldname, column, align: str = "c", valign: str = "t"
+ ) -> None:
+ """Add a column to the table.
+
+ Arguments:
+
+ fieldname - name of the field to contain the new column of data
+ column - column of data, should be a list with as many elements as the
+ table has rows
+ align - desired alignment for this column - "l" for left, "c" for centre and
+ "r" for right
+ valign - desired vertical alignment for new columns - "t" for top,
+ "m" for middle and "b" for bottom"""
+
+ if len(self._rows) in (0, len(column)):
+ self._validate_align(align)
+ self._validate_valign(valign)
+ self._field_names.append(fieldname)
+ self._align[fieldname] = align
+ self._valign[fieldname] = valign
+ for i in range(0, len(column)):
+ if len(self._rows) < i + 1:
+ self._rows.append([])
+ self._dividers.append(False)
+ self._rows[i].append(column[i])
+ else:
+ msg = (
+ f"Column length {len(column)} does not match number of rows "
+ f"{len(self._rows)}"
+ )
+ raise ValueError(msg)
+
+ def add_autoindex(self, fieldname: str = "Index") -> None:
+ """Add an auto-incrementing index column to the table.
+ Arguments:
+ fieldname - name of the field to contain the new column of data"""
+ self._field_names.insert(0, fieldname)
+ self._align[fieldname] = self.align
+ self._valign[fieldname] = self.valign
+ for i, row in enumerate(self._rows):
+ row.insert(0, i + 1)
+
+ def del_column(self, fieldname) -> None:
+ """Delete a column from the table
+
+ Arguments:
+
+ fieldname - The field name of the column you want to delete."""
+
+ if fieldname not in self._field_names:
+ msg = (
+ "Can't delete column {!r} which is not a field name of this table."
+ " Field names are: {}".format(
+ fieldname, ", ".join(map(repr, self._field_names))
+ )
+ )
+ raise ValueError(msg)
+
+ col_index = self._field_names.index(fieldname)
+ del self._field_names[col_index]
+ for row in self._rows:
+ del row[col_index]
+
+ def clear_rows(self) -> None:
+ """Delete all rows from the table but keep the current field names"""
+
+ self._rows = []
+ self._dividers = []
+
+ def clear(self) -> None:
+ """Delete all rows and field names from the table, maintaining nothing but
+ styling options"""
+
+ self._rows = []
+ self._dividers = []
+ self._field_names = []
+ self._widths = []
+
+ ##############################
+ # MISC PUBLIC METHODS #
+ ##############################
+
+ def copy(self):
+ import copy
+
+ return copy.deepcopy(self)
+
+ def get_formatted_string(self, out_format: str = "text", **kwargs) -> str:
+ """Return string representation of specified format of table in current state.
+
+ Arguments:
+ out_format - resulting table format
+ kwargs - passed through to function that performs formatting
+ """
+ if out_format == "text":
+ return self.get_string(**kwargs)
+ if out_format == "html":
+ return self.get_html_string(**kwargs)
+ if out_format == "json":
+ return self.get_json_string(**kwargs)
+ if out_format == "csv":
+ return self.get_csv_string(**kwargs)
+ if out_format == "latex":
+ return self.get_latex_string(**kwargs)
+
+ msg = (
+ f"Invalid format {out_format}. "
+ "Must be one of: text, html, json, csv, or latex"
+ )
+ raise ValueError(msg)
+
+ ##############################
+ # MISC PRIVATE METHODS #
+ ##############################
+
+ def _format_value(self, field, value):
+ if isinstance(value, int) and field in self._int_format:
+ return ("%%%sd" % self._int_format[field]) % value
+ elif isinstance(value, float) and field in self._float_format:
+ return ("%%%sf" % self._float_format[field]) % value
+
+ formatter = self._custom_format.get(field, (lambda f, v: str(v)))
+ return formatter(field, value)
+
+ def _compute_table_width(self, options):
+ table_width = 2 if options["vrules"] in (FRAME, ALL) else 0
+ per_col_padding = sum(self._get_padding_widths(options))
+ for index, fieldname in enumerate(self.field_names):
+ if not options["fields"] or (
+ options["fields"] and fieldname in options["fields"]
+ ):
+ table_width += self._widths[index] + per_col_padding
+ return table_width
+
+ def _compute_widths(self, rows, options) -> None:
+ if options["header"]:
+ widths = [_get_size(field)[0] for field in self._field_names]
+ else:
+ widths = len(self.field_names) * [0]
+
+ for row in rows:
+ for index, value in enumerate(row):
+ fieldname = self.field_names[index]
+ if self.none_format.get(fieldname) is not None:
+ if value == "None" or value is None:
+ value = self.none_format.get(fieldname)
+ if fieldname in self.max_width:
+ widths[index] = max(
+ widths[index],
+ min(_get_size(value)[0], self.max_width[fieldname]),
+ )
+ else:
+ widths[index] = max(widths[index], _get_size(value)[0])
+ if fieldname in self.min_width:
+ widths[index] = max(widths[index], self.min_width[fieldname])
+ self._widths = widths
+
+ # Are we exceeding max_table_width?
+ if self._max_table_width:
+ table_width = self._compute_table_width(options)
+ if table_width > self._max_table_width:
+ # Shrink widths in proportion
+ scale = 1.0 * self._max_table_width / table_width
+ widths = [int(w * scale) for w in widths]
+ self._widths = widths
+
+ # Are we under min_table_width or title width?
+ if self._min_table_width or options["title"]:
+ if options["title"]:
+ title_width = len(options["title"]) + sum(
+ self._get_padding_widths(options)
+ )
+ if options["vrules"] in (FRAME, ALL):
+ title_width += 2
+ else:
+ title_width = 0
+ min_table_width = self.min_table_width or 0
+ min_width = max(title_width, min_table_width)
+ if options["border"]:
+ borders = len(widths) + 1
+ elif options["preserve_internal_border"]:
+ borders = len(widths)
+ else:
+ borders = 0
+
+ # Subtract padding for each column and borders
+ min_width -= (
+ sum([sum(self._get_padding_widths(options)) for _ in widths]) + borders
+ )
+ # What is being scaled is content so we sum column widths
+ content_width = sum(widths) or 1
+
+ if content_width < min_width:
+ # Grow widths in proportion
+ scale = 1.0 * min_width / content_width
+ widths = [int(w * scale) for w in widths]
+ if sum(widths) < min_width:
+ widths[-1] += min_width - sum(widths)
+ self._widths = widths
+
+ def _get_padding_widths(self, options):
+ if options["left_padding_width"] is not None:
+ lpad = options["left_padding_width"]
+ else:
+ lpad = options["padding_width"]
+ if options["right_padding_width"] is not None:
+ rpad = options["right_padding_width"]
+ else:
+ rpad = options["padding_width"]
+ return lpad, rpad
+
+ def _get_rows(self, options):
+ """Return only those data rows that should be printed, based on slicing and
+ sorting.
+
+ Arguments:
+
+ options - dictionary of option settings."""
+ import copy
+
+ if options["oldsortslice"]:
+ rows = copy.deepcopy(self._rows[options["start"] : options["end"]])
+ else:
+ rows = copy.deepcopy(self._rows)
+
+ # Sort
+ if options["sortby"]:
+ sortindex = self._field_names.index(options["sortby"])
+ # Decorate
+ rows = [[row[sortindex]] + row for row in rows]
+ # Sort
+ rows.sort(reverse=options["reversesort"], key=options["sort_key"])
+ # Undecorate
+ rows = [row[1:] for row in rows]
+
+ # Slice if necessary
+ if not options["oldsortslice"]:
+ rows = rows[options["start"] : options["end"]]
+
+ return rows
+
+ def _get_dividers(self, options):
+ """Return only those dividers that should be printed, based on slicing.
+
+ Arguments:
+
+ options - dictionary of option settings."""
+ import copy
+
+ if options["oldsortslice"]:
+ dividers = copy.deepcopy(self._dividers[options["start"] : options["end"]])
+ else:
+ dividers = copy.deepcopy(self._dividers)
+
+ if options["sortby"]:
+ dividers = [False for divider in dividers]
+
+ return dividers
+
+ def _format_row(self, row):
+ return [
+ self._format_value(field, value)
+ for (field, value) in zip(self._field_names, row)
+ ]
+
+ def _format_rows(self, rows):
+ return [self._format_row(row) for row in rows]
+
+ ##############################
+ # PLAIN TEXT STRING METHODS #
+ ##############################
+
+ def get_string(self, **kwargs) -> str:
+ """Return string representation of table in current state.
+
+ Arguments:
+
+ title - optional table title
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ fields - names of fields (columns) to include
+ header - print a header showing field names (True or False)
+ border - print a border around the table (True or False)
+ preserve_internal_border - print a border inside the table even if
+ border is disabled (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: ALL, FRAME, HEADER, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ custom_format - controls formatting of any column using callable
+ padding_width - number of spaces on either side of column data (only used if
+ left and right paddings are None)
+ left_padding_width - number of spaces on left hand side of column data
+ right_padding_width - number of spaces on right hand side of column data
+ vertical_char - single character string used to draw vertical lines
+ horizontal_char - single character string used to draw horizontal lines
+ horizontal_align_char - single character string used to indicate alignment
+ junction_char - single character string used to draw line junctions
+ junction_char - single character string used to draw line junctions
+ top_junction_char - single character string used to draw top line junctions
+ bottom_junction_char -
+ single character string used to draw bottom line junctions
+ right_junction_char - single character string used to draw right line junctions
+ left_junction_char - single character string used to draw left line junctions
+ top_right_junction_char -
+ single character string used to draw top-right line junctions
+ top_left_junction_char -
+ single character string used to draw top-left line junctions
+ bottom_right_junction_char -
+ single character string used to draw bottom-right line junctions
+ bottom_left_junction_char -
+ single character string used to draw bottom-left line junctions
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ reversesort - True or False to sort in descending or ascending order
+ print empty - if True, stringify just the header for an empty table,
+ if False return an empty string"""
+
+ options = self._get_options(kwargs)
+
+ lines = []
+
+ # Don't think too hard about an empty table
+ # Is this the desired behaviour? Maybe we should still print the header?
+ if self.rowcount == 0 and (not options["print_empty"] or not options["border"]):
+ return ""
+
+ # Get the rows we need to print, taking into account slicing, sorting, etc.
+ rows = self._get_rows(options)
+ dividers = self._get_dividers(options)
+
+ # Turn all data in all rows into Unicode, formatted as desired
+ formatted_rows = self._format_rows(rows)
+
+ # Compute column widths
+ self._compute_widths(formatted_rows, options)
+ self._hrule = self._stringify_hrule(options)
+
+ # Add title
+ title = options["title"] or self._title
+ if title:
+ lines.append(self._stringify_title(title, options))
+
+ # Add header or top of border
+ if options["header"]:
+ lines.append(self._stringify_header(options))
+ elif options["border"] and options["hrules"] in (ALL, FRAME):
+ lines.append(self._stringify_hrule(options, where="top_"))
+ if title and options["vrules"] in (ALL, FRAME):
+ lines[-1] = (
+ self.left_junction_char + lines[-1][1:-1] + self.right_junction_char
+ )
+
+ # Add rows
+ for row, divider in zip(formatted_rows[:-1], dividers[:-1]):
+ lines.append(self._stringify_row(row, options, self._hrule))
+ if divider:
+ lines.append(self._stringify_hrule(options, where="bottom_"))
+ if formatted_rows:
+ lines.append(
+ self._stringify_row(
+ formatted_rows[-1],
+ options,
+ self._stringify_hrule(options, where="bottom_"),
+ )
+ )
+
+ # Add bottom of border
+ if options["border"] and options["hrules"] == FRAME:
+ lines.append(self._stringify_hrule(options, where="bottom_"))
+
+ if "orgmode" in self.__dict__ and self.orgmode:
+ lines = [
+ "|" + new_line[1:-1] + "|"
+ for old_line in lines
+ for new_line in old_line.split("\n")
+ ]
+
+ return "\n".join(lines)
+
+ def _stringify_hrule(self, options, where: str = ""):
+ if not options["border"] and not options["preserve_internal_border"]:
+ return ""
+ lpad, rpad = self._get_padding_widths(options)
+ if options["vrules"] in (ALL, FRAME):
+ bits = [options[where + "left_junction_char"]]
+ else:
+ bits = [options["horizontal_char"]]
+ # For tables with no data or fieldnames
+ if not self._field_names:
+ bits.append(options[where + "right_junction_char"])
+ return "".join(bits)
+ for field, width in zip(self._field_names, self._widths):
+ if options["fields"] and field not in options["fields"]:
+ continue
+
+ line = (width + lpad + rpad) * options["horizontal_char"]
+
+ # If necessary, add column alignment characters (e.g. ":" for Markdown)
+ if self._horizontal_align_char:
+ if self._align[field] in ("l", "c"):
+ line = " " + self._horizontal_align_char + line[2:]
+ if self._align[field] in ("c", "r"):
+ line = line[:-2] + self._horizontal_align_char + " "
+
+ bits.append(line)
+ if options["vrules"] == ALL:
+ bits.append(options[where + "junction_char"])
+ else:
+ bits.append(options["horizontal_char"])
+ if options["vrules"] in (ALL, FRAME):
+ bits.pop()
+ bits.append(options[where + "right_junction_char"])
+
+ if options["preserve_internal_border"] and not options["border"]:
+ bits = bits[1:-1]
+
+ return "".join(bits)
+
+ def _stringify_title(self, title, options):
+ lines = []
+ lpad, rpad = self._get_padding_widths(options)
+ if options["border"]:
+ if options["vrules"] == ALL:
+ options["vrules"] = FRAME
+ lines.append(self._stringify_hrule(options, "top_"))
+ options["vrules"] = ALL
+ elif options["vrules"] == FRAME:
+ lines.append(self._stringify_hrule(options, "top_"))
+ bits = []
+ endpoint = (
+ options["vertical_char"]
+ if options["vrules"] in (ALL, FRAME) and options["border"]
+ else " "
+ )
+ bits.append(endpoint)
+ title = " " * lpad + title + " " * rpad
+ bits.append(self._justify(title, len(self._hrule) - 2, "c"))
+ bits.append(endpoint)
+ lines.append("".join(bits))
+ return "\n".join(lines)
+
+ def _stringify_header(self, options):
+ bits = []
+ lpad, rpad = self._get_padding_widths(options)
+ if options["border"]:
+ if options["hrules"] in (ALL, FRAME):
+ bits.append(self._stringify_hrule(options, "top_"))
+ if options["title"] and options["vrules"] in (ALL, FRAME):
+ bits[-1] = (
+ self.left_junction_char
+ + bits[-1][1:-1]
+ + self.right_junction_char
+ )
+ bits.append("\n")
+ if options["vrules"] in (ALL, FRAME):
+ bits.append(options["vertical_char"])
+ else:
+ bits.append(" ")
+ # For tables with no data or field names
+ if not self._field_names:
+ if options["vrules"] in (ALL, FRAME):
+ bits.append(options["vertical_char"])
+ else:
+ bits.append(" ")
+ for field, width in zip(self._field_names, self._widths):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ if self._header_style == "cap":
+ fieldname = field.capitalize()
+ elif self._header_style == "title":
+ fieldname = field.title()
+ elif self._header_style == "upper":
+ fieldname = field.upper()
+ elif self._header_style == "lower":
+ fieldname = field.lower()
+ else:
+ fieldname = field
+ if _str_block_width(fieldname) > width:
+ fieldname = fieldname[:width]
+ bits.append(
+ " " * lpad
+ + self._justify(fieldname, width, self._align[field])
+ + " " * rpad
+ )
+ if options["border"] or options["preserve_internal_border"]:
+ if options["vrules"] == ALL:
+ bits.append(options["vertical_char"])
+ else:
+ bits.append(" ")
+
+ # If only preserve_internal_border is true, then we just appended
+ # a vertical character at the end when we wanted a space
+ if not options["border"] and options["preserve_internal_border"]:
+ bits.pop()
+ bits.append(" ")
+ # If vrules is FRAME, then we just appended a space at the end
+ # of the last field, when we really want a vertical character
+ if options["border"] and options["vrules"] == FRAME:
+ bits.pop()
+ bits.append(options["vertical_char"])
+ if (options["border"] or options["preserve_internal_border"]) and options[
+ "hrules"
+ ] != NONE:
+ bits.append("\n")
+ bits.append(self._hrule)
+ return "".join(bits)
+
+ def _stringify_row(self, row, options, hrule):
+ import textwrap
+
+ for index, field, value, width in zip(
+ range(0, len(row)), self._field_names, row, self._widths
+ ):
+ # Enforce max widths
+ lines = value.split("\n")
+ new_lines = []
+ for line in lines:
+ if line == "None" and self.none_format.get(field) is not None:
+ line = self.none_format[field]
+ if _str_block_width(line) > width:
+ line = textwrap.fill(line, width)
+ new_lines.append(line)
+ lines = new_lines
+ value = "\n".join(lines)
+ row[index] = value
+
+ row_height = 0
+ for c in row:
+ h = _get_size(c)[1]
+ if h > row_height:
+ row_height = h
+
+ bits = []
+ lpad, rpad = self._get_padding_widths(options)
+ for y in range(0, row_height):
+ bits.append([])
+ if options["border"]:
+ if options["vrules"] in (ALL, FRAME):
+ bits[y].append(self.vertical_char)
+ else:
+ bits[y].append(" ")
+
+ for field, value, width in zip(self._field_names, row, self._widths):
+ valign = self._valign[field]
+ lines = value.split("\n")
+ d_height = row_height - len(lines)
+ if d_height:
+ if valign == "m":
+ lines = (
+ [""] * int(d_height / 2)
+ + lines
+ + [""] * (d_height - int(d_height / 2))
+ )
+ elif valign == "b":
+ lines = [""] * d_height + lines
+ else:
+ lines = lines + [""] * d_height
+
+ y = 0
+ for line in lines:
+ if options["fields"] and field not in options["fields"]:
+ continue
+
+ bits[y].append(
+ " " * lpad
+ + self._justify(line, width, self._align[field])
+ + " " * rpad
+ )
+ if options["border"] or options["preserve_internal_border"]:
+ if options["vrules"] == ALL:
+ bits[y].append(self.vertical_char)
+ else:
+ bits[y].append(" ")
+ y += 1
+
+ # If only preserve_internal_border is true, then we just appended
+ # a vertical character at the end when we wanted a space
+ if not options["border"] and options["preserve_internal_border"]:
+ bits[-1].pop()
+ bits[-1].append(" ")
+
+ # If vrules is FRAME, then we just appended a space at the end
+ # of the last field, when we really want a vertical character
+ for y in range(0, row_height):
+ if options["border"] and options["vrules"] == FRAME:
+ bits[y].pop()
+ bits[y].append(options["vertical_char"])
+
+ if options["border"] and options["hrules"] == ALL:
+ bits[row_height - 1].append("\n")
+ bits[row_height - 1].append(hrule)
+
+ for y in range(0, row_height):
+ bits[y] = "".join(bits[y])
+
+ return "\n".join(bits)
+
+ def paginate(self, page_length: int = 58, line_break: str = "\f", **kwargs):
+ pages = []
+ kwargs["start"] = kwargs.get("start", 0)
+ true_end = kwargs.get("end", self.rowcount)
+ while True:
+ kwargs["end"] = min(kwargs["start"] + page_length, true_end)
+ pages.append(self.get_string(**kwargs))
+ if kwargs["end"] == true_end:
+ break
+ kwargs["start"] += page_length
+ return line_break.join(pages)
+
+ ##############################
+ # CSV STRING METHODS #
+ ##############################
+ def get_csv_string(self, **kwargs) -> str:
+ """Return string representation of CSV formatted table in the current state
+
+ Keyword arguments are first interpreted as table formatting options, and
+ then any unused keyword arguments are passed to csv.writer(). For
+ example, get_csv_string(header=False, delimiter='\t') would use
+ header as a PrettyTable formatting option (skip the header row) and
+ delimiter as a csv.writer keyword argument.
+ """
+ import csv
+
+ options = self._get_options(kwargs)
+ csv_options = {
+ key: value for key, value in kwargs.items() if key not in options
+ }
+ csv_buffer = io.StringIO()
+ csv_writer = csv.writer(csv_buffer, **csv_options)
+
+ if options.get("header"):
+ csv_writer.writerow(self._field_names)
+ for row in self._get_rows(options):
+ csv_writer.writerow(row)
+
+ return csv_buffer.getvalue()
+
+ ##############################
+ # JSON STRING METHODS #
+ ##############################
+ def get_json_string(self, **kwargs) -> str:
+ """Return string representation of JSON formatted table in the current state
+
+ Keyword arguments are first interpreted as table formatting options, and
+ then any unused keyword arguments are passed to json.dumps(). For
+ example, get_json_string(header=False, indent=2) would use header as
+ a PrettyTable formatting option (skip the header row) and indent as a
+ json.dumps keyword argument.
+ """
+ import json
+
+ options = self._get_options(kwargs)
+ json_options: Any = {"indent": 4, "separators": (",", ": "), "sort_keys": True}
+ json_options.update(
+ {key: value for key, value in kwargs.items() if key not in options}
+ )
+ objects = []
+
+ if options.get("header"):
+ objects.append(self.field_names)
+ for row in self._get_rows(options):
+ objects.append(dict(zip(self._field_names, row)))
+
+ return json.dumps(objects, **json_options)
+
+ ##############################
+ # HTML STRING METHODS #
+ ##############################
+
+ def get_html_string(self, **kwargs) -> str:
+ """Return string representation of HTML formatted version of table in current
+ state.
+
+ Arguments:
+
+ title - optional table title
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ fields - names of fields (columns) to include
+ header - print a header showing field names (True or False)
+ border - print a border around the table (True or False)
+ preserve_internal_border - print a border inside the table even if
+ border is disabled (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: ALL, FRAME, HEADER, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ custom_format - controls formatting of any column using callable
+ padding_width - number of spaces on either side of column data (only used if
+ left and right paddings are None)
+ left_padding_width - number of spaces on left hand side of column data
+ right_padding_width - number of spaces on right hand side of column data
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ attributes - dictionary of name/value pairs to include as HTML attributes in the
+ <table> tag
+ format - Controls whether or not HTML tables are formatted to match
+ styling options (True or False)
+ xhtml - print <br/> tags if True, <br> tags if False"""
+
+ options = self._get_options(kwargs)
+
+ if options["format"]:
+ string = self._get_formatted_html_string(options)
+ else:
+ string = self._get_simple_html_string(options)
+
+ return string
+
+ def _get_simple_html_string(self, options):
+ from html import escape
+
+ lines = []
+ if options["xhtml"]:
+ linebreak = "<br/>"
+ else:
+ linebreak = "<br>"
+
+ open_tag = ["<table"]
+ if options["attributes"]:
+ for attr_name in options["attributes"]:
+ open_tag.append(
+ f' {escape(attr_name)}="{escape(options["attributes"][attr_name])}"'
+ )
+ open_tag.append(">")
+ lines.append("".join(open_tag))
+
+ # Title
+ title = options["title"] or self._title
+ if title:
+ lines.append(f" <caption>{escape(title)}</caption>")
+
+ # Headers
+ if options["header"]:
+ lines.append(" <thead>")
+ lines.append(" <tr>")
+ for field in self._field_names:
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ " <th>%s</th>" % escape(field).replace("\n", linebreak)
+ )
+ lines.append(" </tr>")
+ lines.append(" </thead>")
+
+ # Data
+ lines.append(" <tbody>")
+ rows = self._get_rows(options)
+ formatted_rows = self._format_rows(rows)
+ for row in formatted_rows:
+ lines.append(" <tr>")
+ for field, datum in zip(self._field_names, row):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ " <td>%s</td>" % escape(datum).replace("\n", linebreak)
+ )
+ lines.append(" </tr>")
+ lines.append(" </tbody>")
+ lines.append("</table>")
+
+ return "\n".join(lines)
+
+ def _get_formatted_html_string(self, options):
+ from html import escape
+
+ lines = []
+ lpad, rpad = self._get_padding_widths(options)
+ if options["xhtml"]:
+ linebreak = "<br/>"
+ else:
+ linebreak = "<br>"
+
+ open_tag = ["<table"]
+ if options["border"]:
+ if options["hrules"] == ALL and options["vrules"] == ALL:
+ open_tag.append(' frame="box" rules="all"')
+ elif options["hrules"] == FRAME and options["vrules"] == FRAME:
+ open_tag.append(' frame="box"')
+ elif options["hrules"] == FRAME and options["vrules"] == ALL:
+ open_tag.append(' frame="box" rules="cols"')
+ elif options["hrules"] == FRAME:
+ open_tag.append(' frame="hsides"')
+ elif options["hrules"] == ALL:
+ open_tag.append(' frame="hsides" rules="rows"')
+ elif options["vrules"] == FRAME:
+ open_tag.append(' frame="vsides"')
+ elif options["vrules"] == ALL:
+ open_tag.append(' frame="vsides" rules="cols"')
+ if not options["border"] and options["preserve_internal_border"]:
+ open_tag.append(' rules="cols"')
+ if options["attributes"]:
+ for attr_name in options["attributes"]:
+ open_tag.append(
+ f' {escape(attr_name)}="{escape(options["attributes"][attr_name])}"'
+ )
+ open_tag.append(">")
+ lines.append("".join(open_tag))
+
+ # Title
+ title = options["title"] or self._title
+ if title:
+ lines.append(f" <caption>{escape(title)}</caption>")
+
+ # Headers
+ if options["header"]:
+ lines.append(" <thead>")
+ lines.append(" <tr>")
+ for field in self._field_names:
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ ' <th style="padding-left: %dem; padding-right: %dem; text-align: center">%s</th>' # noqa: E501
+ % (lpad, rpad, escape(field).replace("\n", linebreak))
+ )
+ lines.append(" </tr>")
+ lines.append(" </thead>")
+
+ # Data
+ lines.append(" <tbody>")
+ rows = self._get_rows(options)
+ formatted_rows = self._format_rows(rows)
+ aligns = []
+ valigns = []
+ for field in self._field_names:
+ aligns.append(
+ {"l": "left", "r": "right", "c": "center"}[self._align[field]]
+ )
+ valigns.append(
+ {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
+ )
+ for row in formatted_rows:
+ lines.append(" <tr>")
+ for field, datum, align, valign in zip(
+ self._field_names, row, aligns, valigns
+ ):
+ if options["fields"] and field not in options["fields"]:
+ continue
+ lines.append(
+ ' <td style="padding-left: %dem; padding-right: %dem; text-align: %s; vertical-align: %s">%s</td>' # noqa: E501
+ % (
+ lpad,
+ rpad,
+ align,
+ valign,
+ escape(datum).replace("\n", linebreak),
+ )
+ )
+ lines.append(" </tr>")
+ lines.append(" </tbody>")
+ lines.append("</table>")
+
+ return "\n".join(lines)
+
+ ##############################
+ # LATEX STRING METHODS #
+ ##############################
+
+ def get_latex_string(self, **kwargs) -> str:
+ """Return string representation of LaTex formatted version of table in current
+ state.
+
+ Arguments:
+
+ start - index of first data row to include in output
+ end - index of last data row to include in output PLUS ONE (list slice style)
+ fields - names of fields (columns) to include
+ header - print a header showing field names (True or False)
+ border - print a border around the table (True or False)
+ preserve_internal_border - print a border inside the table even if
+ border is disabled (True or False)
+ hrules - controls printing of horizontal rules after rows.
+ Allowed values: ALL, FRAME, HEADER, NONE
+ vrules - controls printing of vertical rules between columns.
+ Allowed values: FRAME, ALL, NONE
+ int_format - controls formatting of integer data
+ float_format - controls formatting of floating point data
+ sortby - name of field to sort rows by
+ sort_key - sorting key function, applied to data points before sorting
+ format - Controls whether or not HTML tables are formatted to match
+ styling options (True or False)
+ """
+ options = self._get_options(kwargs)
+
+ if options["format"]:
+ string = self._get_formatted_latex_string(options)
+ else:
+ string = self._get_simple_latex_string(options)
+ return string
+
+ def _get_simple_latex_string(self, options):
+ lines = []
+
+ wanted_fields = []
+ if options["fields"]:
+ wanted_fields = [
+ field for field in self._field_names if field in options["fields"]
+ ]
+ else:
+ wanted_fields = self._field_names
+
+ alignments = "".join([self._align[field] for field in wanted_fields])
+
+ begin_cmd = "\\begin{tabular}{%s}" % alignments
+ lines.append(begin_cmd)
+
+ # Headers
+ if options["header"]:
+ lines.append(" & ".join(wanted_fields) + " \\\\")
+
+ # Data
+ rows = self._get_rows(options)
+ formatted_rows = self._format_rows(rows)
+ for row in formatted_rows:
+ wanted_data = [
+ d for f, d in zip(self._field_names, row) if f in wanted_fields
+ ]
+ lines.append(" & ".join(wanted_data) + " \\\\")
+
+ lines.append("\\end{tabular}")
+
+ return "\r\n".join(lines)
+
+ def _get_formatted_latex_string(self, options):
+ lines = []
+
+ wanted_fields = []
+ if options["fields"]:
+ wanted_fields = [
+ field for field in self._field_names if field in options["fields"]
+ ]
+ else:
+ wanted_fields = self._field_names
+
+ wanted_alignments = [self._align[field] for field in wanted_fields]
+ if options["border"] and options["vrules"] == ALL:
+ alignment_str = "|".join(wanted_alignments)
+ elif not options["border"] and options["preserve_internal_border"]:
+ alignment_str = "|".join(wanted_alignments)
+ else:
+ alignment_str = "".join(wanted_alignments)
+
+ if options["border"] and options["vrules"] in [ALL, FRAME]:
+ alignment_str = "|" + alignment_str + "|"
+
+ begin_cmd = "\\begin{tabular}{%s}" % alignment_str
+ lines.append(begin_cmd)
+ if options["border"] and options["hrules"] in [ALL, FRAME]:
+ lines.append("\\hline")
+
+ # Headers
+ if options["header"]:
+ lines.append(" & ".join(wanted_fields) + " \\\\")
+ if (options["border"] or options["preserve_internal_border"]) and options[
+ "hrules"
+ ] in [ALL, HEADER]:
+ lines.append("\\hline")
+
+ # Data
+ rows = self._get_rows(options)
+ formatted_rows = self._format_rows(rows)
+ rows = self._get_rows(options)
+ for row in formatted_rows:
+ wanted_data = [
+ d for f, d in zip(self._field_names, row) if f in wanted_fields
+ ]
+ lines.append(" & ".join(wanted_data) + " \\\\")
+ if options["border"] and options["hrules"] == ALL:
+ lines.append("\\hline")
+
+ if options["border"] and options["hrules"] == FRAME:
+ lines.append("\\hline")
+
+ lines.append("\\end{tabular}")
+
+ return "\r\n".join(lines)
+
+
+##############################
+# UNICODE WIDTH FUNCTION #
+##############################
+
+
+def _str_block_width(val):
+ import wcwidth # type: ignore[import-not-found]
+
+ return wcwidth.wcswidth(_re.sub("", val))
+
+
+##############################
+# TABLE FACTORIES #
+##############################
+
+
+def from_csv(fp, field_names: Any | None = None, **kwargs):
+ import csv
+
+ fmtparams = {}
+ for param in [
+ "delimiter",
+ "doublequote",
+ "escapechar",
+ "lineterminator",
+ "quotechar",
+ "quoting",
+ "skipinitialspace",
+ "strict",
+ ]:
+ if param in kwargs:
+ fmtparams[param] = kwargs.pop(param)
+ if fmtparams:
+ reader = csv.reader(fp, **fmtparams)
+ else:
+ dialect = csv.Sniffer().sniff(fp.read(1024))
+ fp.seek(0)
+ reader = csv.reader(fp, dialect)
+
+ table = PrettyTable(**kwargs)
+ if field_names:
+ table.field_names = field_names
+ else:
+ table.field_names = [x.strip() for x in next(reader)]
+
+ for row in reader:
+ table.add_row([x.strip() for x in row])
+
+ return table
+
+
+def from_db_cursor(cursor, **kwargs):
+ if cursor.description:
+ table = PrettyTable(**kwargs)
+ table.field_names = [col[0] for col in cursor.description]
+ for row in cursor.fetchall():
+ table.add_row(row)
+ return table
+
+
+def from_json(json_string, **kwargs):
+ import json
+
+ table = PrettyTable(**kwargs)
+ objects = json.loads(json_string)
+ table.field_names = objects[0]
+ for obj in objects[1:]:
+ row = [obj[key] for key in table.field_names]
+ table.add_row(row)
+ return table
+
+
+class TableHandler(HTMLParser):
+ def __init__(self, **kwargs) -> None:
+ HTMLParser.__init__(self)
+ self.kwargs = kwargs
+ self.tables: list[list] = []
+ self.last_row: list[str] = []
+ self.rows: list[Any] = []
+ self.max_row_width = 0
+ self.active = None
+ self.last_content = ""
+ self.is_last_row_header = False
+ self.colspan = 0
+
+ def handle_starttag(self, tag, attrs) -> None:
+ self.active = tag
+ if tag == "th":
+ self.is_last_row_header = True
+ for key, value in attrs:
+ if key == "colspan":
+ self.colspan = int(value)
+
+ def handle_endtag(self, tag) -> None:
+ if tag in ["th", "td"]:
+ stripped_content = self.last_content.strip()
+ self.last_row.append(stripped_content)
+ if self.colspan:
+ for i in range(1, self.colspan):
+ self.last_row.append("")
+ self.colspan = 0
+
+ if tag == "tr":
+ self.rows.append((self.last_row, self.is_last_row_header))
+ self.max_row_width = max(self.max_row_width, len(self.last_row))
+ self.last_row = []
+ self.is_last_row_header = False
+ if tag == "table":
+ table = self.generate_table(self.rows)
+ self.tables.append(table)
+ self.rows = []
+ self.last_content = " "
+ self.active = None
+
+ def handle_data(self, data) -> None:
+ self.last_content += data
+
+ def generate_table(self, rows):
+ """
+ Generates from a list of rows a PrettyTable object.
+ """
+ table = PrettyTable(**self.kwargs)
+ for row in self.rows:
+ if len(row[0]) < self.max_row_width:
+ appends = self.max_row_width - len(row[0])
+ for i in range(1, appends):
+ row[0].append("-")
+
+ if row[1]:
+ self.make_fields_unique(row[0])
+ table.field_names = row[0]
+ else:
+ table.add_row(row[0])
+ return table
+
+ def make_fields_unique(self, fields) -> None:
+ """
+ iterates over the row and make each field unique
+ """
+ for i in range(0, len(fields)):
+ for j in range(i + 1, len(fields)):
+ if fields[i] == fields[j]:
+ fields[j] += "'"
+
+
+def from_html(html_code, **kwargs):
+ """
+ Generates a list of PrettyTables from a string of HTML code. Each <table> in
+ the HTML becomes one PrettyTable object.
+ """
+
+ parser = TableHandler(**kwargs)
+ parser.feed(html_code)
+ return parser.tables
+
+
+def from_html_one(html_code, **kwargs):
+ """
+ Generates a PrettyTables from a string of HTML code which contains only a
+ single <table>
+ """
+
+ tables = from_html(html_code, **kwargs)
+ try:
+ assert len(tables) == 1
+ except AssertionError:
+ msg = "More than one <table> in provided HTML code. Use from_html instead."
+ raise ValueError(msg)
+ return tables[0]
diff --git a/contrib/python/prettytable/py3/prettytable/py.typed b/contrib/python/prettytable/py3/prettytable/py.typed
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/contrib/python/prettytable/py3/prettytable/py.typed
diff --git a/contrib/python/prettytable/py3/tests/test_colortable.py b/contrib/python/prettytable/py3/tests/test_colortable.py
new file mode 100644
index 0000000000..1e057e2afc
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/test_colortable.py
@@ -0,0 +1,96 @@
+from __future__ import annotations
+
+import pytest
+
+from prettytable import PrettyTable
+from prettytable.colortable import RESET_CODE, ColorTable, Theme
+
+
+@pytest.fixture
+def row_prettytable() -> PrettyTable:
+ # Row by row...
+ table = PrettyTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ return table
+
+
+@pytest.fixture
+def row_colortable():
+ table = ColorTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ return table
+
+
+@pytest.fixture
+def color_theme() -> Theme:
+ return Theme(
+ default_color="31",
+ vertical_color="32",
+ horizontal_color="33",
+ junction_color="34",
+ )
+
+
+class TestColorTable:
+ def test_themeless(
+ self, row_prettytable: PrettyTable, row_colortable: ColorTable
+ ) -> None:
+ # Not worth the logic customizing the reset code
+ # For now we'll just get rid of it
+ assert (
+ row_colortable.get_string().replace(RESET_CODE, "")
+ == row_prettytable.get_string()
+ )
+
+ def test_theme_setter(self, color_theme: Theme) -> None:
+ table1 = ColorTable(theme=color_theme)
+
+ table2 = ColorTable()
+ table2.theme = color_theme
+
+ assert table1.theme == table2.theme
+
+ dict1 = table1.__dict__
+ dict2 = table2.__dict__
+
+ # So we don't compare functions
+ del dict1["_sort_key"]
+ del dict2["_sort_key"]
+
+ assert dict1 == dict2
+
+
+class TestFormatCode:
+ def test_basic(self) -> None:
+ assert Theme.format_code("31") == "\x1b[31m"
+
+ def test_prefix(self) -> None:
+ assert Theme.format_code("\x1b[35m") == "\x1b[35m"
+
+ def test_escapes(self) -> None:
+ assert Theme.format_code("\033[41m") == "\x1b[41m"
+ assert Theme.format_code("\u001b[41m") == "\x1b[41m"
+
+ def test_empty(self) -> None:
+ assert Theme.format_code("") == ""
+
+ def test_stripped(self) -> None:
+ assert Theme.format_code("\t\t \t") == ""
+
+ def test_multiple(self) -> None:
+ assert Theme.format_code("30;42") == "\x1b[30;42m"
+ assert Theme.format_code("\x1b[30;42m") == "\x1b[30;42m"
diff --git a/contrib/python/prettytable/py3/tests/test_prettytable.py b/contrib/python/prettytable/py3/tests/test_prettytable.py
new file mode 100644
index 0000000000..214c5c084e
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/test_prettytable.py
@@ -0,0 +1,2196 @@
+from __future__ import annotations
+
+import datetime as dt
+import io
+import random
+import sqlite3
+from math import e, pi, sqrt
+from typing import Any
+
+import pytest
+from pytest_lazy_fixtures import lf
+
+import prettytable
+from prettytable import (
+ ALL,
+ DEFAULT,
+ DOUBLE_BORDER,
+ FRAME,
+ HEADER,
+ MARKDOWN,
+ MSWORD_FRIENDLY,
+ NONE,
+ ORGMODE,
+ PLAIN_COLUMNS,
+ RANDOM,
+ SINGLE_BORDER,
+ PrettyTable,
+ from_csv,
+ from_db_cursor,
+ from_html,
+ from_html_one,
+ from_json,
+)
+
+
+def test_version() -> None:
+ assert isinstance(prettytable.__version__, str)
+ assert prettytable.__version__[0].isdigit()
+ assert prettytable.__version__.count(".") >= 2
+ assert prettytable.__version__[-1].isdigit()
+
+
+def helper_table(rows: int = 3) -> PrettyTable:
+ table = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ v = 1
+ for row in range(rows):
+ # Some have spaces, some not, to help test padding columns of different widths
+ table.add_row([f"value {v}", f"value{v+1}", f"value{v+2}"])
+ v += 3
+ return table
+
+
+@pytest.fixture
+def row_prettytable() -> PrettyTable:
+ # Row by row...
+ table = PrettyTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ return table
+
+
+@pytest.fixture
+def col_prettytable() -> PrettyTable:
+ # Column by column...
+ table = PrettyTable()
+ table.add_column(
+ "City name",
+ ["Adelaide", "Brisbane", "Darwin", "Hobart", "Sydney", "Melbourne", "Perth"],
+ )
+ table.add_column("Area", [1295, 5905, 112, 1357, 2058, 1566, 5386])
+ table.add_column(
+ "Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769]
+ )
+ table.add_column(
+ "Annual Rainfall", [600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4]
+ )
+ return table
+
+
+@pytest.fixture
+def mix_prettytable() -> PrettyTable:
+ # A mix of both!
+ table = PrettyTable()
+ table.field_names = ["City name", "Area"]
+ table.add_row(["Adelaide", 1295])
+ table.add_row(["Brisbane", 5905])
+ table.add_row(["Darwin", 112])
+ table.add_row(["Hobart", 1357])
+ table.add_row(["Sydney", 2058])
+ table.add_row(["Melbourne", 1566])
+ table.add_row(["Perth", 5386])
+ table.add_column(
+ "Population", [1158259, 1857594, 120900, 205556, 4336374, 3806092, 1554769]
+ )
+ table.add_column(
+ "Annual Rainfall", [600.5, 1146.4, 1714.7, 619.5, 1214.8, 646.9, 869.4]
+ )
+ return table
+
+
+class TestNoneOption:
+ def test_none_char_valid_option(self) -> None:
+ PrettyTable(["Field 1", "Field 2", "Field 3"], none_format="")
+
+ def test_none_char_invalid_option(self) -> None:
+ with pytest.raises(TypeError) as exc:
+ PrettyTable(["Field 1", "Field 2", "Field 3"], none_format=2)
+ assert "must be a string" in str(exc.value)
+
+ def test_no_value_replace_none(self) -> None:
+ table = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ table.add_row(["value 1", None, "value 2"])
+ assert (
+ table.get_string().strip()
+ == """
++---------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+---------+---------+
+| value 1 | None | value 2 |
++---------+---------+---------+
+""".strip()
+ )
+
+ def test_no_value_replace_none_with_default_field_names(self) -> None:
+ table = PrettyTable()
+ table.add_row(["value 1", "None", "value 2"])
+ assert (
+ table.get_string().strip()
+ == """
++---------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+---------+---------+
+| value 1 | None | value 2 |
++---------+---------+---------+
+""".strip()
+ )
+
+ def test_replace_none_all(self) -> None:
+ table = PrettyTable(["Field 1", "Field 2", "Field 3"], none_format="N/A")
+ table.add_row(["value 1", None, "None"])
+ assert (
+ table.get_string().strip()
+ == """
++---------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+---------+---------+
+| value 1 | N/A | N/A |
++---------+---------+---------+
+""".strip()
+ )
+
+ def test_replace_none_by_col(self) -> None:
+ table = PrettyTable(["Field 1", "Field 2", "Field 3"])
+ table.none_format["Field 2"] = "N/A"
+ table.none_format["Field 3"] = ""
+ table.add_row(["value 1", None, None])
+ assert (
+ table.get_string().strip()
+ == """
++---------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+---------+---------+
+| value 1 | N/A | |
++---------+---------+---------+
+""".strip()
+ )
+
+ def test_replace_none_recompute_width(self) -> None:
+ table = PrettyTable()
+ table.add_row([None])
+ table.none_format = "0123456789"
+ assert (
+ table.get_string().strip()
+ == """
++------------+
+| Field 1 |
++------------+
+| 0123456789 |
++------------+
+""".strip()
+ )
+
+ def test_replace_none_maintain_width_on_recompute(self) -> None:
+ table = PrettyTable()
+ table.add_row(["Hello"])
+ table.none_format = "0123456789"
+ assert (
+ table.get_string().strip()
+ == """
++---------+
+| Field 1 |
++---------+
+| Hello |
++---------+
+""".strip()
+ )
+
+ def test_replace_none_recompute_width_multi_column(self) -> None:
+ table = PrettyTable()
+ table.add_row(["Hello", None, "World"])
+ table.none_format = "0123456789"
+ assert (
+ table.get_string().strip()
+ == """
++---------+------------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+------------+---------+
+| Hello | 0123456789 | World |
++---------+------------+---------+
+""".strip()
+ )
+
+
+class TestBuildEquivalence:
+ """Make sure that building a table row-by-row and column-by-column yield the same
+ results"""
+
+ @pytest.mark.parametrize(
+ ["left_hand", "right_hand"],
+ [
+ (
+ lf("row_prettytable"),
+ lf("col_prettytable"),
+ ),
+ (
+ lf("row_prettytable"),
+ lf("mix_prettytable"),
+ ),
+ ],
+ )
+ def test_equivalence_ascii(
+ self, left_hand: PrettyTable, right_hand: PrettyTable
+ ) -> None:
+ assert left_hand.get_string() == right_hand.get_string()
+
+ @pytest.mark.parametrize(
+ ["left_hand", "right_hand"],
+ [
+ (
+ lf("row_prettytable"),
+ lf("col_prettytable"),
+ ),
+ (
+ lf("row_prettytable"),
+ lf("mix_prettytable"),
+ ),
+ ],
+ )
+ def test_equivalence_html(
+ self, left_hand: PrettyTable, right_hand: PrettyTable
+ ) -> None:
+ assert left_hand.get_html_string() == right_hand.get_html_string()
+
+ @pytest.mark.parametrize(
+ ["left_hand", "right_hand"],
+ [
+ (
+ lf("row_prettytable"),
+ lf("col_prettytable"),
+ ),
+ (
+ lf("row_prettytable"),
+ lf("mix_prettytable"),
+ ),
+ ],
+ )
+ def test_equivalence_latex(
+ self, left_hand: PrettyTable, right_hand: PrettyTable
+ ) -> None:
+ assert left_hand.get_latex_string() == right_hand.get_latex_string()
+
+
+class TestDeleteColumn:
+ def test_delete_column(self) -> None:
+ table = PrettyTable()
+ table.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
+ table.add_column("Area", [1295, 5905, 112])
+ table.add_column("Population", [1158259, 1857594, 120900])
+ table.del_column("Area")
+
+ without_row = PrettyTable()
+ without_row.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
+ without_row.add_column("Population", [1158259, 1857594, 120900])
+
+ assert table.get_string() == without_row.get_string()
+
+ def test_delete_illegal_column_raises_error(self) -> None:
+ table = PrettyTable()
+ table.add_column("City name", ["Adelaide", "Brisbane", "Darwin"])
+
+ with pytest.raises(ValueError):
+ table.del_column("City not-a-name")
+
+
+@pytest.fixture(scope="function")
+def field_name_less_table() -> PrettyTable:
+ table = PrettyTable()
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ return table
+
+
+class TestFieldNameLessTable:
+ """Make sure that building and stringing a table with no fieldnames works fine"""
+
+ def test_can_string_ascii(self, field_name_less_table: prettytable) -> None:
+ output = field_name_less_table.get_string()
+ assert "| Field 1 | Field 2 | Field 3 | Field 4 |" in output
+ assert "| Adelaide | 1295 | 1158259 | 600.5 |" in output
+
+ def test_can_string_html(self, field_name_less_table: prettytable) -> None:
+ output = field_name_less_table.get_html_string()
+ assert "<th>Field 1</th>" in output
+ assert "<td>Adelaide</td>" in output
+
+ def test_can_string_latex(self, field_name_less_table: prettytable) -> None:
+ output = field_name_less_table.get_latex_string()
+ assert "Field 1 & Field 2 & Field 3 & Field 4 \\\\" in output
+ assert "Adelaide & 1295 & 1158259 & 600.5 \\\\" in output
+
+ def test_add_field_names_later(self, field_name_less_table: prettytable) -> None:
+ field_name_less_table.field_names = [
+ "City name",
+ "Area",
+ "Population",
+ "Annual Rainfall",
+ ]
+ assert (
+ "City name | Area | Population | Annual Rainfall"
+ in field_name_less_table.get_string()
+ )
+
+
+@pytest.fixture(scope="function")
+def aligned_before_table() -> PrettyTable:
+ table = PrettyTable()
+ table.align = "r"
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ return table
+
+
+@pytest.fixture(scope="function")
+def aligned_after_table() -> PrettyTable:
+ table = PrettyTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ table.align = "r"
+ return table
+
+
+class TestAlignment:
+ """Make sure alignment works regardless of when it was set"""
+
+ def test_aligned_ascii(
+ self, aligned_before_table: prettytable, aligned_after_table: prettytable
+ ) -> None:
+ before = aligned_before_table.get_string()
+ after = aligned_after_table.get_string()
+ assert before == after
+
+ def test_aligned_html(
+ self, aligned_before_table: prettytable, aligned_after_table: prettytable
+ ) -> None:
+ before = aligned_before_table.get_html_string()
+ after = aligned_after_table.get_html_string()
+ assert before == after
+
+ def test_aligned_latex(
+ self, aligned_before_table: prettytable, aligned_after_table: prettytable
+ ) -> None:
+ before = aligned_before_table.get_latex_string()
+ after = aligned_after_table.get_latex_string()
+ assert before == after
+
+
+@pytest.fixture(scope="function")
+def city_data_prettytable() -> PrettyTable:
+ """Just build the Australian capital city data example table."""
+ table = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"])
+ table.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table.add_row(["Darwin", 112, 120900, 1714.7])
+ table.add_row(["Hobart", 1357, 205556, 619.5])
+ table.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table.add_row(["Perth", 5386, 1554769, 869.4])
+ return table
+
+
+@pytest.fixture(scope="function")
+def city_data_from_csv() -> PrettyTable:
+ csv_string = """City name, Area, Population, Annual Rainfall
+ Sydney, 2058, 4336374, 1214.8
+ Melbourne, 1566, 3806092, 646.9
+ Brisbane, 5905, 1857594, 1146.4
+ Perth, 5386, 1554769, 869.4
+ Adelaide, 1295, 1158259, 600.5
+ Hobart, 1357, 205556, 619.5
+ Darwin, 0112, 120900, 1714.7"""
+ csv_fp = io.StringIO(csv_string)
+ return from_csv(csv_fp)
+
+
+class TestOptionOverride:
+ """Make sure all options are properly overwritten by get_string."""
+
+ def test_border(self, city_data_prettytable: prettytable) -> None:
+ default = city_data_prettytable.get_string()
+ override = city_data_prettytable.get_string(border=False)
+ assert default != override
+
+ def test_header(self, city_data_prettytable) -> None:
+ default = city_data_prettytable.get_string()
+ override = city_data_prettytable.get_string(header=False)
+ assert default != override
+
+ def test_hrules_all(self, city_data_prettytable) -> None:
+ default = city_data_prettytable.get_string()
+ override = city_data_prettytable.get_string(hrules=ALL)
+ assert default != override
+
+ def test_hrules_none(self, city_data_prettytable) -> None:
+ default = city_data_prettytable.get_string()
+ override = city_data_prettytable.get_string(hrules=NONE)
+ assert default != override
+
+
+class TestOptionAttribute:
+ """Make sure all options which have an attribute interface work as they should.
+ Also make sure option settings are copied correctly when a table is cloned by
+ slicing."""
+
+ def test_set_for_all_columns(self, city_data_prettytable) -> None:
+ city_data_prettytable.field_names = sorted(city_data_prettytable.field_names)
+ city_data_prettytable.align = "l"
+ city_data_prettytable.max_width = 10
+ city_data_prettytable.start = 2
+ city_data_prettytable.end = 4
+ city_data_prettytable.sortby = "Area"
+ city_data_prettytable.reversesort = True
+ city_data_prettytable.header = True
+ city_data_prettytable.border = False
+ city_data_prettytable.hrules = True
+ city_data_prettytable.int_format = "4"
+ city_data_prettytable.float_format = "2.2"
+ city_data_prettytable.padding_width = 2
+ city_data_prettytable.left_padding_width = 2
+ city_data_prettytable.right_padding_width = 2
+ city_data_prettytable.vertical_char = "!"
+ city_data_prettytable.horizontal_char = "~"
+ city_data_prettytable.junction_char = "*"
+ city_data_prettytable.top_junction_char = "@"
+ city_data_prettytable.bottom_junction_char = "#"
+ city_data_prettytable.right_junction_char = "$"
+ city_data_prettytable.left_junction_char = "%"
+ city_data_prettytable.top_right_junction_char = "^"
+ city_data_prettytable.top_left_junction_char = "&"
+ city_data_prettytable.bottom_right_junction_char = "("
+ city_data_prettytable.bottom_left_junction_char = ")"
+ city_data_prettytable.format = True
+ city_data_prettytable.attributes = {"class": "prettytable"}
+ assert (
+ city_data_prettytable.get_string() == city_data_prettytable[:].get_string()
+ )
+
+ def test_set_for_one_column(self, city_data_prettytable) -> None:
+ city_data_prettytable.align["Rainfall"] = "l"
+ city_data_prettytable.max_width["Name"] = 10
+ city_data_prettytable.int_format["Population"] = "4"
+ city_data_prettytable.float_format["Area"] = "2.2"
+ assert (
+ city_data_prettytable.get_string() == city_data_prettytable[:].get_string()
+ )
+
+ def test_preserve_internal_border(self) -> None:
+ table = PrettyTable(preserve_internal_border=True)
+ assert table.preserve_internal_border is True
+
+
+@pytest.fixture(scope="module")
+def db_cursor():
+ conn = sqlite3.connect(":memory:")
+ cur = conn.cursor()
+ yield cur
+ cur.close()
+ conn.close()
+
+
+@pytest.fixture(scope="module")
+def init_db(db_cursor):
+ db_cursor.execute(
+ "CREATE TABLE cities "
+ "(name TEXT, area INTEGER, population INTEGER, rainfall REAL)"
+ )
+ db_cursor.execute('INSERT INTO cities VALUES ("Adelaide", 1295, 1158259, 600.5)')
+ db_cursor.execute('INSERT INTO cities VALUES ("Brisbane", 5905, 1857594, 1146.4)')
+ db_cursor.execute('INSERT INTO cities VALUES ("Darwin", 112, 120900, 1714.7)')
+ db_cursor.execute('INSERT INTO cities VALUES ("Hobart", 1357, 205556, 619.5)')
+ db_cursor.execute('INSERT INTO cities VALUES ("Sydney", 2058, 4336374, 1214.8)')
+ db_cursor.execute('INSERT INTO cities VALUES ("Melbourne", 1566, 3806092, 646.9)')
+ db_cursor.execute('INSERT INTO cities VALUES ("Perth", 5386, 1554769, 869.4)')
+ yield
+ db_cursor.execute("DROP TABLE cities")
+
+
+class TestBasic:
+ """Some very basic tests."""
+
+ def test_table_rows(self, city_data_prettytable: PrettyTable) -> None:
+ rows = city_data_prettytable.rows
+ assert len(rows) == 7
+ assert rows[0] == ["Adelaide", 1295, 1158259, 600.5]
+
+ def _test_no_blank_lines(self, table: prettytable) -> None:
+ string = table.get_string()
+ lines = string.split("\n")
+ assert "" not in lines
+
+ def _test_all_length_equal(self, table: prettytable) -> None:
+ string = table.get_string()
+ lines = string.split("\n")
+ lengths = [len(line) for line in lines]
+ lengths = set(lengths)
+ assert len(lengths) == 1
+
+ def test_no_blank_lines(self, city_data_prettytable) -> None:
+ """No table should ever have blank lines in it."""
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal(self, city_data_prettytable) -> None:
+ """All lines in a table should be of the same length."""
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_with_title(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.title = "My table"
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_with_title(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.title = "My table"
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_all_lengths_equal_with_long_title(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length, even with a long title."""
+ city_data_prettytable.title = "My table (75 characters wide) " + "=" * 45
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_without_border(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.border = False
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_without_border(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.border = False
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_without_header(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.header = False
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_without_header(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.header = False
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_with_hrules_none(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.hrules = NONE
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_with_hrules_none(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.hrules = NONE
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_with_hrules_all(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.hrules = ALL
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_with_hrules_all(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.hrules = ALL
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_with_style_msword(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.set_style(MSWORD_FRIENDLY)
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_with_style_msword(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.set_style(MSWORD_FRIENDLY)
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_with_int_format(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.int_format = "04"
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_with_int_format(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.int_format = "04"
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_with_float_format(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """No table should ever have blank lines in it."""
+ city_data_prettytable.float_format = "6.2f"
+ self._test_no_blank_lines(city_data_prettytable)
+
+ def test_all_lengths_equal_with_float_format(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ """All lines in a table should be of the same length."""
+ city_data_prettytable.float_format = "6.2f"
+ self._test_all_length_equal(city_data_prettytable)
+
+ def test_no_blank_lines_from_csv(self, city_data_from_csv: PrettyTable) -> None:
+ """No table should ever have blank lines in it."""
+ self._test_no_blank_lines(city_data_from_csv)
+
+ def test_all_lengths_equal_from_csv(self, city_data_from_csv: PrettyTable) -> None:
+ """All lines in a table should be of the same length."""
+ self._test_all_length_equal(city_data_from_csv)
+
+ @pytest.mark.usefixtures("init_db")
+ def test_no_blank_lines_from_db(self, db_cursor) -> None:
+ """No table should ever have blank lines in it."""
+ db_cursor.execute("SELECT * FROM cities")
+ pt = from_db_cursor(db_cursor)
+ self._test_no_blank_lines(pt)
+
+ @pytest.mark.usefixtures("init_db")
+ def test_all_lengths_equal_from_db(self, db_cursor) -> None:
+ """No table should ever have blank lines in it."""
+ db_cursor.execute("SELECT * FROM cities")
+ pt = from_db_cursor(db_cursor)
+ self._test_all_length_equal(pt)
+
+
+class TestEmptyTable:
+ """Make sure the print_empty option works"""
+
+ def test_print_empty_true(self, city_data_prettytable: PrettyTable) -> None:
+ table = PrettyTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+
+ assert table.get_string(print_empty=True) != ""
+ assert table.get_string(print_empty=True) != city_data_prettytable.get_string(
+ print_empty=True
+ )
+
+ def test_print_empty_false(self, city_data_prettytable: PrettyTable) -> None:
+ table = PrettyTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+
+ assert table.get_string(print_empty=False) == ""
+ assert table.get_string(print_empty=False) != city_data_prettytable.get_string(
+ print_empty=False
+ )
+
+ def test_interaction_with_border(self) -> None:
+ table = PrettyTable()
+ table.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+
+ assert table.get_string(border=False, print_empty=True) == ""
+
+
+class TestSlicing:
+ def test_slice_all(self, city_data_prettytable: PrettyTable) -> None:
+ table = city_data_prettytable[:]
+ assert city_data_prettytable.get_string() == table.get_string()
+
+ def test_slice_first_two_rows(self, city_data_prettytable: PrettyTable) -> None:
+ table = city_data_prettytable[0:2]
+ string = table.get_string()
+ assert len(string.split("\n")) == 6
+ assert "Adelaide" in string
+ assert "Brisbane" in string
+ assert "Melbourne" not in string
+ assert "Perth" not in string
+
+ def test_slice_last_two_rows(self, city_data_prettytable: PrettyTable) -> None:
+ table = city_data_prettytable[-2:]
+ string = table.get_string()
+ assert len(string.split("\n")) == 6
+ assert "Adelaide" not in string
+ assert "Brisbane" not in string
+ assert "Melbourne" in string
+ assert "Perth" in string
+
+
+class TestSorting:
+ def test_sort_by_different_per_columns(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ city_data_prettytable.sortby = city_data_prettytable.field_names[0]
+ old = city_data_prettytable.get_string()
+ for field in city_data_prettytable.field_names[1:]:
+ city_data_prettytable.sortby = field
+ new = city_data_prettytable.get_string()
+ assert new != old
+
+ def test_reverse_sort(self, city_data_prettytable: PrettyTable) -> None:
+ for field in city_data_prettytable.field_names:
+ city_data_prettytable.sortby = field
+ city_data_prettytable.reversesort = False
+ forward = city_data_prettytable.get_string()
+ city_data_prettytable.reversesort = True
+ backward = city_data_prettytable.get_string()
+ forward_lines = forward.split("\n")[2:] # Discard header lines
+ backward_lines = backward.split("\n")[2:]
+ backward_lines.reverse()
+ assert forward_lines == backward_lines
+
+ def test_sort_key(self, city_data_prettytable: PrettyTable) -> None:
+ # Test sorting by length of city name
+ def key(vals):
+ vals[0] = len(vals[0])
+ return vals
+
+ city_data_prettytable.sortby = "City name"
+ city_data_prettytable.sort_key = key
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Perth | 5386 | 1554769 | 869.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Melbourne | 1566 | 3806092 | 646.9 |
++-----------+------+------------+-----------------+
+""".strip()
+ )
+
+ def test_sort_slice(self) -> None:
+ """Make sure sorting and slicing interact in the expected way"""
+ table = PrettyTable(["Foo"])
+ for i in range(20, 0, -1):
+ table.add_row([i])
+ new_style = table.get_string(sortby="Foo", end=10)
+ assert "10" in new_style
+ assert "20" not in new_style
+ oldstyle = table.get_string(sortby="Foo", end=10, oldsortslice=True)
+ assert "10" not in oldstyle
+ assert "20" in oldstyle
+
+
+@pytest.fixture(scope="function")
+def float_pt() -> PrettyTable:
+ table = PrettyTable(["Constant", "Value"])
+ table.add_row(["Pi", pi])
+ table.add_row(["e", e])
+ table.add_row(["sqrt(2)", sqrt(2)])
+ return table
+
+
+class TestFloatFormat:
+ def test_no_decimals(self, float_pt: PrettyTable) -> None:
+ float_pt.float_format = ".0f"
+ float_pt.caching = False
+ assert "." not in float_pt.get_string()
+
+ def test_round_to_5dp(self, float_pt: PrettyTable) -> None:
+ float_pt.float_format = ".5f"
+ string = float_pt.get_string()
+ assert "3.14159" in string
+ assert "3.141592" not in string
+ assert "2.71828" in string
+ assert "2.718281" not in string
+ assert "2.718282" not in string
+ assert "1.41421" in string
+ assert "1.414213" not in string
+
+ def test_pad_with_2zeroes(self, float_pt: PrettyTable) -> None:
+ float_pt.float_format = "06.2f"
+ string = float_pt.get_string()
+ assert "003.14" in string
+ assert "002.72" in string
+ assert "001.41" in string
+
+
+class TestBreakLine:
+ @pytest.mark.parametrize(
+ ["rows", "hrule", "expected_result"],
+ [
+ (
+ [["value 1", "value2\nsecond line"], ["value 3", "value4"]],
+ ALL,
+ """
++---------+-------------+
+| Field 1 | Field 2 |
++---------+-------------+
+| value 1 | value2 |
+| | second line |
++---------+-------------+
+| value 3 | value4 |
++---------+-------------+
+""",
+ ),
+ (
+ [
+ ["value 1", "value2\nsecond line"],
+ ["value 3\n\nother line", "value4\n\n\nvalue5"],
+ ],
+ ALL,
+ """
++------------+-------------+
+| Field 1 | Field 2 |
++------------+-------------+
+| value 1 | value2 |
+| | second line |
++------------+-------------+
+| value 3 | value4 |
+| | |
+| other line | |
+| | value5 |
++------------+-------------+
+""",
+ ),
+ (
+ [
+ ["value 1", "value2\nsecond line"],
+ ["value 3\n\nother line", "value4\n\n\nvalue5"],
+ ],
+ FRAME,
+ """
++------------+-------------+
+| Field 1 | Field 2 |
++------------+-------------+
+| value 1 | value2 |
+| | second line |
+| value 3 | value4 |
+| | |
+| other line | |
+| | value5 |
++------------+-------------+
+""",
+ ),
+ ],
+ )
+ def test_break_line_ascii(
+ self, rows: list[list[Any]], hrule: int, expected_result: str
+ ) -> None:
+ table = PrettyTable(["Field 1", "Field 2"])
+ for row in rows:
+ table.add_row(row)
+ result = table.get_string(hrules=hrule)
+ assert result.strip() == expected_result.strip()
+
+ def test_break_line_html(self) -> None:
+ table = PrettyTable(["Field 1", "Field 2"])
+ table.add_row(["value 1", "value2\nsecond line"])
+ table.add_row(["value 3", "value4"])
+ result = table.get_html_string(hrules=ALL)
+ assert (
+ result.strip()
+ == """
+<table>
+ <thead>
+ <tr>
+ <th>Field 1</th>
+ <th>Field 2</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>value 1</td>
+ <td>value2<br>second line</td>
+ </tr>
+ <tr>
+ <td>value 3</td>
+ <td>value4</td>
+ </tr>
+ </tbody>
+</table>
+""".strip()
+ )
+
+
+class TestAnsiWidth:
+ colored = "\033[31mC\033[32mO\033[31mL\033[32mO\033[31mR\033[32mE\033[31mD\033[0m"
+
+ def test_color(self) -> None:
+ table = PrettyTable(["Field 1", "Field 2"])
+ table.add_row([self.colored, self.colored])
+ table.add_row(["nothing", "neither"])
+ result = table.get_string()
+ assert (
+ result.strip()
+ == f"""
++---------+---------+
+| Field 1 | Field 2 |
++---------+---------+
+| {self.colored} | {self.colored} |
+| nothing | neither |
++---------+---------+
+""".strip()
+ )
+
+ def test_reset(self) -> None:
+ table = PrettyTable(["Field 1", "Field 2"])
+ table.add_row(["abc def\033(B", "\033[31mabc def\033[m"])
+ table.add_row(["nothing", "neither"])
+ result = table.get_string()
+ assert (
+ result.strip()
+ == """
++---------+---------+
+| Field 1 | Field 2 |
++---------+---------+
+| abc def\033(B | \033[31mabc def\033[m |
+| nothing | neither |
++---------+---------+
+""".strip()
+ )
+
+
+class TestFromDB:
+ @pytest.mark.usefixtures("init_db")
+ def test_non_select_cursor(self, db_cursor) -> None:
+ db_cursor.execute(
+ 'INSERT INTO cities VALUES ("Adelaide", 1295, 1158259, 600.5)'
+ )
+ assert from_db_cursor(db_cursor) is None
+
+
+class TestJSONOutput:
+ def test_json_output(self) -> None:
+ t = helper_table()
+ result = t.get_json_string()
+ assert (
+ result.strip()
+ == """
+[
+ [
+ "Field 1",
+ "Field 2",
+ "Field 3"
+ ],
+ {
+ "Field 1": "value 1",
+ "Field 2": "value2",
+ "Field 3": "value3"
+ },
+ {
+ "Field 1": "value 4",
+ "Field 2": "value5",
+ "Field 3": "value6"
+ },
+ {
+ "Field 1": "value 7",
+ "Field 2": "value8",
+ "Field 3": "value9"
+ }
+]""".strip()
+ )
+
+ def test_json_output_options(self) -> None:
+ t = helper_table()
+ result = t.get_json_string(header=False, indent=None, separators=(",", ":"))
+ assert (
+ result
+ == """[{"Field 1":"value 1","Field 2":"value2","Field 3":"value3"},"""
+ """{"Field 1":"value 4","Field 2":"value5","Field 3":"value6"},"""
+ """{"Field 1":"value 7","Field 2":"value8","Field 3":"value9"}]"""
+ )
+
+
+class TestHtmlOutput:
+ def test_html_output(self) -> None:
+ t = helper_table()
+ result = t.get_html_string()
+ assert (
+ result.strip()
+ == """
+<table>
+ <thead>
+ <tr>
+ <th>Field 1</th>
+ <th>Field 2</th>
+ <th>Field 3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>value 1</td>
+ <td>value2</td>
+ <td>value3</td>
+ </tr>
+ <tr>
+ <td>value 4</td>
+ <td>value5</td>
+ <td>value6</td>
+ </tr>
+ <tr>
+ <td>value 7</td>
+ <td>value8</td>
+ <td>value9</td>
+ </tr>
+ </tbody>
+</table>
+""".strip()
+ )
+
+ def test_html_output_formatted(self) -> None:
+ t = helper_table()
+ result = t.get_html_string(format=True)
+ assert (
+ result.strip()
+ == """
+<table frame="box" rules="cols">
+ <thead>
+ <tr>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 1</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 2</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 1</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value2</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value3</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 4</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value5</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value6</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 7</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value8</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value9</td>
+ </tr>
+ </tbody>
+</table>
+""".strip() # noqa: E501
+ )
+
+ def test_html_output_with_title(self) -> None:
+ t = helper_table()
+ t.title = "Title & Title"
+ result = t.get_html_string(attributes={"bgcolor": "red", "a<b": "1<2"})
+ assert (
+ result.strip()
+ == """
+<table bgcolor="red" a&lt;b="1&lt;2">
+ <caption>Title &amp; Title</caption>
+ <thead>
+ <tr>
+ <th>Field 1</th>
+ <th>Field 2</th>
+ <th>Field 3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>value 1</td>
+ <td>value2</td>
+ <td>value3</td>
+ </tr>
+ <tr>
+ <td>value 4</td>
+ <td>value5</td>
+ <td>value6</td>
+ </tr>
+ <tr>
+ <td>value 7</td>
+ <td>value8</td>
+ <td>value9</td>
+ </tr>
+ </tbody>
+</table>
+""".strip()
+ )
+
+ def test_html_output_formatted_with_title(self) -> None:
+ t = helper_table()
+ t.title = "Title & Title"
+ result = t.get_html_string(
+ attributes={"bgcolor": "red", "a<b": "1<2"}, format=True
+ )
+ assert (
+ result.strip()
+ == """
+<table frame="box" rules="cols" bgcolor="red" a&lt;b="1&lt;2">
+ <caption>Title &amp; Title</caption>
+ <thead>
+ <tr>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 1</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 2</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 1</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value2</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value3</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 4</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value5</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value6</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 7</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value8</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value9</td>
+ </tr>
+ </tbody>
+</table>
+""".strip() # noqa: E501
+ )
+
+
+class TestPositionalJunctions:
+ """Verify different cases for positional-junction characters"""
+
+ def test_default(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
+╔═══════════╦══════╦════════════╦═════════════════╗
+║ City name ║ Area ║ Population ║ Annual Rainfall ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
+║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
+║ Darwin ║ 112 ║ 120900 ║ 1714.7 ║
+║ Hobart ║ 1357 ║ 205556 ║ 619.5 ║
+║ Sydney ║ 2058 ║ 4336374 ║ 1214.8 ║
+║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
+║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
+╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
+ )
+
+ def test_no_header(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+ city_data_prettytable.header = False
+
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
+╔═══════════╦══════╦═════════╦════════╗
+║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
+║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
+║ Darwin ║ 112 ║ 120900 ║ 1714.7 ║
+║ Hobart ║ 1357 ║ 205556 ║ 619.5 ║
+║ Sydney ║ 2058 ║ 4336374 ║ 1214.8 ║
+║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
+║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
+╚═══════════╩══════╩═════════╩════════╝""".strip()
+ )
+
+ def test_with_title(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+ city_data_prettytable.title = "Title"
+
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
+╔═════════════════════════════════════════════════╗
+║ Title ║
+╠═══════════╦══════╦════════════╦═════════════════╣
+║ City name ║ Area ║ Population ║ Annual Rainfall ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
+║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
+║ Darwin ║ 112 ║ 120900 ║ 1714.7 ║
+║ Hobart ║ 1357 ║ 205556 ║ 619.5 ║
+║ Sydney ║ 2058 ║ 4336374 ║ 1214.8 ║
+║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
+║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
+╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
+ )
+
+ def test_with_title_no_header(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+ city_data_prettytable.title = "Title"
+ city_data_prettytable.header = False
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
+╔═════════════════════════════════════╗
+║ Title ║
+╠═══════════╦══════╦═════════╦════════╣
+║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
+║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
+║ Darwin ║ 112 ║ 120900 ║ 1714.7 ║
+║ Hobart ║ 1357 ║ 205556 ║ 619.5 ║
+║ Sydney ║ 2058 ║ 4336374 ║ 1214.8 ║
+║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
+║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
+╚═══════════╩══════╩═════════╩════════╝""".strip()
+ )
+
+ def test_hrule_all(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+ city_data_prettytable.title = "Title"
+ city_data_prettytable.hrules = ALL
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
+╔═════════════════════════════════════════════════╗
+║ Title ║
+╠═══════════╦══════╦════════════╦═════════════════╣
+║ City name ║ Area ║ Population ║ Annual Rainfall ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Darwin ║ 112 ║ 120900 ║ 1714.7 ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Hobart ║ 1357 ║ 205556 ║ 619.5 ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Sydney ║ 2058 ║ 4336374 ║ 1214.8 ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
+╠═══════════╬══════╬════════════╬═════════════════╣
+║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
+╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
+ )
+
+ def test_vrules_none(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+ city_data_prettytable.vrules = NONE
+ assert (
+ city_data_prettytable.get_string().strip()
+ == "═══════════════════════════════════════════════════\n"
+ " City name Area Population Annual Rainfall \n"
+ "═══════════════════════════════════════════════════\n"
+ " Adelaide 1295 1158259 600.5 \n"
+ " Brisbane 5905 1857594 1146.4 \n"
+ " Darwin 112 120900 1714.7 \n"
+ " Hobart 1357 205556 619.5 \n"
+ " Sydney 2058 4336374 1214.8 \n"
+ " Melbourne 1566 3806092 646.9 \n"
+ " Perth 5386 1554769 869.4 \n"
+ "═══════════════════════════════════════════════════".strip()
+ )
+
+ def test_vrules_frame_with_title(self, city_data_prettytable: PrettyTable) -> None:
+ city_data_prettytable.set_style(DOUBLE_BORDER)
+ city_data_prettytable.vrules = FRAME
+ city_data_prettytable.title = "Title"
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
+╔═════════════════════════════════════════════════╗
+║ Title ║
+╠═════════════════════════════════════════════════╣
+║ City name Area Population Annual Rainfall ║
+╠═════════════════════════════════════════════════╣
+║ Adelaide 1295 1158259 600.5 ║
+║ Brisbane 5905 1857594 1146.4 ║
+║ Darwin 112 120900 1714.7 ║
+║ Hobart 1357 205556 619.5 ║
+║ Sydney 2058 4336374 1214.8 ║
+║ Melbourne 1566 3806092 646.9 ║
+║ Perth 5386 1554769 869.4 ║
+╚═════════════════════════════════════════════════╝""".strip()
+ )
+
+
+class TestStyle:
+ @pytest.mark.parametrize(
+ "style, expected",
+ [
+ pytest.param(
+ DEFAULT,
+ """
++---------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+---------+---------+
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
++---------+---------+---------+
+""",
+ id="DEFAULT",
+ ),
+ pytest.param(
+ MARKDOWN,
+ """
+| Field 1 | Field 2 | Field 3 |
+| :-----: | :-----: | :-----: |
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+""",
+ id="MARKDOWN",
+ ),
+ pytest.param(
+ MSWORD_FRIENDLY,
+ """
+| Field 1 | Field 2 | Field 3 |
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+""",
+ id="MSWORD_FRIENDLY",
+ ),
+ pytest.param(
+ ORGMODE,
+ """
+|---------+---------+---------|
+| Field 1 | Field 2 | Field 3 |
+|---------+---------+---------|
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+|---------+---------+---------|
+""",
+ id="ORGMODE",
+ ),
+ pytest.param(
+ PLAIN_COLUMNS,
+ "Field 1 Field 2 Field 3 \n"
+ "value 1 value2 value3 \n"
+ "value 4 value5 value6 \n"
+ "value 7 value8 value9",
+ id="PLAIN_COLUMNS",
+ ),
+ pytest.param(
+ RANDOM,
+ """
+'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
+% value 1% value2% value3%
+'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
+% value 4% value5% value6%
+'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
+% value 7% value8% value9%
+'^^^^^^^^^^^'^^^^^^^^^^'^^^^^^^^^^'
+""",
+ id="RANDOM",
+ ),
+ pytest.param(
+ DOUBLE_BORDER,
+ """
+╔═════════╦═════════╦═════════╗
+║ Field 1 ║ Field 2 ║ Field 3 ║
+╠═════════╬═════════╬═════════╣
+║ value 1 ║ value2 ║ value3 ║
+║ value 4 ║ value5 ║ value6 ║
+║ value 7 ║ value8 ║ value9 ║
+╚═════════╩═════════╩═════════╝
+""",
+ ),
+ pytest.param(
+ SINGLE_BORDER,
+ """
+┌─────────┬─────────┬─────────┐
+│ Field 1 │ Field 2 │ Field 3 │
+├─────────┼─────────┼─────────┤
+│ value 1 │ value2 │ value3 │
+│ value 4 │ value5 │ value6 │
+│ value 7 │ value8 │ value9 │
+└─────────┴─────────┴─────────┘
+""",
+ ),
+ ],
+ )
+ def test_style(self, style, expected) -> None:
+ # Arrange
+ t = helper_table()
+ random.seed(1234)
+
+ # Act
+ t.set_style(style)
+
+ # Assert
+ result = t.get_string()
+ assert result.strip() == expected.strip()
+
+ def test_style_invalid(self) -> None:
+ # Arrange
+ t = helper_table()
+
+ # Act / Assert
+ # This is an hrule style, not a table style
+ with pytest.raises(ValueError):
+ t.set_style(ALL)
+
+ @pytest.mark.parametrize(
+ "style, expected",
+ [
+ pytest.param(
+ MARKDOWN,
+ """
+| Align left | Align centre | Align right |
+| :----------| :----------: |-----------: |
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+""",
+ id="MARKDOWN",
+ ),
+ ],
+ )
+ def test_style_align(self, style, expected) -> None:
+ # Arrange
+ t = helper_table()
+ t.field_names = ["Align left", "Align centre", "Align right"]
+
+ # Act
+ t.set_style(style)
+ t.align["Align left"] = "l"
+ t.align["Align centre"] = "c"
+ t.align["Align right"] = "r"
+
+ # Assert
+ result = t.get_string()
+ assert result.strip() == expected.strip()
+
+
+class TestCsvOutput:
+ def test_csv_output(self) -> None:
+ t = helper_table()
+ assert t.get_csv_string(delimiter="\t", header=False) == (
+ "value 1\tvalue2\tvalue3\r\n"
+ "value 4\tvalue5\tvalue6\r\n"
+ "value 7\tvalue8\tvalue9\r\n"
+ )
+ assert t.get_csv_string() == (
+ "Field 1,Field 2,Field 3\r\n"
+ "value 1,value2,value3\r\n"
+ "value 4,value5,value6\r\n"
+ "value 7,value8,value9\r\n"
+ )
+
+
+class TestLatexOutput:
+ def test_latex_output(self) -> None:
+ t = helper_table()
+ assert t.get_latex_string() == (
+ "\\begin{tabular}{ccc}\r\n"
+ "Field 1 & Field 2 & Field 3 \\\\\r\n"
+ "value 1 & value2 & value3 \\\\\r\n"
+ "value 4 & value5 & value6 \\\\\r\n"
+ "value 7 & value8 & value9 \\\\\r\n"
+ "\\end{tabular}"
+ )
+ options = {"fields": ["Field 1", "Field 3"]}
+ assert t.get_latex_string(**options) == (
+ "\\begin{tabular}{cc}\r\n"
+ "Field 1 & Field 3 \\\\\r\n"
+ "value 1 & value3 \\\\\r\n"
+ "value 4 & value6 \\\\\r\n"
+ "value 7 & value9 \\\\\r\n"
+ "\\end{tabular}"
+ )
+
+ def test_latex_output_formatted(self) -> None:
+ t = helper_table()
+ assert t.get_latex_string(format=True) == (
+ "\\begin{tabular}{|c|c|c|}\r\n"
+ "\\hline\r\n"
+ "Field 1 & Field 2 & Field 3 \\\\\r\n"
+ "value 1 & value2 & value3 \\\\\r\n"
+ "value 4 & value5 & value6 \\\\\r\n"
+ "value 7 & value8 & value9 \\\\\r\n"
+ "\\hline\r\n"
+ "\\end{tabular}"
+ )
+
+ options = {"fields": ["Field 1", "Field 3"]}
+ assert t.get_latex_string(format=True, **options) == (
+ "\\begin{tabular}{|c|c|}\r\n"
+ "\\hline\r\n"
+ "Field 1 & Field 3 \\\\\r\n"
+ "value 1 & value3 \\\\\r\n"
+ "value 4 & value6 \\\\\r\n"
+ "value 7 & value9 \\\\\r\n"
+ "\\hline\r\n"
+ "\\end{tabular}"
+ )
+
+ options = {"vrules": FRAME}
+ assert t.get_latex_string(format=True, **options) == (
+ "\\begin{tabular}{|ccc|}\r\n"
+ "\\hline\r\n"
+ "Field 1 & Field 2 & Field 3 \\\\\r\n"
+ "value 1 & value2 & value3 \\\\\r\n"
+ "value 4 & value5 & value6 \\\\\r\n"
+ "value 7 & value8 & value9 \\\\\r\n"
+ "\\hline\r\n"
+ "\\end{tabular}"
+ )
+
+ options = {"hrules": ALL}
+ assert t.get_latex_string(format=True, **options) == (
+ "\\begin{tabular}{|c|c|c|}\r\n"
+ "\\hline\r\n"
+ "Field 1 & Field 2 & Field 3 \\\\\r\n"
+ "\\hline\r\n"
+ "value 1 & value2 & value3 \\\\\r\n"
+ "\\hline\r\n"
+ "value 4 & value5 & value6 \\\\\r\n"
+ "\\hline\r\n"
+ "value 7 & value8 & value9 \\\\\r\n"
+ "\\hline\r\n"
+ "\\end{tabular}"
+ )
+
+ def test_latex_output_header(self) -> None:
+ t = helper_table()
+ assert t.get_latex_string(format=True, hrules=HEADER) == (
+ "\\begin{tabular}{|c|c|c|}\r\n"
+ "Field 1 & Field 2 & Field 3 \\\\\r\n"
+ "\\hline\r\n"
+ "value 1 & value2 & value3 \\\\\r\n"
+ "value 4 & value5 & value6 \\\\\r\n"
+ "value 7 & value8 & value9 \\\\\r\n"
+ "\\end{tabular}"
+ )
+
+
+class TestJSONConstructor:
+ def test_json_and_back(self, city_data_prettytable: PrettyTable) -> None:
+ json_string = city_data_prettytable.get_json_string()
+ new_table = from_json(json_string)
+ assert new_table.get_string() == city_data_prettytable.get_string()
+
+
+class TestHtmlConstructor:
+ def test_html_and_back(self, city_data_prettytable: PrettyTable) -> None:
+ html_string = city_data_prettytable.get_html_string()
+ new_table = from_html(html_string)[0]
+ assert new_table.get_string() == city_data_prettytable.get_string()
+
+ def test_html_one_and_back(self, city_data_prettytable: PrettyTable) -> None:
+ html_string = city_data_prettytable.get_html_string()
+ new_table = from_html_one(html_string)
+ assert new_table.get_string() == city_data_prettytable.get_string()
+
+ def test_html_one_fail_on_many(self, city_data_prettytable: PrettyTable) -> None:
+ html_string = city_data_prettytable.get_html_string()
+ html_string += city_data_prettytable.get_html_string()
+ with pytest.raises(ValueError):
+ from_html_one(html_string)
+
+
+@pytest.fixture
+def japanese_pretty_table() -> PrettyTable:
+ table = PrettyTable(["Kanji", "Hiragana", "English"])
+ table.add_row(["神戸", "こうべ", "Kobe"])
+ table.add_row(["京都", "きょうと", "Kyoto"])
+ table.add_row(["長崎", "ながさき", "Nagasaki"])
+ table.add_row(["名古屋", "なごや", "Nagoya"])
+ table.add_row(["大阪", "おおさか", "Osaka"])
+ table.add_row(["札幌", "さっぽろ", "Sapporo"])
+ table.add_row(["東京", "とうきょう", "Tokyo"])
+ table.add_row(["横浜", "よこはま", "Yokohama"])
+ return table
+
+
+@pytest.fixture
+def emoji_pretty_table() -> PrettyTable:
+ thunder1 = [
+ '\033[38;5;226m _`/""\033[38;5;250m.-. \033[0m',
+ "\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m",
+ "\033[38;5;226m /\033[38;5;250m(___(__) \033[0m",
+ "\033[38;5;228;5m ⚡\033[38;5;111;25mʻ ʻ\033[38;5;228;5m"
+ "⚡\033[38;5;111;25mʻ ʻ \033[0m",
+ "\033[38;5;111m ʻ ʻ ʻ ʻ \033[0m",
+ ]
+ thunder2 = [
+ "\033[38;5;240;1m .-. \033[0m",
+ "\033[38;5;240;1m ( ). \033[0m",
+ "\033[38;5;240;1m (___(__) \033[0m",
+ "\033[38;5;21;1m ‚ʻ\033[38;5;228;5m⚡\033[38;5;21;25mʻ‚\033[38;5;228;5m"
+ "⚡\033[38;5;21;25m‚ʻ \033[0m",
+ "\033[38;5;21;1m ‚ʻ‚ʻ\033[38;5;228;5m⚡\033[38;5;21;25mʻ‚ʻ \033[0m",
+ ]
+ table = PrettyTable(["Thunderbolt", "Lightning"])
+ for i in range(len(thunder1)):
+ table.add_row([thunder1[i], thunder2[i]])
+ return table
+
+
+class TestMultiPattern:
+ @pytest.mark.parametrize(
+ ["pt", "expected_output", "test_type"],
+ [
+ (
+ lf("city_data_prettytable"),
+ """
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
+| Hobart | 1357 | 205556 | 619.5 |
+| Sydney | 2058 | 4336374 | 1214.8 |
+| Melbourne | 1566 | 3806092 | 646.9 |
+| Perth | 5386 | 1554769 | 869.4 |
++-----------+------+------------+-----------------+
+""",
+ "English Table",
+ ),
+ (
+ lf("japanese_pretty_table"),
+ """
++--------+------------+----------+
+| Kanji | Hiragana | English |
++--------+------------+----------+
+| 神戸 | こうべ | Kobe |
+| 京都 | きょうと | Kyoto |
+| 長崎 | ながさき | Nagasaki |
+| 名古屋 | なごや | Nagoya |
+| 大阪 | おおさか | Osaka |
+| 札幌 | さっぽろ | Sapporo |
+| 東京 | とうきょう | Tokyo |
+| 横浜 | よこはま | Yokohama |
++--------+------------+----------+
+
+""",
+ "Japanese table",
+ ),
+ (
+ lf("emoji_pretty_table"),
+ """
++-----------------+-----------------+
+| Thunderbolt | Lightning |
++-----------------+-----------------+
+| \x1b[38;5;226m _`/""\x1b[38;5;250m.-. \x1b[0m | \x1b[38;5;240;1m .-. \x1b[0m |
+| \x1b[38;5;226m ,\\_\x1b[38;5;250m( ). \x1b[0m | \x1b[38;5;240;1m ( ). \x1b[0m |
+| \x1b[38;5;226m /\x1b[38;5;250m(___(__) \x1b[0m | \x1b[38;5;240;1m (___(__) \x1b[0m |
+| \x1b[38;5;228;5m ⚡\x1b[38;5;111;25mʻ ʻ\x1b[38;5;228;5m⚡\x1b[38;5;111;25mʻ ʻ \x1b[0m | \x1b[38;5;21;1m ‚ʻ\x1b[38;5;228;5m⚡\x1b[38;5;21;25mʻ‚\x1b[38;5;228;5m⚡\x1b[38;5;21;25m‚ʻ \x1b[0m |
+| \x1b[38;5;111m ʻ ʻ ʻ ʻ \x1b[0m | \x1b[38;5;21;1m ‚ʻ‚ʻ\x1b[38;5;228;5m⚡\x1b[38;5;21;25mʻ‚ʻ \x1b[0m |
++-----------------+-----------------+
+ """, # noqa: E501
+ "Emoji table",
+ ),
+ ],
+ )
+ def test_multi_pattern_outputs(
+ self, pt: PrettyTable, expected_output: str, test_type: str
+ ) -> None:
+ printed_table = pt.get_string()
+ assert (
+ printed_table.strip() == expected_output.strip()
+ ), f"Error output for test output of type {test_type}"
+
+
+def test_paginate() -> None:
+ # Arrange
+ t = helper_table(rows=7)
+ expected_page_1 = """
++----------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++----------+---------+---------+
+| value 1 | value2 | value3 |
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
+| value 10 | value11 | value12 |
++----------+---------+---------+
+ """.strip()
+ expected_page_2 = """
++----------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++----------+---------+---------+
+| value 13 | value14 | value15 |
+| value 16 | value17 | value18 |
+| value 19 | value20 | value21 |
++----------+---------+---------+
+""".strip()
+
+ # Act
+ paginated = t.paginate(page_length=4)
+
+ # Assert
+ paginated = paginated.strip()
+ assert paginated.startswith(expected_page_1)
+ assert "\f" in paginated
+ assert paginated.endswith(expected_page_2)
+
+ # Act
+ paginated = t.paginate(page_length=4, line_break="\n")
+
+ # Assert
+ assert "\f" not in paginated
+ assert "\n" in paginated
+
+
+def test_add_rows() -> None:
+ """A table created with multiple add_row calls
+ is the same as one created with a single add_rows
+ """
+ # Arrange
+ table1 = PrettyTable(["A", "B", "C"])
+ table2 = PrettyTable(["A", "B", "C"])
+ table1.add_row([1, 2, 3])
+ table1.add_row([4, 5, 6])
+ rows = [
+ [1, 2, 3],
+ [4, 5, 6],
+ ]
+
+ # Act
+ table2.add_rows(rows)
+
+ # Assert
+ assert str(table1) == str(table2)
+
+
+def test_autoindex() -> None:
+ """Testing that a table with a custom index row is
+ equal to the one produced by the function
+ .add_autoindex()
+ """
+ table1 = PrettyTable()
+ table1.field_names = ["City name", "Area", "Population", "Annual Rainfall"]
+ table1.add_row(["Adelaide", 1295, 1158259, 600.5])
+ table1.add_row(["Brisbane", 5905, 1857594, 1146.4])
+ table1.add_row(["Darwin", 112, 120900, 1714.7])
+ table1.add_row(["Hobart", 1357, 205556, 619.5])
+ table1.add_row(["Sydney", 2058, 4336374, 1214.8])
+ table1.add_row(["Melbourne", 1566, 3806092, 646.9])
+ table1.add_row(["Perth", 5386, 1554769, 869.4])
+ table1.add_autoindex(fieldname="Test")
+
+ table2 = PrettyTable()
+ table2.field_names = ["Test", "City name", "Area", "Population", "Annual Rainfall"]
+ table2.add_row([1, "Adelaide", 1295, 1158259, 600.5])
+ table2.add_row([2, "Brisbane", 5905, 1857594, 1146.4])
+ table2.add_row([3, "Darwin", 112, 120900, 1714.7])
+ table2.add_row([4, "Hobart", 1357, 205556, 619.5])
+ table2.add_row([5, "Sydney", 2058, 4336374, 1214.8])
+ table2.add_row([6, "Melbourne", 1566, 3806092, 646.9])
+ table2.add_row([7, "Perth", 5386, 1554769, 869.4])
+
+ assert str(table1) == str(table2)
+
+
+@pytest.fixture(scope="function")
+def unpadded_pt() -> PrettyTable:
+ table = PrettyTable(header=False, padding_width=0)
+ table.add_row("abc")
+ table.add_row("def")
+ table.add_row("g..")
+ return table
+
+
+class TestUnpaddedTable:
+ def test_unbordered(self, unpadded_pt: PrettyTable) -> None:
+ unpadded_pt.border = False
+ result = unpadded_pt.get_string()
+ expected = """
+abc
+def
+g..
+"""
+ assert result.strip() == expected.strip()
+
+ def test_bordered(self, unpadded_pt: PrettyTable) -> None:
+ unpadded_pt.border = True
+ result = unpadded_pt.get_string()
+ expected = """
++-+-+-+
+|a|b|c|
+|d|e|f|
+|g|.|.|
++-+-+-+
+"""
+ assert result.strip() == expected.strip()
+
+
+class TestCustomFormatter:
+ def test_init_custom_format_is_empty(self) -> None:
+ table = PrettyTable()
+ assert table.custom_format == {}
+
+ def test_init_custom_format_set_value(self) -> None:
+ table = PrettyTable(
+ custom_format={"col1": (lambda col_name, value: f"{value:.2}")}
+ )
+ assert len(table.custom_format) == 1
+
+ def test_init_custom_format_throw_error_is_not_callable(self) -> None:
+ with pytest.raises(ValueError) as e:
+ PrettyTable(custom_format={"col1": "{:.2}"})
+
+ assert "Invalid value for custom_format.col1. Must be a function." in str(
+ e.value
+ )
+
+ def test_can_set_custom_format_from_property_setter(self) -> None:
+ table = PrettyTable()
+ table.custom_format = {"col1": (lambda col_name, value: f"{value:.2}")}
+ assert len(table.custom_format) == 1
+
+ def test_set_custom_format_to_none_set_empty_dict(self) -> None:
+ table = PrettyTable()
+ table.custom_format = None
+ assert len(table.custom_format) == 0
+ assert isinstance(table.custom_format, dict)
+
+ def test_set_custom_format_invalid_type_throw_error(self) -> None:
+ table = PrettyTable()
+ with pytest.raises(TypeError) as e:
+ table.custom_format = "Some String"
+ assert "The custom_format property need to be a dictionary or callable" in str(
+ e.value
+ )
+
+ def test_use_custom_formatter_for_int(
+ self, city_data_prettytable: PrettyTable
+ ) -> None:
+ city_data_prettytable.custom_format["Annual Rainfall"] = lambda n, v: f"{v:.2f}"
+ assert (
+ city_data_prettytable.get_string().strip()
+ == """
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.50 |
+| Brisbane | 5905 | 1857594 | 1146.40 |
+| Darwin | 112 | 120900 | 1714.70 |
+| Hobart | 1357 | 205556 | 619.50 |
+| Sydney | 2058 | 4336374 | 1214.80 |
+| Melbourne | 1566 | 3806092 | 646.90 |
+| Perth | 5386 | 1554769 | 869.40 |
++-----------+------+------------+-----------------+
+""".strip()
+ )
+
+ def test_custom_format_multi_type(self) -> None:
+ table = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
+ table.add_row([dt.date(2021, 1, 1), "January", 12345.12345, 12345678])
+ table.add_row([dt.date(2021, 2, 1), "February", 54321.12345, 87654321])
+ table.custom_format["col_date"] = lambda f, v: v.strftime("%d %b %Y")
+ table.custom_format["col_float"] = lambda f, v: f"{v:.3f}"
+ table.custom_format["col_int"] = lambda f, v: f"{v:,}"
+ assert (
+ table.get_string().strip()
+ == """
++-------------+----------+-----------+------------+
+| col_date | col_str | col_float | col_int |
++-------------+----------+-----------+------------+
+| 01 Jan 2021 | January | 12345.123 | 12,345,678 |
+| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
++-------------+----------+-----------+------------+
+""".strip()
+ )
+
+ def test_custom_format_multi_type_using_on_function(self) -> None:
+ table = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
+ table.add_row([dt.date(2021, 1, 1), "January", 12345.12345, 12345678])
+ table.add_row([dt.date(2021, 2, 1), "February", 54321.12345, 87654321])
+
+ def my_format(col: str, value: Any) -> str:
+ if col == "col_date":
+ return value.strftime("%d %b %Y")
+ if col == "col_float":
+ return f"{value:.3f}"
+ if col == "col_int":
+ return f"{value:,}"
+ return str(value)
+
+ table.custom_format = my_format
+ assert (
+ table.get_string().strip()
+ == """
++-------------+----------+-----------+------------+
+| col_date | col_str | col_float | col_int |
++-------------+----------+-----------+------------+
+| 01 Jan 2021 | January | 12345.123 | 12,345,678 |
+| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
++-------------+----------+-----------+------------+
+""".strip()
+ )
+
+
+class TestRepr:
+ def test_default_repr(self, row_prettytable: PrettyTable) -> None:
+ assert row_prettytable.__str__() == row_prettytable.__repr__()
+
+ def test_jupyter_repr(self, row_prettytable: PrettyTable) -> None:
+ assert row_prettytable._repr_html_() == row_prettytable.get_html_string()
+
+
+class TestMinTableWidth:
+ @pytest.mark.parametrize(
+ "loops, fields, desired_width, border, internal_border",
+ [
+ (15, ["Test table"], 20, True, False),
+ (16, ["Test table"], 21, True, False),
+ (18, ["Test table", "Test table 2"], 40, True, False),
+ (19, ["Test table", "Test table 2"], 41, True, False),
+ (21, ["Test table", "Test col 2", "Test col 3"], 50, True, False),
+ (22, ["Test table", "Test col 2", "Test col 3"], 51, True, False),
+ (19, ["Test table"], 20, False, False),
+ (20, ["Test table"], 21, False, False),
+ (25, ["Test table", "Test table 2"], 40, False, False),
+ (26, ["Test table", "Test table 2"], 41, False, False),
+ (25, ["Test table", "Test col 2", "Test col 3"], 50, False, False),
+ (26, ["Test table", "Test col 2", "Test col 3"], 51, False, False),
+ (18, ["Test table"], 20, False, True),
+ (19, ["Test table"], 21, False, True),
+ (23, ["Test table", "Test table 2"], 40, False, True),
+ (24, ["Test table", "Test table 2"], 41, False, True),
+ (22, ["Test table", "Test col 2", "Test col 3"], 50, False, True),
+ (23, ["Test table", "Test col 2", "Test col 3"], 51, False, True),
+ ],
+ )
+ def test_min_table_width(
+ self, loops, fields, desired_width, border, internal_border
+ ) -> None:
+ for col_width in range(loops):
+ x = prettytable.PrettyTable()
+ x.border = border
+ x.preserve_internal_border = internal_border
+ x.field_names = fields
+ x.add_row(["X" * col_width] + ["" for _ in range(len(fields) - 1)])
+ x.min_table_width = desired_width
+ t = x.get_string()
+ if border is False and internal_border is False:
+ assert [len(x) for x in t.split("\n")] == [desired_width, desired_width]
+ elif border is False and internal_border is True:
+ assert [len(x) for x in t.split("\n")] == [
+ desired_width,
+ desired_width - 1,
+ desired_width,
+ ]
+ else:
+ assert [len(x) for x in t.split("\n")] == [
+ desired_width,
+ desired_width,
+ desired_width,
+ desired_width,
+ desired_width,
+ ]
+
+
+class TestMaxTableWidth:
+ def test_max_table_width(self) -> None:
+ table = PrettyTable()
+ table.max_table_width = 5
+ table.add_row([0])
+
+ assert (
+ table.get_string().strip()
+ == """
++-----+
+| Fie |
++-----+
+| 0 |
++-----+
+""".strip()
+ )
+
+
+class TestRowEndSection:
+ def test_row_end_section(self) -> None:
+ table = PrettyTable()
+ v = 1
+ for row in range(4):
+ if row % 2 == 0:
+ table.add_row(
+ [f"value {v}", f"value{v+1}", f"value{v+2}"], divider=True
+ )
+ else:
+ table.add_row(
+ [f"value {v}", f"value{v+1}", f"value{v+2}"], divider=False
+ )
+ v += 3
+ table.del_row(0)
+ assert (
+ table.get_string().strip()
+ == """
++----------+---------+---------+
+| Field 1 | Field 2 | Field 3 |
++----------+---------+---------+
+| value 4 | value5 | value6 |
+| value 7 | value8 | value9 |
++----------+---------+---------+
+| value 10 | value11 | value12 |
++----------+---------+---------+
+""".strip()
+ )
+
+
+class TestClearing:
+ def test_clear_rows(self, row_prettytable: PrettyTable) -> None:
+ t = helper_table()
+ t.add_row(["a", "b", "c"], divider=True)
+ t.clear_rows()
+ assert t.rows == []
+ assert t.dividers == []
+ assert t.field_names == ["Field 1", "Field 2", "Field 3"]
+
+ def test_clear(self, row_prettytable: PrettyTable) -> None:
+ t = helper_table()
+ t.add_row(["a", "b", "c"], divider=True)
+ t.clear()
+ assert t.rows == []
+ assert t.dividers == []
+ assert t.field_names == []
+
+
+class TestPreservingInternalBorders:
+ def test_internal_border_preserved(self) -> None:
+ pt = helper_table(3)
+ pt.border = False
+ pt.preserve_internal_border = True
+
+ assert (
+ pt.get_string().strip()
+ == """
+ Field 1 | Field 2 | Field 3
+---------+---------+---------
+ value 1 | value2 | value3
+ value 4 | value5 | value6
+ value 7 | value8 | value9
+""".strip() # noqa: W291
+ )
+
+ def test_internal_border_preserved_latex(self) -> None:
+ pt = helper_table(3)
+ pt.border = False
+ pt.format = True
+ pt.preserve_internal_border = True
+
+ assert pt.get_latex_string().strip() == (
+ "\\begin{tabular}{c|c|c}\r\n"
+ "Field 1 & Field 2 & Field 3 \\\\\r\n"
+ "value 1 & value2 & value3 \\\\\r\n"
+ "value 4 & value5 & value6 \\\\\r\n"
+ "value 7 & value8 & value9 \\\\\r\n"
+ "\\end{tabular}"
+ )
+
+ def test_internal_border_preserved_html(self) -> None:
+ pt = helper_table(3)
+ pt.format = True
+ pt.border = False
+ pt.preserve_internal_border = True
+
+ assert (
+ pt.get_html_string().strip()
+ == """
+<table rules="cols">
+ <thead>
+ <tr>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 1</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 2</th>
+ <th style="padding-left: 1em; padding-right: 1em; text-align: center">Field 3</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 1</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value2</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value3</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 4</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value5</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value6</td>
+ </tr>
+ <tr>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 7</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value8</td>
+ <td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value9</td>
+ </tr>
+ </tbody>
+</table>
+""".strip() # noqa: E501
+ )
+
+
+class TestGeneralOutput:
+ def test_copy(self) -> None:
+ # Arrange
+ t = helper_table()
+
+ # Act
+ t_copy = t.copy()
+
+ # Assert
+ assert t.get_string() == t_copy.get_string()
+
+ def test_text(self) -> None:
+ t = helper_table()
+ assert t.get_formatted_string("text") == t.get_string()
+ # test with default arg, too
+ assert t.get_formatted_string() == t.get_string()
+ # args passed through
+ assert t.get_formatted_string(border=False) == t.get_string(border=False)
+
+ def test_csv(self) -> None:
+ t = helper_table()
+ assert t.get_formatted_string("csv") == t.get_csv_string()
+ # args passed through
+ assert t.get_formatted_string("csv", border=False) == t.get_csv_string(
+ border=False
+ )
+
+ def test_json(self) -> None:
+ t = helper_table()
+ assert t.get_formatted_string("json") == t.get_json_string()
+ # args passed through
+ assert t.get_formatted_string("json", border=False) == t.get_json_string(
+ border=False
+ )
+
+ def test_html(self) -> None:
+ t = helper_table()
+ assert t.get_formatted_string("html") == t.get_html_string()
+ # args passed through
+ assert t.get_formatted_string("html", border=False) == t.get_html_string(
+ border=False
+ )
+
+ def test_latex(self) -> None:
+ t = helper_table()
+ assert t.get_formatted_string("latex") == t.get_latex_string()
+ # args passed through
+ assert t.get_formatted_string("latex", border=False) == t.get_latex_string(
+ border=False
+ )
+
+ def test_invalid(self) -> None:
+ t = helper_table()
+ with pytest.raises(ValueError):
+ t.get_formatted_string("pdf")
diff --git a/contrib/python/prettytable/py3/tests/ya.make b/contrib/python/prettytable/py3/tests/ya.make
new file mode 100644
index 0000000000..50fbf7a3ac
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/ya.make
@@ -0,0 +1,17 @@
+PY3TEST()
+
+SUBSCRIBER(g:python-contrib)
+
+PEERDIR(
+ contrib/python/prettytable
+ contrib/python/pytest-lazy-fixtures
+)
+
+TEST_SRCS(
+ test_colortable.py
+ test_prettytable.py
+)
+
+NO_LINT()
+
+END()
diff --git a/contrib/python/prettytable/py3/ya.make b/contrib/python/prettytable/py3/ya.make
new file mode 100644
index 0000000000..1cadef12cd
--- /dev/null
+++ b/contrib/python/prettytable/py3/ya.make
@@ -0,0 +1,35 @@
+# Generated by devtools/yamaker (pypi).
+
+PY3_LIBRARY()
+
+SUBSCRIBER(manushkin g:python-contrib)
+
+VERSION(3.10.0)
+
+LICENSE(BSD-3-Clause)
+
+PEERDIR(
+ contrib/python/wcwidth
+)
+
+NO_LINT()
+
+PY_SRCS(
+ TOP_LEVEL
+ prettytable/__init__.py
+ prettytable/colortable.py
+ prettytable/prettytable.py
+)
+
+RESOURCE_FILES(
+ PREFIX contrib/python/prettytable/py3/
+ .dist-info/METADATA
+ .dist-info/top_level.txt
+ prettytable/py.typed
+)
+
+END()
+
+RECURSE_FOR_TESTS(
+ tests
+)
diff --git a/contrib/python/prettytable/ya.make b/contrib/python/prettytable/ya.make
new file mode 100644
index 0000000000..594a928dcf
--- /dev/null
+++ b/contrib/python/prettytable/ya.make
@@ -0,0 +1,20 @@
+PY23_LIBRARY()
+
+LICENSE(Service-Py23-Proxy)
+
+SUBSCRIBER(g:python-contrib)
+
+IF (PYTHON2)
+ PEERDIR(contrib/python/prettytable/py2)
+ELSE()
+ PEERDIR(contrib/python/prettytable/py3)
+ENDIF()
+
+NO_LINT()
+
+END()
+
+RECURSE(
+ py2
+ py3
+)