diff options
author | aozeritsky <aozeritsky@ydb.tech> | 2024-05-22 13:40:51 +0300 |
---|---|---|
committer | aozeritsky <aozeritsky@ydb.tech> | 2024-05-22 13:52:22 +0300 |
commit | 2b21072ba9e98cefcbfd28d197d0c4c1335a93d6 (patch) | |
tree | 69222745e7505b63a5b54774cf2295c081f08e77 | |
parent | 2166bc25086cc3437a69d2d31bb7b6da10a65062 (diff) | |
download | ydb-2b21072ba9e98cefcbfd28d197d0c4c1335a93d6.tar.gz |
Add contrib/python/prettytable
8db8e29fdff582b08a53f0952a29362e0c760185
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<b="1<2"> + <caption>Title & 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<b="1<2"> + <caption>Title & 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 +) |