summaryrefslogtreecommitdiffstats
path: root/contrib/python/prettytable
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-07-09 01:59:16 +0300
committerrobot-piglet <[email protected]>2026-07-09 02:31:39 +0300
commit0a6ad8975632e0d3260c7e66f6295e5f1ed38eb9 (patch)
treeac7da00b0db0ec6640d4430117c8fc3554d7f039 /contrib/python/prettytable
parent9577015f874827f134d4095e8ac6a0fe6b6309c7 (diff)
Intermediate changes
commit_hash:84e4bb7342d3d78316fd09d0e25bd5290245afd4
Diffstat (limited to 'contrib/python/prettytable')
-rw-r--r--contrib/python/prettytable/py3/.dist-info/METADATA123
-rw-r--r--contrib/python/prettytable/py3/README.md116
-rw-r--r--contrib/python/prettytable/py3/patches/01-fix-tests.patch6
-rw-r--r--contrib/python/prettytable/py3/prettytable/__init__.py7
-rw-r--r--contrib/python/prettytable/py3/prettytable/_version.py26
-rw-r--r--contrib/python/prettytable/py3/prettytable/colortable.py35
-rw-r--r--contrib/python/prettytable/py3/prettytable/prettytable.py792
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_align_center.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_align_left.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_align_right.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt7
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_cjk.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_combining_chars.txt6
-rw-r--r--contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt7
-rw-r--r--contrib/python/prettytable/py3/tests/test_colortable.py4
-rw-r--r--contrib/python/prettytable/py3/tests/test_html.py138
-rw-r--r--contrib/python/prettytable/py3/tests/test_json.py10
-rw-r--r--contrib/python/prettytable/py3/tests/test_mediawiki.py10
-rw-r--r--contrib/python/prettytable/py3/tests/test_prettytable.py578
-rw-r--r--contrib/python/prettytable/py3/tests/test_rst.py97
-rw-r--r--contrib/python/prettytable/py3/tests/test_sorting.py59
-rw-r--r--contrib/python/prettytable/py3/tests/test_style.py223
-rw-r--r--contrib/python/prettytable/py3/ya.make2
23 files changed, 1648 insertions, 622 deletions
diff --git a/contrib/python/prettytable/py3/.dist-info/METADATA b/contrib/python/prettytable/py3/.dist-info/METADATA
index 6c57a35f2bc..85d2d452c4b 100644
--- a/contrib/python/prettytable/py3/.dist-info/METADATA
+++ b/contrib/python/prettytable/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: prettytable
-Version: 3.17.0
+Version: 3.18.0
Summary: A simple Python library for easily displaying tabular data in a visually appealing ASCII table format
Project-URL: Changelog, https://github.com/prettytable/prettytable/releases
Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-prettytable?utm_source=pypi-prettytable&utm_medium=pypi
@@ -18,16 +18,17 @@ Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
+Classifier: Programming Language :: Python :: 3.16
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Text Processing
Classifier: Typing :: Typed
Requires-Python: >=3.10
-Requires-Dist: wcwidth
+Requires-Dist: wcwidth>=0.3.5
Provides-Extra: tests
-Requires-Dist: pytest; extra == 'tests'
Requires-Dist: pytest-cov; extra == 'tests'
Requires-Dist: pytest-lazy-fixtures; extra == 'tests'
+Requires-Dist: pytest>=9; extra == 'tests'
Description-Content-Type: text/markdown
# PrettyTable
@@ -61,11 +62,11 @@ PrettyTable lets you print tables in an attractive ASCII form:
Install via pip:
- python -m pip install -U prettytable
+ python3 -m pip install -U prettytable
Install latest development version:
- python -m pip install -U git+https://github.com/prettytable/prettytable
+ python3 -m pip install -U git+https://github.com/prettytable/prettytable
Or from `requirements.txt`:
@@ -135,13 +136,10 @@ method, which takes two arguments - a string which is the name for the field the
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("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])
+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
@@ -255,6 +253,30 @@ def my_cli_function(table_format: str = 'text'):
print(table.get_formatted_string(table_format))
```
+#### Paginating your table
+
+If you have a large table and want to split it into multiple pages, you can use the
+`paginate` method. This method splits the table into pages of a specified length and
+separates them with a line break character (by default, a form feed character `\f`):
+
+```python
+paginated_string = table.paginate(page_length=10)
+print(paginated_string)
+```
+
+The `page_length` parameter controls how many rows appear on each page (default: 58).
+The `line_break` parameter specifies the string used to separate pages (default: `"\f"`
+form feed). You can also pass any keyword arguments that are accepted by `get_string()`
+to control the formatting of each page:
+
+```python
+paginated_string = table.paginate(page_length=20, line_break="\n\n---\n\n", border=True)
+print(paginated_string)
+```
+
+This is particularly useful when printing large tables to a terminal or when you want to
+format output for pagination in documents.
+
#### Controlling which data gets displayed
If you like, you can restrict the output of `print(table)` or `table.get_string` to only
@@ -497,6 +519,39 @@ to get a table like this:
Any added dividers will be removed if a table is sorted.
+### Adding a title to your table
+
+You can add a title to your table with the `title` attribute:
+
+```python
+table.title = "Australian Cities"
+```
+
+Titles can span multiple lines by including `\n` in the string:
+
+```python
+table.title = "Australian Cities\nPopulation Data"
+print(table)
+```
+
+This produces:
+
+```
++-------------------------------------------------+
+| Australian Cities |
+| Population Data |
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
++-----------+------+------------+-----------------+
+```
+
+Each line of the title is centered and bordered independently. Multiline titles also
+work with HTML output (using `<br>` in the `<caption>` tag) and Markdown output.
+
### Changing the appearance of your table - the easy way
By default, PrettyTable produces ASCII tables that look like the ones used in SQL
@@ -525,6 +580,8 @@ In addition to `MARKDOWN` you can use these in-built styles:
- `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
+- `RST` - A table style that produces reStructuredText
+ [grid tables](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#tables)
- `SINGLE_BORDER` and `DOUBLE_BORDER` - Styles that use continuous single/double border
lines with Box drawing characters for a fancier display on terminal
@@ -642,6 +699,36 @@ print(table.get_string(border=False))
print(table)
```
+#### Custom format example (with _colors_)
+
+The `custom_format` attribute allows you to define a function for custom cell value
+formatting. For example, the following code demonstrates how to apply a single function
+across all columns to display strings in `blue`, positive numbers in `green`, and
+negative numbers in `red`, all with a precision of two decimal places. Your custom
+function can also inspect the `field` parameter to apply field-specific formatting.
+
+```python
+from prettytable import PrettyTable
+from typing import Any
+from termcolor import colored
+
+def _colored(field: str, val: Any) -> str:
+ if isinstance(val, (int, float)):
+ if val >= 0:
+ return colored(f"{val:.2f}", "green")
+ return colored(f"{val:.2f}", "red")
+ elif isinstance(val, str):
+ return colored(val, "blue")
+ return f"{val}"
+
+table = PrettyTable(("Name", "Overtime"))
+table.custom_format = _colored
+
+for row in [["John Doe", 5.0], ["Jane Smith", -2.0]]:
+ table.add_row([row[0], row[1]])
+print(table)
+```
+
### Changing the appearance of your table - with _colors_!
PrettyTable has the functionality of printing your table with ANSI color codes. This
@@ -806,10 +893,18 @@ new_table = old_table[0:5]
## Contributing
-After editing files, use the [Black](https://github.com/psf/black) linter to auto-format
+After editing files, use at least [Black](https://github.com/psf/black) to auto-format
changed lines.
```sh
-python -m pip install black
-black prettytable*.py
+python3 -m pip install black
+black .
+```
+
+To run all pre-commit checks, linters, formatters (including Black), and tests use
+[tox](https://github.com/tox-dev/tox):
+
+```sh
+python3 -m pip install tox
+tox
```
diff --git a/contrib/python/prettytable/py3/README.md b/contrib/python/prettytable/py3/README.md
index a06e6c35cf6..52c3c694cba 100644
--- a/contrib/python/prettytable/py3/README.md
+++ b/contrib/python/prettytable/py3/README.md
@@ -29,11 +29,11 @@ PrettyTable lets you print tables in an attractive ASCII form:
Install via pip:
- python -m pip install -U prettytable
+ python3 -m pip install -U prettytable
Install latest development version:
- python -m pip install -U git+https://github.com/prettytable/prettytable
+ python3 -m pip install -U git+https://github.com/prettytable/prettytable
Or from `requirements.txt`:
@@ -103,13 +103,10 @@ method, which takes two arguments - a string which is the name for the field the
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("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])
+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
@@ -223,6 +220,30 @@ def my_cli_function(table_format: str = 'text'):
print(table.get_formatted_string(table_format))
```
+#### Paginating your table
+
+If you have a large table and want to split it into multiple pages, you can use the
+`paginate` method. This method splits the table into pages of a specified length and
+separates them with a line break character (by default, a form feed character `\f`):
+
+```python
+paginated_string = table.paginate(page_length=10)
+print(paginated_string)
+```
+
+The `page_length` parameter controls how many rows appear on each page (default: 58).
+The `line_break` parameter specifies the string used to separate pages (default: `"\f"`
+form feed). You can also pass any keyword arguments that are accepted by `get_string()`
+to control the formatting of each page:
+
+```python
+paginated_string = table.paginate(page_length=20, line_break="\n\n---\n\n", border=True)
+print(paginated_string)
+```
+
+This is particularly useful when printing large tables to a terminal or when you want to
+format output for pagination in documents.
+
#### Controlling which data gets displayed
If you like, you can restrict the output of `print(table)` or `table.get_string` to only
@@ -465,6 +486,39 @@ to get a table like this:
Any added dividers will be removed if a table is sorted.
+### Adding a title to your table
+
+You can add a title to your table with the `title` attribute:
+
+```python
+table.title = "Australian Cities"
+```
+
+Titles can span multiple lines by including `\n` in the string:
+
+```python
+table.title = "Australian Cities\nPopulation Data"
+print(table)
+```
+
+This produces:
+
+```
++-------------------------------------------------+
+| Australian Cities |
+| Population Data |
++-----------+------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | 600.5 |
+| Brisbane | 5905 | 1857594 | 1146.4 |
+| Darwin | 112 | 120900 | 1714.7 |
++-----------+------+------------+-----------------+
+```
+
+Each line of the title is centered and bordered independently. Multiline titles also
+work with HTML output (using `<br>` in the `<caption>` tag) and Markdown output.
+
### Changing the appearance of your table - the easy way
By default, PrettyTable produces ASCII tables that look like the ones used in SQL
@@ -493,6 +547,8 @@ In addition to `MARKDOWN` you can use these in-built styles:
- `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
+- `RST` - A table style that produces reStructuredText
+ [grid tables](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html#tables)
- `SINGLE_BORDER` and `DOUBLE_BORDER` - Styles that use continuous single/double border
lines with Box drawing characters for a fancier display on terminal
@@ -610,6 +666,36 @@ print(table.get_string(border=False))
print(table)
```
+#### Custom format example (with _colors_)
+
+The `custom_format` attribute allows you to define a function for custom cell value
+formatting. For example, the following code demonstrates how to apply a single function
+across all columns to display strings in `blue`, positive numbers in `green`, and
+negative numbers in `red`, all with a precision of two decimal places. Your custom
+function can also inspect the `field` parameter to apply field-specific formatting.
+
+```python
+from prettytable import PrettyTable
+from typing import Any
+from termcolor import colored
+
+def _colored(field: str, val: Any) -> str:
+ if isinstance(val, (int, float)):
+ if val >= 0:
+ return colored(f"{val:.2f}", "green")
+ return colored(f"{val:.2f}", "red")
+ elif isinstance(val, str):
+ return colored(val, "blue")
+ return f"{val}"
+
+table = PrettyTable(("Name", "Overtime"))
+table.custom_format = _colored
+
+for row in [["John Doe", 5.0], ["Jane Smith", -2.0]]:
+ table.add_row([row[0], row[1]])
+print(table)
+```
+
### Changing the appearance of your table - with _colors_!
PrettyTable has the functionality of printing your table with ANSI color codes. This
@@ -774,10 +860,18 @@ new_table = old_table[0:5]
## Contributing
-After editing files, use the [Black](https://github.com/psf/black) linter to auto-format
+After editing files, use at least [Black](https://github.com/psf/black) to auto-format
changed lines.
```sh
-python -m pip install black
-black prettytable*.py
+python3 -m pip install black
+black .
+```
+
+To run all pre-commit checks, linters, formatters (including Black), and tests use
+[tox](https://github.com/tox-dev/tox):
+
+```sh
+python3 -m pip install tox
+tox
```
diff --git a/contrib/python/prettytable/py3/patches/01-fix-tests.patch b/contrib/python/prettytable/py3/patches/01-fix-tests.patch
new file mode 100644
index 00000000000..c439ba2902c
--- /dev/null
+++ b/contrib/python/prettytable/py3/patches/01-fix-tests.patch
@@ -0,0 +1,6 @@
+--- contrib/python/prettytable/py3/tests/test_style.py (index)
++++ contrib/python/prettytable/py3/tests/test_style.py (working tree)
+@@ -18 +18,2 @@ from prettytable.prettytable import _str_block_width
+-DATA_DIR = os.path.join(os.path.dirname(__file__), "data")
++import yatest.common as yc
++DATA_DIR = os.path.join(os.path.dirname(yc.source_path(__file__)), "data")
diff --git a/contrib/python/prettytable/py3/prettytable/__init__.py b/contrib/python/prettytable/py3/prettytable/__init__.py
index f28c03658ab..5876dcfeac4 100644
--- a/contrib/python/prettytable/py3/prettytable/__init__.py
+++ b/contrib/python/prettytable/py3/prettytable/__init__.py
@@ -1,6 +1,6 @@
from __future__ import annotations
-from typing import Any
+__lazy_modules__ = {"prettytable._version", "prettytable.prettytable"}
from ._version import __version__
from .prettytable import ( # noqa: F401
@@ -31,6 +31,11 @@ from .prettytable import ( # noqa: F401
from_mediawiki,
)
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from typing import Any
+
+
__all__ = [
"ALL",
"DEFAULT",
diff --git a/contrib/python/prettytable/py3/prettytable/_version.py b/contrib/python/prettytable/py3/prettytable/_version.py
index b6f2ce2e738..d38ec03f655 100644
--- a/contrib/python/prettytable/py3/prettytable/_version.py
+++ b/contrib/python/prettytable/py3/prettytable/_version.py
@@ -1,5 +1,6 @@
-# file generated by setuptools-scm
+# file generated by vcs-versioning
# don't change, don't track in version control
+from __future__ import annotations
__all__ = [
"__version__",
@@ -10,25 +11,14 @@ __all__ = [
"commit_id",
]
-TYPE_CHECKING = False
-if TYPE_CHECKING:
- from typing import Tuple
- from typing import Union
-
- VERSION_TUPLE = Tuple[Union[int, str], ...]
- COMMIT_ID = Union[str, None]
-else:
- VERSION_TUPLE = object
- COMMIT_ID = object
-
version: str
__version__: str
-__version_tuple__: VERSION_TUPLE
-version_tuple: VERSION_TUPLE
-commit_id: COMMIT_ID
-__commit_id__: COMMIT_ID
+__version_tuple__: tuple[int | str, ...]
+version_tuple: tuple[int | str, ...]
+commit_id: str | None
+__commit_id__: str | None
-__version__ = version = '3.17.0'
-__version_tuple__ = version_tuple = (3, 17, 0)
+__version__ = version = '3.18.0'
+__version_tuple__ = version_tuple = (3, 18, 0)
__commit_id__ = commit_id = None
diff --git a/contrib/python/prettytable/py3/prettytable/colortable.py b/contrib/python/prettytable/py3/prettytable/colortable.py
index 52e1fa20a4c..0f9d417a4e3 100644
--- a/contrib/python/prettytable/py3/prettytable/colortable.py
+++ b/contrib/python/prettytable/py3/prettytable/colortable.py
@@ -2,6 +2,11 @@ from __future__ import annotations
from .prettytable import PrettyTable
+TYPE_CHECKING = False
+if TYPE_CHECKING:
+ from collections.abc import Sequence
+ from typing import ClassVar, Final
+
try:
from colorama import init
@@ -10,10 +15,18 @@ except ImportError:
pass
-RESET_CODE = "\x1b[0m"
+RESET_CODE: Final = "\x1b[0m"
class Theme:
+ default_color: Final[str]
+ vertical_char: Final[str]
+ vertical_color: Final[str]
+ horizontal_char: Final[str]
+ horizontal_color: Final[str]
+ junction_char: Final[str]
+ junction_color: Final[str]
+
def __init__(
self,
default_color: str = "",
@@ -44,50 +57,50 @@ class Theme:
class Themes:
- DEFAULT = Theme()
- DYSLEXIA_FRIENDLY = Theme(
+ DEFAULT: ClassVar[Theme] = Theme()
+ DYSLEXIA_FRIENDLY: ClassVar[Theme] = Theme(
default_color="38;5;223",
vertical_color="38;5;22",
horizontal_color="38;5;22",
junction_color="38;5;58",
)
- EARTH = Theme(
+ EARTH: ClassVar[Theme] = Theme(
default_color="33",
vertical_color="38;5;94",
horizontal_color="38;5;22",
junction_color="38;5;130",
)
- GLARE_REDUCTION = Theme(
+ GLARE_REDUCTION: ClassVar[Theme] = Theme(
default_color="38;5;252",
vertical_color="38;5;240",
horizontal_color="38;5;240",
junction_color="38;5;246",
)
- HIGH_CONTRAST = Theme(
+ HIGH_CONTRAST: ClassVar[Theme] = Theme(
default_color="97",
vertical_color="91",
horizontal_color="94",
junction_color="93",
)
- LAVENDER = Theme(
+ LAVENDER: ClassVar[Theme] = Theme(
default_color="38;5;183",
vertical_color="35",
horizontal_color="38;5;147",
junction_color="38;5;219",
)
- OCEAN = Theme(
+ OCEAN: ClassVar[Theme] = Theme(
default_color="96",
vertical_color="34",
horizontal_color="34",
junction_color="36",
)
- OCEAN_DEEP = Theme(
+ OCEAN_DEEP: ClassVar[Theme] = Theme(
default_color="96",
vertical_color="34",
horizontal_color="36",
junction_color="94",
)
- PASTEL = Theme(
+ PASTEL: ClassVar[Theme] = Theme(
default_color="38;5;223",
vertical_color="38;5;152",
horizontal_color="38;5;187",
@@ -96,7 +109,7 @@ class Themes:
class ColorTable(PrettyTable):
- def __init__(self, field_names=None, **kwargs) -> None:
+ def __init__(self, field_names: Sequence[str] | None = None, **kwargs) -> None:
super().__init__(field_names=field_names, **kwargs)
# TODO: Validate option
diff --git a/contrib/python/prettytable/py3/prettytable/prettytable.py b/contrib/python/prettytable/py3/prettytable/prettytable.py
index 1b6c964652c..d3cdf990fb7 100644
--- a/contrib/python/prettytable/py3/prettytable/prettytable.py
+++ b/contrib/python/prettytable/py3/prettytable/prettytable.py
@@ -1,5 +1,3 @@
-#!/usr/bin/env python
-#
# Copyright (c) 2009-2014, Luke Maurits <[email protected]>
# All rights reserved.
# With contributions from:
@@ -33,22 +31,71 @@
from __future__ import annotations
-import io
-import re
from enum import IntEnum
from functools import lru_cache
-from html.parser import HTMLParser
-from typing import Any, Literal, TypedDict, cast
+from typing import Any, Literal, cast
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Callable, Mapping, Sequence
- from sqlite3 import Cursor
- from typing import Final, TypeAlias
+ from typing import Final, TypeAlias, TypedDict
from _typeshed import SupportsRichComparison
from typing_extensions import Self
+ class OptionsType(TypedDict):
+ title: str | None
+ start: int
+ end: int | None
+ fields: Sequence[str | None] | None
+ header: bool
+ use_header_width: bool
+ border: bool
+ preserve_internal_border: bool
+ sortby: str | None
+ reversesort: bool
+ sort_key: Callable[[RowType], SupportsRichComparison]
+ row_filter: Callable[[RowType], bool]
+ attributes: dict[str, str]
+ format: bool
+ hrules: HRuleStyle
+ vrules: VRuleStyle
+ int_format: str | dict[str, str] | None
+ float_format: str | dict[str, str] | None
+ custom_format: (
+ Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None
+ )
+ min_table_width: int | None
+ max_table_width: int | None
+ padding_width: int
+ left_padding_width: int | None
+ right_padding_width: int | None
+ vertical_char: str
+ horizontal_char: str
+ horizontal_align_char: str
+ header_horizontal_char: str | None
+ junction_char: str
+ header_style: HeaderStyleType
+ xhtml: bool
+ print_empty: bool
+ oldsortslice: bool
+ top_junction_char: str
+ bottom_junction_char: str
+ right_junction_char: str
+ left_junction_char: str
+ top_right_junction_char: str
+ top_left_junction_char: str
+ bottom_right_junction_char: str
+ bottom_left_junction_char: str
+ align: dict[str, AlignType]
+ valign: dict[str, VAlignType]
+ min_width: int | dict[str, int] | None
+ max_width: int | dict[str, int] | None
+ none_format: str | dict[str, str | None] | None
+ escape_header: bool
+ escape_data: bool
+ break_on_hyphens: bool
+
class HRuleStyle(IntEnum):
FRAME = 0
@@ -71,6 +118,7 @@ class TableStyle(IntEnum):
ORGMODE = 14
DOUBLE_BORDER = 15
SINGLE_BORDER = 16
+ RST = 17
RANDOM = 20
@@ -97,63 +145,39 @@ VAlignType: TypeAlias = Literal["t", "m", "b"]
HeaderStyleType: TypeAlias = Literal["cap", "title", "upper", "lower"] | None
-class OptionsType(TypedDict):
- title: str | None
- start: int
- end: int | None
- fields: Sequence[str | None] | None
- header: bool
- use_header_width: bool
- border: bool
- preserve_internal_border: bool
- sortby: str | None
- reversesort: bool
- sort_key: Callable[[RowType], SupportsRichComparison]
- row_filter: Callable[[RowType], bool]
- attributes: dict[str, str]
- format: bool
- hrules: HRuleStyle
- vrules: VRuleStyle
- int_format: str | dict[str, str] | None
- float_format: str | dict[str, str] | None
- custom_format: (
- Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None
- )
- min_table_width: int | None
- max_table_width: int | None
- padding_width: int
- left_padding_width: int | None
- right_padding_width: int | None
- vertical_char: str
- horizontal_char: str
- horizontal_align_char: str
- junction_char: str
- header_style: HeaderStyleType
- xhtml: bool
- print_empty: bool
- oldsortslice: bool
- top_junction_char: str
- bottom_junction_char: str
- right_junction_char: str
- left_junction_char: str
- top_right_junction_char: str
- top_left_junction_char: str
- bottom_right_junction_char: str
- bottom_left_junction_char: str
- align: dict[str, AlignType]
- valign: dict[str, VAlignType]
- min_width: int | dict[str, int] | None
- max_width: int | dict[str, int] | None
- none_format: str | dict[str, str | None] | None
- escape_header: bool
- escape_data: bool
- break_on_hyphens: bool
+class ObservableDict(dict[str, Any]):
+ """A dictionary that notifies a callback when items are set or changed.
+
+ This dictionary subclass allows setting a callback function that will be
+ invoked whenever an item is set or its value changes. This is used to
+ maintain consistency between related format dictionaries.
+ """
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the observable dictionary.
+
+ Arguments:
+ *args: Positional arguments passed to dict.__init__
+ **kwargs: Keyword arguments passed to dict.__init__
+ """
+ super().__init__(*args, **kwargs)
+ self.callback = None
+
+ def __setitem__(self, key: str, value: Any):
+ """Set an item and trigger callback if value changed.
+ Sets the item in the dictionary and calls the callback function
+ (if set) with the key, old value, and new value when the value
+ actually changes.
-# ANSI colour codes
-_re = re.compile(r"\033\[[0-9;]*m|\033\(B")
-# OSC 8 hyperlinks
-_osc8_re = re.compile(r"\033\]8;;.*?\033\\(.*?)\033\]8;;\033\\")
+ Arguments:
+ key: The dictionary key
+ value: The new value to set
+ """
+ old_value = self.get(key)
+ if self.callback is not None and old_value != value:
+ self.callback(key, old_value, value)
+ super().__setitem__(key, value)
@lru_cache
@@ -196,6 +220,7 @@ class PrettyTable:
_vertical_char: str
_horizontal_char: str
_horizontal_align_char: str | None
+ _header_horizontal_char: str | None
_junction_char: str
_top_junction_char: str | None
_bottom_junction_char: str | None
@@ -253,6 +278,8 @@ class PrettyTable:
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
+ header_horizontal_char - single character string used to draw the header
+ separator, or None to use the same as horizontal_char
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 -
@@ -274,7 +301,7 @@ class PrettyTable:
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"
- break_on_hyphens - Whether long lines are broken on hypens or not, default: True
+ break_on_hyphens - Whether long lines are broken on hyphens or not, default: True
"""
self.encoding = kwargs.get("encoding", "UTF-8")
@@ -282,13 +309,6 @@ class PrettyTable:
self._field_names: list[str] = []
self._rows: list[RowType] = []
self._dividers: list[bool] = []
- self.align = {}
- self.valign = {}
- self.max_width = {}
- self.min_width = {}
- self.int_format = {}
- self.float_format = {}
- self.custom_format = {}
self._style = None
# Options
@@ -320,6 +340,7 @@ class PrettyTable:
"vertical_char",
"horizontal_char",
"horizontal_align_char",
+ "header_horizontal_char",
"junction_char",
"header_style",
"xhtml",
@@ -343,7 +364,31 @@ class PrettyTable:
"break_on_hyphens",
]
- self._none_format: dict[str, str | None] = {}
+ self._none_format: dict[str, str | None] = ObservableDict()
+ self._none_format.callback = self._remove_custom_format_callback
+
+ self._int_format: dict[str, str | None] = ObservableDict()
+ self._int_format.callback = self._remove_custom_format_callback
+
+ self._float_format: dict[str, str | None] = ObservableDict()
+ self._float_format.callback = self._remove_custom_format_callback
+
+ self._custom_format: dict[str, Callable[[str, Any], str]] = ObservableDict()
+ self._custom_format.callback = self._custom_format_callback
+
+ self._align: dict[str, str | None] = ObservableDict()
+ self._align[BASE_ALIGN_VALUE] = "c"
+ self._align.callback = self._align_callback
+
+ self._valign: dict[str, str | None] = ObservableDict()
+ self._valign.callback = self._valign_callback
+
+ self._max_width: dict[str, int | None] = ObservableDict()
+ self._max_width.callback = self._max_width_callback
+
+ self._min_width: dict[str, int | None] = ObservableDict()
+ self._min_width.callback = self._min_width_callback
+
self._kwargs = {}
if field_names:
self.field_names = field_names
@@ -414,6 +459,7 @@ class PrettyTable:
self._vertical_char = kwargs["vertical_char"] or "|"
self._horizontal_char = kwargs["horizontal_char"] or "-"
self._horizontal_align_char = kwargs["horizontal_align_char"]
+ self._header_horizontal_char = kwargs["header_horizontal_char"]
self._junction_char = kwargs["junction_char"] or "+"
self._top_junction_char = kwargs["top_junction_char"]
self._bottom_junction_char = kwargs["bottom_junction_char"]
@@ -457,36 +503,20 @@ class PrettyTable:
)
def _justify(self, text: str, width: int, align: AlignType) -> str:
- excess = width - _str_block_width(text)
+ import wcwidth
+
if align == "l":
- return text + excess * " "
+ return wcwidth.ljust(text, width)
elif align == "r":
- return excess * " " + text
+ return wcwidth.rjust(text, width)
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) * " "
+ return wcwidth.center(text, width)
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
+ return len(self._field_names)
else:
raise AttributeError(name)
@@ -494,8 +524,7 @@ class PrettyTable:
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"))
+ setattr(new, f"_{attr}", getattr(self, f"_{attr}"))
if isinstance(index, slice):
for row in self._rows[index]:
new.add_row(row)
@@ -587,6 +616,7 @@ class PrettyTable:
"vertical_char",
"horizontal_char",
"horizontal_align_char",
+ "header_horizontal_char",
"junction_char",
"top_junction_char",
"bottom_junction_char",
@@ -701,7 +731,7 @@ class PrettyTable:
def _validate_function(self, name, val):
try:
- assert hasattr(val, "__call__")
+ assert callable(val)
except AssertionError:
msg = f"Invalid value for {name}. Must be a function."
raise ValueError(msg)
@@ -782,9 +812,7 @@ class PrettyTable:
val - The alternative representation to be used for None values
"""
- if not self._field_names:
- self._none_format = {}
- elif isinstance(val, str):
+ if isinstance(val, str):
for field in self._field_names:
self._none_format[field] = None
self._validate_none_format(val)
@@ -795,8 +823,7 @@ class PrettyTable:
self._validate_none_format(fval)
self._none_format[field] = fval
else:
- for field in self._field_names:
- self._none_format[field] = None
+ self._none_format.clear()
@property
def field_names(self) -> list[str]:
@@ -822,8 +849,8 @@ class PrettyTable:
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)
+ if old_name not in val:
+ self._align.pop(old_name, None)
elif self._align:
for field_name in self._field_names:
self._align[field_name] = self._align[BASE_ALIGN_VALUE]
@@ -833,11 +860,25 @@ class PrettyTable:
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)
+ if old_name not in val:
+ self._valign.pop(old_name, None)
else:
self.valign = "t"
+ def _align_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from align dict and
+ calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_align(new_value)
+
@property
def align(self) -> dict[str, AlignType]:
"""Controls alignment of fields
@@ -849,7 +890,6 @@ class PrettyTable:
@align.setter
def align(self, val: AlignType | dict[str, AlignType] | None) -> None:
if isinstance(val, str):
- self._validate_align(val)
if not self._field_names:
self._align = {BASE_ALIGN_VALUE: val}
else:
@@ -857,14 +897,26 @@ class PrettyTable:
self._align[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_align(fval)
self._align[field] = fval
+ elif self._field_names:
+ for field in self._field_names:
+ self._align[field] = "c"
else:
- if not self._field_names:
- self._align = {BASE_ALIGN_VALUE: "c"}
- else:
- for field in self._field_names:
- self._align[field] = "c"
+ self._align = {BASE_ALIGN_VALUE: "c"}
+
+ def _valign_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from valign dict
+ and calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_valign(new_value)
@property
def valign(self) -> dict[str, VAlignType]:
@@ -877,19 +929,31 @@ class PrettyTable:
@valign.setter
def valign(self, val: VAlignType | dict[str, VAlignType] | None) -> None:
if not self._field_names:
- self._valign = {}
+ self._valign.clear()
if isinstance(val, str):
- self._validate_valign(val)
for field in self._field_names:
self._valign[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_valign(fval)
self._valign[field] = fval
else:
for field in self._field_names:
self._valign[field] = "t"
+ def _max_width_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from max_width dict
+ and calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_option("max_width", new_value)
+
@property
def max_width(self) -> dict[str, int]:
"""Controls maximum width of fields
@@ -901,15 +965,27 @@ class PrettyTable:
@max_width.setter
def max_width(self, val: int | dict[str, int] | None) -> None:
if isinstance(val, int):
- self._validate_option("max_width", val)
for field in self._field_names:
self._max_width[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_option("max_width", fval)
self._max_width[field] = fval
else:
- self._max_width = {}
+ self._max_width.clear()
+
+ def _min_width_callback(self, field_name, old_value, new_value):
+ """Callback to call validators if dict attrs are modified.
+
+ This callback is triggered when a field is modified from min_width dict
+ and calls the validator for the new value.
+
+ Arguments:
+ field_name: Name of the field being modified
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+
+ """
+ self._validate_option("min_width", new_value)
@property
def min_width(self) -> dict[str, int]:
@@ -922,15 +998,13 @@ class PrettyTable:
@min_width.setter
def min_width(self, val: int | dict[str, int] | None) -> None:
if isinstance(val, int):
- self._validate_option("min_width", val)
for field in self._field_names:
self._min_width[field] = val
elif isinstance(val, dict) and val:
for field, fval in val.items():
- self._validate_option("min_width", fval)
self._min_width[field] = fval
else:
- self._min_width = {}
+ self._min_width.clear()
@property
def min_table_width(self) -> int | None:
@@ -1180,7 +1254,7 @@ class PrettyTable:
self._validate_option("int_format", fval)
self._int_format[field] = fval
else:
- self._int_format = {}
+ self._int_format.clear()
@property
def float_format(self) -> dict[str, str]:
@@ -1201,7 +1275,41 @@ class PrettyTable:
self._validate_option("float_format", fval)
self._float_format[field] = fval
else:
- self._float_format = {}
+ self._float_format.clear()
+
+ def _remove_custom_format_callback(self, field_name, old_value, new_value):
+ """Callback to remove custom format when a field is removed from format dicts.
+
+ This callback is triggered when a field is removed from _none_format,
+ _int_format, or _float_format dictionaries, and removes the corresponding
+ entry from _custom_format if it exists.
+
+ Arguments:
+ field_name: Name of the field being removed
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+ """
+ if field_name in self._custom_format:
+ del self._custom_format[field_name]
+
+ def _custom_format_callback(self, field_name, old_value, new_value):
+ """Callback to remove std formats when a field is removed from custom_format.
+
+ This callback is triggered when a field is removed from _custom_format
+ dictionary, and removes the corresponding entries from _float_format,
+ _int_format, and _none_format if they exist.
+
+ Arguments:
+ field_name: Name of the field being removed
+ old_value: Previous value (unused)
+ new_value: New value (unused)
+ """
+ if field_name in self._float_format:
+ del self._float_format[field_name]
+ if field_name in self._int_format:
+ del self._int_format[field_name]
+ if field_name in self._none_format:
+ del self._none_format[field_name]
@property
def custom_format(self) -> dict[str, Callable[[str, Any], str]]:
@@ -1216,13 +1324,24 @@ class PrettyTable:
self,
val: Callable[[str, Any], str] | dict[str, Callable[[str, Any], str]] | None,
):
+ """Set custom format for columns using callable functions.
+
+ Arguments:
+ val: Can be:
+ - None: Clears all custom formats
+ - dict: Dictionary mapping field names to callable functions
+ - callable: A single callable function applied to all fields
+
+ Raises:
+ TypeError: If val is not None, a dict, or a callable
+ """
if val is None:
- self._custom_format = {}
+ self._custom_format.clear()
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__"):
+ for field, fval in val.items():
+ self._validate_function(f"custom_value.{field}", fval)
+ self._custom_format[field] = fval
+ elif callable(val):
self._validate_function("custom_value", val)
for field in self._field_names:
self._custom_format[field] = val
@@ -1309,7 +1428,7 @@ class PrettyTable:
Arguments:
horizontal_align_char - single character string used to indicate alignment"""
- return self._bottom_left_junction_char or self.junction_char
+ return self._horizontal_align_char or self.junction_char
@horizontal_align_char.setter
def horizontal_align_char(self, val: str) -> None:
@@ -1318,6 +1437,23 @@ class PrettyTable:
self._horizontal_align_char = val
@property
+ def header_horizontal_char(self) -> str | None:
+ """The character used when printing the header separator line
+
+ Arguments:
+
+ header_horizontal_char - single character string used to draw the header
+ separator, or None to use the same as horizontal_char"""
+ return self._header_horizontal_char
+
+ @header_horizontal_char.setter
+ def header_horizontal_char(self, val: str | None) -> None:
+ if val is not None:
+ val = str(val)
+ self._validate_option("header_horizontal_char", val)
+ self._header_horizontal_char = val
+
+ @property
def junction_char(self) -> str:
"""The character used when printing table borders to draw line junctions
@@ -1558,7 +1694,7 @@ class PrettyTable:
options[option] = kwargs[option]
else:
options[option] = getattr(self, option)
- return cast(OptionsType, options)
+ return cast("OptionsType", options)
##############################
# PRESET STYLE LOGIC #
@@ -1579,6 +1715,8 @@ class PrettyTable:
self._set_double_border_style()
elif style == TableStyle.SINGLE_BORDER:
self._set_single_border_style()
+ elif style == TableStyle.RST:
+ self._set_rst_style()
elif style == TableStyle.RANDOM:
self._set_random_style()
elif style != TableStyle.DEFAULT:
@@ -1599,6 +1737,19 @@ class PrettyTable:
self.junction_char = "|"
self._horizontal_align_char = ":"
+ def _set_rst_style(self) -> None:
+ self.header = True
+ self.border = True
+ self._hrules = HRuleStyle.ALL
+ self.padding_width = 1
+ self.left_padding_width = 1
+ self.right_padding_width = 1
+ self.vertical_char = "|"
+ self.junction_char = "+"
+ self.horizontal_char = "-"
+ self._horizontal_align_char = None
+ self.header_horizontal_char = "="
+
def _set_default_style(self) -> None:
self.header = True
self.border = True
@@ -1610,6 +1761,7 @@ class PrettyTable:
self.vertical_char = "|"
self.horizontal_char = "-"
self._horizontal_align_char = None
+ self.header_horizontal_char = None
self.junction_char = "+"
self._top_junction_char = None
self._bottom_junction_char = None
@@ -1866,12 +2018,24 @@ class PrettyTable:
return (f"%{self._float_format[field]}f") % value
formatter = self._custom_format.get(field, (lambda f, v: str(v)))
- return formatter(field, value)
+ # PrettyTable is unaware of a terminal's tabstops, and it does not know at
+ # what specific location of the screen it will be displayed, so it also cannot
+ # calculate tabstop positions or width: A '\t' character is variable-width,
+ # depending on the location of the screen it is displayed.
+ #
+ # Although wcwidth library functions like width() do measure tab control
+ # character as a width of 8, it would require PrettyTable to display the left
+ # margin of the table's contents to begin "at the tabstop" for the table
+ # contents and dividers to line up correctly.
+ #
+ # PrettyTable is "screen unaware", so it is best to alter tabstops to a fixed
+ # width, to allow same-width display anywhere on the screen.
+ return formatter(field, value).expandtabs()
def _compute_table_width(self, options) -> int:
if options["vrules"] == VRuleStyle.FRAME:
table_width = 2
- if options["vrules"] == VRuleStyle.ALL:
+ elif options["vrules"] == VRuleStyle.ALL:
table_width = 1
else:
table_width = 0
@@ -1932,7 +2096,10 @@ class PrettyTable:
# Are we under min_table_width or title width?
if self._min_table_width or options["title"]:
if options["title"]:
- title_width = _str_block_width(options["title"]) + per_col_padding
+ title_width = (
+ max(_str_block_width(line) for line in options["title"].split("\n"))
+ + per_col_padding
+ )
if options["vrules"] in (VRuleStyle.FRAME, VRuleStyle.ALL):
title_width += 2
else:
@@ -2058,7 +2225,8 @@ class PrettyTable:
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
+ header_horizontal_char - single character string used to draw the header
+ separator, or None to use the same as horizontal_char
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 -
@@ -2103,7 +2271,10 @@ class PrettyTable:
# Add title
title = options["title"] or self._title
if title:
- lines.append(self._stringify_title(title, options))
+ if self._style != TableStyle.MARKDOWN:
+ lines.append(self._stringify_title(title, options))
+ else:
+ lines.extend([f"**{line}**" for line in title.split("\n")] + [""])
# Add header or top of border
if options["header"]:
@@ -2158,12 +2329,12 @@ class PrettyTable:
return ""
lpad, rpad = self._get_padding_widths(options)
if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
- bits = [options[where + "left_junction_char"]] # type: ignore[literal-required]
+ bits = [options[f"{where}left_junction_char"]] # type: ignore[literal-required]
else:
bits = [options["horizontal_char"]]
# For tables with no data or fieldnames
if not self._field_names:
- bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
+ bits.append(options[f"{where}right_junction_char"]) # type: ignore[literal-required]
return "".join(bits)
for field, width in zip(self._field_names, self._widths):
if options["fields"] and field not in options["fields"]:
@@ -2180,12 +2351,12 @@ class PrettyTable:
bits.append(line)
if options["vrules"] == VRuleStyle.ALL:
- bits.append(options[where + "junction_char"]) # type: ignore[literal-required]
+ bits.append(options[f"{where}junction_char"]) # type: ignore[literal-required]
else:
bits.append(options["horizontal_char"])
if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME):
bits.pop()
- bits.append(options[where + "right_junction_char"]) # type: ignore[literal-required]
+ bits.append(options[f"{where}right_junction_char"]) # type: ignore[literal-required]
if options["preserve_internal_border"] and not options["border"]:
bits = bits[1:-1]
@@ -2194,7 +2365,6 @@ class PrettyTable:
def _stringify_title(self, title: str, options: OptionsType) -> str:
lines: list[str] = []
- lpad, rpad = self._get_padding_widths(options)
if options["border"]:
if options["vrules"] == VRuleStyle.ALL:
options["vrules"] = VRuleStyle.FRAME
@@ -2202,21 +2372,19 @@ class PrettyTable:
options["vrules"] = VRuleStyle.ALL
elif options["vrules"] == VRuleStyle.FRAME:
lines.append(self._stringify_hrule(options, "top_"))
- bits: list[str] = []
endpoint = (
options["vertical_char"]
if options["vrules"] in (VRuleStyle.ALL, VRuleStyle.FRAME)
and options["border"]
else " "
)
- bits.append(endpoint)
- title = " " * lpad + title + " " * rpad
lpad, rpad = self._get_padding_widths(options)
sum_widths = sum([n + lpad + rpad + 1 for n in self._widths])
-
- bits.append(self._justify(title, sum_widths - 1, "c"))
- bits.append(endpoint)
- lines.append("".join(bits))
+ for title_line in title.split("\n"):
+ padded = " " * lpad + title_line + " " * rpad
+ lines.append(
+ endpoint + self._justify(padded, sum_widths - 1, "c") + endpoint
+ )
return "\n".join(lines)
def _stringify_header(self, options: OptionsType) -> str:
@@ -2287,11 +2455,18 @@ class PrettyTable:
"hrules"
] != HRuleStyle.NONE:
bits.append("\n")
- bits.append(self._hrule)
+ if options["header_horizontal_char"]:
+ header_options = {
+ **options,
+ "horizontal_char": options["header_horizontal_char"],
+ }
+ bits.append(self._stringify_hrule(header_options)) # type: ignore[arg-type]
+ else:
+ bits.append(self._hrule)
return "".join(bits)
def _stringify_row(self, row: list[str], options: OptionsType, hrule: str) -> str:
- import textwrap
+ import wcwidth
for index, field, value, width in zip(
range(len(row)), self._field_names, row, self._widths
@@ -2306,19 +2481,17 @@ class PrettyTable:
):
line = none_val
if _str_block_width(line) > width:
- line = textwrap.fill(
- line, width, break_on_hyphens=options["break_on_hyphens"]
+ line = "\n".join(
+ wcwidth.wrap(
+ line, width, break_on_hyphens=options["break_on_hyphens"]
+ )
)
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
+ row_height = max(_get_size(c)[1] for c in row)
bits: list[list[str]] = []
lpad, rpad = self._get_padding_widths(options)
@@ -2382,6 +2555,18 @@ class PrettyTable:
return "\n".join(bits_str)
def paginate(self, page_length: int = 58, line_break: str = "\f", **kwargs) -> str:
+ """Return string representation of table split into pages.
+
+ Arguments:
+
+ page_length - number of rows per page (default: 58)
+ line_break - string used to separate pages (default: "\f" form feed)
+ **kwargs - additional keyword arguments passed to get_string() method,
+ such as title, fields, header, border, etc.
+
+ The table is split into pages of the specified length, with each page
+ separated by the line_break character. All formatting options available
+ in get_string() can be used via kwargs."""
pages: list[str] = []
kwargs["start"] = kwargs.get("start", 0)
true_end = kwargs.get("end", self.rowcount)
@@ -2406,6 +2591,7 @@ class PrettyTable:
delimiter as a csv.writer keyword argument.
"""
import csv
+ import io
options = self._get_options(kwargs)
csv_options = {
@@ -2422,7 +2608,7 @@ class PrettyTable:
else:
csv_writer.writerow(self._field_names)
- rows = self._get_rows(options)
+ rows = self._format_rows(self._get_rows(options))
if options["fields"]:
rows = [
[d for f, d in zip(self._field_names, row) if f in options["fields"]]
@@ -2465,17 +2651,12 @@ class PrettyTable:
objects.append(self.field_names)
rows = self._get_rows(options)
if options["fields"]:
- for row in rows:
- objects.append(
- {
- f: d
- for f, d in zip(self._field_names, row)
- if f in options["fields"]
- }
- )
+ objects.extend(
+ {f: d for f, d in zip(self._field_names, row) if f in options["fields"]}
+ for row in rows
+ )
else:
- for row in rows:
- objects.append(dict(zip(self._field_names, row)))
+ objects.extend(dict(zip(self._field_names, row)) for row in rows)
return json.dumps(objects, **json_options)
@@ -2518,7 +2699,7 @@ class PrettyTable:
styling options (True or False)
escape_data - escapes the text within a data field (True or False)
xhtml - print <br/> tags if True, <br> tags if False
- break_on_hyphens - Whether long lines are broken on hypens or not, default: True
+ break_on_hyphens - Whether long lines are broken on hyphens or not, default: True
"""
options = self._get_options(kwargs)
@@ -2549,7 +2730,8 @@ class PrettyTable:
# Title
title = options["title"] or self._title
if title:
- lines.append(f" <caption>{escape(title)}</caption>")
+ caption = escape(title).replace("\n", linebreak)
+ lines.append(f" <caption>{caption}</caption>")
# Headers
if options["header"]:
@@ -2635,7 +2817,8 @@ class PrettyTable:
# Title
title = options["title"] or self._title
if title:
- lines.append(f" <caption>{escape(title)}</caption>")
+ caption = escape(title).replace("\n", linebreak)
+ lines.append(f" <caption>{caption}</caption>")
# Headers
if options["header"]:
@@ -2661,15 +2844,14 @@ class PrettyTable:
lines.append(" <tbody>")
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows)
- aligns: list[str] = []
- valigns: list[str] = []
- 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]]
- )
+ aligns: list[str] = [
+ {"l": "left", "r": "right", "c": "center"}[self._align[field]]
+ for field in self._field_names
+ ]
+ valigns: list[str] = [
+ {"t": "top", "m": "middle", "b": "bottom"}[self._valign[field]]
+ for field in self._field_names
+ ]
for row in formatted_rows:
lines.append(" <tr>")
for field, datum, align, valign in zip(
@@ -2734,7 +2916,6 @@ class PrettyTable:
def _get_simple_latex_string(self, options: OptionsType) -> str:
lines: list[str] = []
- wanted_fields = []
if options["fields"]:
wanted_fields = [
field for field in self._field_names if field in options["fields"]
@@ -2744,12 +2925,12 @@ class PrettyTable:
alignments = "".join([self._align[field] for field in wanted_fields])
- begin_cmd = f"\\begin{{tabular}}{{{alignments}}}"
+ begin_cmd = rf"\begin{{tabular}}{{{alignments}}}"
lines.append(begin_cmd)
# Headers
if options["header"]:
- lines.append(" & ".join(wanted_fields) + " \\\\")
+ lines.append(" & ".join(wanted_fields) + r" \\")
# Data
rows = self._get_rows(options)
@@ -2758,9 +2939,9 @@ class PrettyTable:
wanted_data = [
d for f, d in zip(self._field_names, row) if f in wanted_fields
]
- lines.append(" & ".join(wanted_data) + " \\\\")
+ lines.append(" & ".join(wanted_data) + r" \\")
- lines.append("\\end{tabular}")
+ lines.append(r"\end{tabular}")
return "\r\n".join(lines)
@@ -2786,40 +2967,39 @@ class PrettyTable:
VRuleStyle.ALL,
VRuleStyle.FRAME,
]:
- alignment_str = "|" + alignment_str + "|"
+ alignment_str = f"|{alignment_str}|"
- begin_cmd = f"\\begin{{tabular}}{{{alignment_str}}}"
+ begin_cmd = rf"\begin{{tabular}}{{{alignment_str}}}"
lines.append(begin_cmd)
if options["border"] and options["hrules"] in [
HRuleStyle.ALL,
HRuleStyle.FRAME,
]:
- lines.append("\\hline")
+ lines.append(r"\hline")
# Headers
if options["header"]:
- lines.append(" & ".join(wanted_fields) + " \\\\")
+ lines.append(" & ".join(wanted_fields) + r" \\")
if (options["border"] or options["preserve_internal_border"]) and options[
"hrules"
] in [HRuleStyle.ALL, HRuleStyle.HEADER]:
- lines.append("\\hline")
+ lines.append(r"\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) + " \\\\")
+ lines.append(" & ".join(wanted_data) + r" \\")
if options["border"] and options["hrules"] == HRuleStyle.ALL:
- lines.append("\\hline")
+ lines.append(r"\hline")
if options["border"] and options["hrules"] == HRuleStyle.FRAME:
- lines.append("\\hline")
+ lines.append(r"\hline")
- lines.append("\\end{tabular}")
+ lines.append(r"\end{tabular}")
return "\r\n".join(lines)
@@ -2845,42 +3025,42 @@ class PrettyTable:
options = self._get_options(kwargs)
lines: list[str] = []
- if (
- options.get("attributes")
- and isinstance(options["attributes"], dict)
- and options["attributes"]
- ):
- attr_str = " ".join(f'{k}="{v}"' for k, v in options["attributes"].items())
- lines.append("{| " + attr_str)
+ attributes_option = options.get("attributes")
+ if attributes_option and isinstance(attributes_option, dict):
+ attr_str = " ".join(f'{k}="{v}"' for k, v in attributes_option.items())
+ lines.append(f"{{| {attr_str}")
else:
lines.append('{| class="wikitable"')
caption = options.get("title", self._title)
if caption:
- lines.append("|+ " + caption)
+ lines.append(f"|+ {caption}")
+ fields_option = options.get("fields")
if options.get("header"):
lines.append("|-")
- headers = []
- fields_option = options.get("fields")
- for field in self._field_names:
- if fields_option is not None and field not in fields_option:
- continue
- headers.append(field)
+ if fields_option is None:
+ headers = self._field_names
+ else:
+ headers = [
+ field for field in self._field_names if field in fields_option
+ ]
if headers:
header_line = " !! ".join(headers)
- lines.append("! " + header_line)
+ lines.append(f"! {header_line}")
rows = self._get_rows(options)
formatted_rows = self._format_rows(rows)
for row in formatted_rows:
lines.append("|-")
- cells = []
- fields_option = options.get("fields")
- for field, cell in zip(self._field_names, row):
- if fields_option is not None and field not in fields_option:
- continue
- cells.append(cell)
+ if fields_option is None:
+ cells = row
+ else:
+ cells = [
+ cell
+ for field, cell in zip(self._field_names, row)
+ if field in fields_option
+ ]
if cells:
lines.append("| " + " || ".join(cells))
@@ -2897,8 +3077,7 @@ class PrettyTable:
def _str_block_width(val: str) -> int:
import wcwidth
- val = _osc8_re.sub(r"\1", val)
- return wcwidth.wcswidth(_re.sub("", val))
+ return wcwidth.width(val)
##############################
@@ -2941,7 +3120,7 @@ def from_csv(fp, field_names: Sequence[str] | None = None, **kwargs) -> PrettyTa
return table
-def from_db_cursor(cursor: Cursor, **kwargs) -> PrettyTable | None:
+def from_db_cursor(cursor: Any, **kwargs) -> PrettyTable | None:
if cursor.description:
table = PrettyTable(**kwargs)
table.field_names = [col[0] for col in cursor.description]
@@ -2963,77 +3142,101 @@ def from_json(json_string: str | bytes, **kwargs) -> PrettyTable:
return table
-class TableHandler(HTMLParser):
- def __init__(self, **kwargs) -> None:
- HTMLParser.__init__(self)
- self.kwargs = kwargs
- self.tables: list[PrettyTable] = []
- self.last_row: list[str] = []
- self.rows: list[tuple[list[str], bool]] = []
- self.max_row_width = 0
- self.active: str | None = None
- self.last_content = ""
- self.is_last_row_header = False
- self.colspan = 0
+def _make_table_handler():
+ from html.parser import HTMLParser
- def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> 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) # type: ignore[arg-type]
+ class _TableHandler(HTMLParser):
+ def __init__(self, **kwargs) -> None:
+ HTMLParser.__init__(self)
+ self.kwargs = kwargs
+ self.tables: list[PrettyTable] = []
+ self.last_row: list[str] = []
+ self.rows: list[tuple[list[str], bool]] = []
+ self.max_row_width = 0
+ self.active: str | None = None
+ self.last_content = ""
+ self.is_last_row_header = False
+ self.colspan = 0
- def handle_endtag(self, tag: str) -> None:
- if tag in ["th", "td"]:
- stripped_content = self.last_content.strip()
- self.last_row.append(stripped_content)
- if self.colspan:
- for _ in range(1, self.colspan):
- self.last_row.append("")
- self.colspan = 0
+ def handle_starttag(
+ self, tag: str, attrs: list[tuple[str, str | None]]
+ ) -> 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) # type: ignore[arg-type]
- 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_endtag(self, tag: str) -> None:
+ if tag in ["th", "td"]:
+ stripped_content = self.last_content.strip()
+ self.last_row.append(stripped_content)
+ if self.colspan:
+ for _ in range(1, self.colspan):
+ self.last_row.append("")
+ self.colspan = 0
- def handle_data(self, data: str) -> None:
- self.last_content += data
+ 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 generate_table(self, rows: list[tuple[list[str], bool]]) -> PrettyTable:
- """
- 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("-")
+ def handle_data(self, data: str) -> None:
+ self.last_content += data
- if row[1]:
- self.make_fields_unique(row[0])
- table.field_names = row[0]
- else:
- table.add_row(row[0])
- return table
+ def generate_table(self, rows: list[tuple[list[str], bool]]) -> PrettyTable:
+ """
+ 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("-")
- def make_fields_unique(self, fields: list[str]) -> None:
- """
- iterates over the row and make each field unique
- """
- for i in range(len(fields)):
- for j in range(i + 1, len(fields)):
- if fields[i] == fields[j]:
- fields[j] += "'"
+ 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: list[str]) -> None:
+ """
+ iterates over the row and make each field unique
+ """
+ for i in range(len(fields)):
+ for j in range(i + 1, len(fields)):
+ if fields[i] == fields[j]:
+ fields[j] += "'"
+
+ return _TableHandler
+
+
+class TableHandler:
+ """Deprecated: use from_html or from_html_one instead."""
+
+ def __init__(self, **kwargs) -> None:
+ import warnings
+
+ warnings.warn(
+ "TableHandler is deprecated and will be removed in a future release. "
+ "Use from_html() or from_html_one() instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ cls = _make_table_handler()
+ self.__class__ = cls
+ cls.__init__(self, **kwargs)
def from_html(html_code: str, **kwargs) -> list[PrettyTable]:
@@ -3041,8 +3244,7 @@ def from_html(html_code: str, **kwargs) -> list[PrettyTable]:
Generates a list of PrettyTables from a string of HTML code. Each <table> in
the HTML becomes one PrettyTable object.
"""
-
- parser = TableHandler(**kwargs)
+ parser = _make_table_handler()(**kwargs)
parser.feed(html_code)
return parser.tables
@@ -3089,11 +3291,11 @@ def from_mediawiki(wiki_text: str, **kwargs) -> PrettyTable:
if line.startswith("|+"):
continue
if line.startswith("!"):
- header = [cell.strip() for cell in re.split(r"\s*!!\s*", line[1:])]
+ header = [cell.strip() for cell in line[1:].split("!!")]
table.field_names = header
continue
if line.startswith("|"):
- row_data = [cell.strip() for cell in re.split(r"\s*\|\|\s*", line[1:])]
+ row_data = [cell.strip() for cell in line[1:].split("||")]
rows.append(row_data)
continue
@@ -3129,4 +3331,20 @@ def _warn_deprecation(name: str, module_globals: dict[str, Any]) -> Any:
def __getattr__(name: str) -> Any:
+ if name == "OptionsType":
+ import warnings
+ from typing import TypedDict
+
+ warnings.warn(
+ "OptionsType is deprecated and will be removed in a future release",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+
+ class OptionsType(TypedDict):
+ pass
+
+ globals()[name] = OptionsType
+ return OptionsType
+
return _warn_deprecation(name, module_globals=globals())
diff --git a/contrib/python/prettytable/py3/tests/data/table_align_center.txt b/contrib/python/prettytable/py3/tests/data/table_align_center.txt
new file mode 100644
index 00000000000..c25becb19d7
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_align_center.txt
@@ -0,0 +1,6 @@
++------+
+| Name |
++------+
+| 👨‍👩‍👧 |
+| Hi |
++------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_align_left.txt b/contrib/python/prettytable/py3/tests/data/table_align_left.txt
new file mode 100644
index 00000000000..ad65ed94378
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_align_left.txt
@@ -0,0 +1,6 @@
++------+
+| Name |
++------+
+| 👨‍👩‍👧 |
+| Hi |
++------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_align_right.txt b/contrib/python/prettytable/py3/tests/data/table_align_right.txt
new file mode 100644
index 00000000000..3e1e73875d0
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_align_right.txt
@@ -0,0 +1,6 @@
++------+
+| Name |
++------+
+| 👨‍👩‍👧 |
+| Hi |
++------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt b/contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt
new file mode 100644
index 00000000000..4dc5d86f520
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_ansi_colors.txt
@@ -0,0 +1,7 @@
++--------+-------+
+| Status | Count |
++--------+-------+
+| OK | 10 |
+| Failed | 2 |
+| Normal | 5 |
++--------+-------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_cjk.txt b/contrib/python/prettytable/py3/tests/data/table_cjk.txt
new file mode 100644
index 00000000000..e928b529282
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_cjk.txt
@@ -0,0 +1,6 @@
++------+-------+
+| CJK | Width |
++------+-------+
+| 中文 | 4 |
+| Test | 4 |
++------+-------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_combining_chars.txt b/contrib/python/prettytable/py3/tests/data/table_combining_chars.txt
new file mode 100644
index 00000000000..c0da2c2b05f
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_combining_chars.txt
@@ -0,0 +1,6 @@
++------+------+
+| Word | Lang |
++------+------+
+| café | FR |
+| cafe | EN |
++------+------+
diff --git a/contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt b/contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt
new file mode 100644
index 00000000000..2a39e8164f3
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/data/table_complex_emoji.txt
@@ -0,0 +1,7 @@
++-------+--------+
+| Emoji | Name |
++-------+--------+
+| 👨‍👩‍👧 | Family |
+| 🇺🇸 | USA |
+| Hi | Text |
++-------+--------+
diff --git a/contrib/python/prettytable/py3/tests/test_colortable.py b/contrib/python/prettytable/py3/tests/test_colortable.py
index 40a287169a0..5f25acf7c6b 100644
--- a/contrib/python/prettytable/py3/tests/test_colortable.py
+++ b/contrib/python/prettytable/py3/tests/test_colortable.py
@@ -157,7 +157,7 @@ class TestColorTableRendering:
table.header = with_header
table.add_row([1, 2, 3, 4, 5, 6])
- expected = header_str + "\n" + body_str + "\x1b[0m"
+ expected = f"{header_str}\n{body_str}\x1b[0m"
result = str(table)
assert expected == result
@@ -170,7 +170,7 @@ class TestColorTableRendering:
table.title = "Theme Test"
table.add_row([1, 2, 3, 4, 5, 6])
- for theme_name, theme in vars(Themes).items():
+ for theme in vars(Themes).values():
if isinstance(theme, Theme):
table.theme = theme
result = str(table)
diff --git a/contrib/python/prettytable/py3/tests/test_html.py b/contrib/python/prettytable/py3/tests/test_html.py
index 0c3ef03a21d..acfd31c2a00 100644
--- a/contrib/python/prettytable/py3/tests/test_html.py
+++ b/contrib/python/prettytable/py3/tests/test_html.py
@@ -26,9 +26,7 @@ class TestHtmlConstructor:
class TestHtmlOutput:
def test_html_output(self, helper_table: PrettyTable) -> None:
result = helper_table.get_html_string()
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -60,13 +58,10 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_formatted(self, helper_table: PrettyTable) -> None:
result = helper_table.get_html_string(format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -97,17 +92,14 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_with_title(self, helper_table: PrettyTable) -> None:
helper_table.title = "Title & Title"
result = helper_table.get_html_string(
attributes={"bgcolor": "red", "a<b": "1<2"}
)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table bgcolor="red" a&lt;b="1&lt;2">
<caption>Title &amp; Title</caption>
<thead>
@@ -140,16 +132,13 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_formatted_with_title(self, helper_table: PrettyTable) -> None:
helper_table.title = "Title & Title"
result = helper_table.get_html_string(
attributes={"bgcolor": "red", "a<b": "1<2"}, format=True
)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols" bgcolor="red" a&lt;b="1&lt;2">
<caption>Title &amp; Title</caption>
<thead>
@@ -181,8 +170,7 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_without_escaped_header(
self, empty_helper_table: PrettyTable
@@ -194,9 +182,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=False)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -210,7 +196,6 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_without_escaped_data(
self, empty_helper_table: PrettyTable
@@ -224,9 +209,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=False)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -246,7 +229,6 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_with_escaped_header(
self, empty_helper_table: PrettyTable
@@ -258,9 +240,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -274,7 +254,6 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_html_output_with_escaped_data(
self, empty_helper_table: PrettyTable
@@ -288,9 +267,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -309,8 +286,7 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_without_escaped_header(
self, empty_helper_table: PrettyTable
@@ -322,9 +298,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=False, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -337,8 +311,7 @@ class TestHtmlOutput:
<tbody>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_without_escaped_data(
self, empty_helper_table: PrettyTable
@@ -352,9 +325,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=False, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -373,8 +344,7 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_with_escaped_header(
self, empty_helper_table: PrettyTable
@@ -386,9 +356,7 @@ class TestHtmlOutput:
"<a href='#'>Field 3</a>",
]
result = empty_helper_table.get_html_string(escape_header=True, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -401,8 +369,7 @@ class TestHtmlOutput:
<tbody>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
def test_html_output_formatted_with_escaped_data(
self, empty_helper_table: PrettyTable
@@ -416,9 +383,7 @@ class TestHtmlOutput:
]
)
result = empty_helper_table.get_html_string(escape_data=True, format=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -437,8 +402,47 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".strip()
+
+ def test_escape_ctor(self) -> None:
+ table = PrettyTable(["<>", "&"], escape_data=False, escape_header=False)
+ table.add_row(["<>", "&"])
+ assert table.get_html_string().strip() == """
+<table>
+ <thead>
+ <tr>
+ <th><></th>
+ <th>&</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><></td>
+ <td>&</td>
+ </tr>
+ </tbody>
+</table>""".strip()
+
+ def test_escape_setter(self) -> None:
+ table = PrettyTable(["<>", "&"])
+ table.escape_data = False
+ table.escape_header = False
+ table.add_row(["<>", "&"])
+ assert table.get_html_string().strip() == """
+<table>
+ <thead>
+ <tr>
+ <th><></th>
+ <th>&</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td><></td>
+ <td>&</td>
+ </tr>
+ </tbody>
+</table>""".strip()
def test_table_formatted_html_autoindex(self) -> None:
"""See also #199"""
@@ -450,9 +454,7 @@ class TestHtmlOutput:
table.format = True
table.add_autoindex("I")
- assert (
- table.get_html_string().strip()
- == """
+ assert table.get_html_string().strip() == """
<table frame="box" rules="cols">
<thead>
<tr>
@@ -482,17 +484,14 @@ class TestHtmlOutput:
<td style="padding-left: 1em; padding-right: 1em; text-align: center; vertical-align: top">value 207</td>
</tr>
</tbody>
-</table>""".strip() # noqa: E501
- )
+</table>""".strip()
def test_internal_border_preserved_html(self, helper_table: PrettyTable) -> None:
helper_table.format = True
helper_table.border = False
helper_table.preserve_internal_border = True
- assert (
- helper_table.get_html_string().strip()
- == """
+ assert helper_table.get_html_string().strip() == """
<table rules="cols">
<thead>
<tr>
@@ -523,17 +522,14 @@ class TestHtmlOutput:
</tr>
</tbody>
</table>
-""".strip() # noqa: E501
- )
+""".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=HRuleStyle.ALL)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -553,16 +549,13 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
def test_break_line_xhtml(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=HRuleStyle.ALL, xhtml=True)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
<table>
<thead>
<tr>
@@ -582,4 +575,3 @@ class TestHtmlOutput:
</tbody>
</table>
""".strip()
- )
diff --git a/contrib/python/prettytable/py3/tests/test_json.py b/contrib/python/prettytable/py3/tests/test_json.py
index 0e2b3659397..c04b72c2876 100644
--- a/contrib/python/prettytable/py3/tests/test_json.py
+++ b/contrib/python/prettytable/py3/tests/test_json.py
@@ -6,9 +6,7 @@ from prettytable import PrettyTable, from_json
class TestJSONOutput:
def test_json_output(self, helper_table: PrettyTable) -> None:
result = helper_table.get_json_string()
- assert (
- result.strip()
- == """
+ assert result.strip() == """
[
[
"",
@@ -35,12 +33,9 @@ class TestJSONOutput:
"Field 3": "value9"
}
]""".strip()
- )
options = {"fields": ["Field 1", "Field 3"]}
result = helper_table.get_json_string(**options)
- assert (
- result.strip()
- == """
+ assert result.strip() == """
[
[
"Field 1",
@@ -59,7 +54,6 @@ class TestJSONOutput:
"Field 3": "value9"
}
]""".strip()
- )
def test_json_output_options(self, helper_table: PrettyTable) -> None:
result = helper_table.get_json_string(
diff --git a/contrib/python/prettytable/py3/tests/test_mediawiki.py b/contrib/python/prettytable/py3/tests/test_mediawiki.py
index 7684b617324..f472440a53a 100644
--- a/contrib/python/prettytable/py3/tests/test_mediawiki.py
+++ b/contrib/python/prettytable/py3/tests/test_mediawiki.py
@@ -7,9 +7,7 @@ from prettytable import PrettyTable, from_mediawiki
class TestMediaWikiOutput:
def test_mediawiki_output(self, helper_table: PrettyTable) -> None:
- assert (
- helper_table.get_mediawiki_string(header=True).strip()
- == """
+ assert helper_table.get_mediawiki_string(header=True).strip() == """
{| class="wikitable"
|-
! !! Field 1 !! Field 2 !! Field 3
@@ -21,12 +19,9 @@ class TestMediaWikiOutput:
| 7 || value 7 || value8 || value9
|}
""".strip()
- )
def test_mediawiki_output_without_header(self, helper_table: PrettyTable) -> None:
- assert (
- helper_table.get_mediawiki_string(header=False).strip()
- == """
+ assert helper_table.get_mediawiki_string(header=False).strip() == """
{| class="wikitable"
|-
| 1 || value 1 || value2 || value3
@@ -36,7 +31,6 @@ class TestMediaWikiOutput:
| 7 || value 7 || value8 || value9
|}
""".strip()
- )
def test_mediawiki_output_with_caption(self, helper_table: PrettyTable) -> None:
assert (
diff --git a/contrib/python/prettytable/py3/tests/test_prettytable.py b/contrib/python/prettytable/py3/tests/test_prettytable.py
index 7d85fad86f1..01bf4ac0cb5 100644
--- a/contrib/python/prettytable/py3/tests/test_prettytable.py
+++ b/contrib/python/prettytable/py3/tests/test_prettytable.py
@@ -52,107 +52,112 @@ class TestNoneOption:
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()
- == """
+ 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()
- == """
+ 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", "Field 4"], none_format="N/A"
)
table.add_row(["value 1", None, "None", ""])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+---------+---------+---------+
| Field 1 | Field 2 | Field 3 | Field 4 |
+---------+---------+---------+---------+
| 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()
- == """
+ 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()
- == """
+ 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()
- == """
+ 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()
- == """
+ assert table.get_string().strip() == """
++---------+------------+---------+
+| Field 1 | Field 2 | Field 3 |
++---------+------------+---------+
+| Hello | 0123456789 | World |
++---------+------------+---------+
+""".strip()
+
+ def test_replace_custom_format_with_none(self) -> None:
+ table = PrettyTable()
+ table.custom_format = {"Field 2": lambda f, v: f"'{v}'"}
+ 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()
+
+ def test_replace_custom_format_with_none_dict(self) -> None:
+ table = PrettyTable()
+ table.custom_format = {"Field 2": lambda f, v: f"'{v}'"}
+ table.add_row(["Hello", None, "World"])
+ table.none_format = {"Field 2": "0123456789"}
+ assert table.get_string().strip() == """
+---------+------------+---------+
| Field 1 | Field 2 | Field 3 |
+---------+------------+---------+
| Hello | 0123456789 | World |
+---------+------------+---------+
""".strip()
- )
class TestBuildEquivalence:
@@ -361,6 +366,50 @@ class TestAlignment:
header=True
) == aligned_after_table.get_mediawiki_string(header=True)
+ def test_aligned_one_column(self, city_data: PrettyTable) -> None:
+ city_data.align["Population"] = "r"
+ assert city_data.get_string() == """
++-----------+------+------------+-----------------+
+| 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_aligned_one_column_invalid(self, city_data: PrettyTable) -> None:
+ with pytest.raises(ValueError):
+ city_data.align["Population"] = "unexpected" # type: ignore[assignment]
+
+ def test_aligned_one_column_invalid_dict(self, city_data: PrettyTable) -> None:
+ with pytest.raises(ValueError):
+ city_data.align = {"Population": "unexpected"} # type: ignore[dict-item]
+
+ def test_rename_drops_stale_align_keys(self) -> None:
+ table = PrettyTable()
+ table.field_names = ["a", "b", "c"]
+ table.add_row([1, 2, 3])
+ table.align["a"] = "l"
+ table.field_names = ["x", "y", "z"]
+ assert set(table.field_names) == {"x", "y", "z"}
+ for old in ("a", "b", "c"):
+ assert old not in table._align
+ assert old not in table._valign
+ assert table.align["x"] == "l"
+
+ def test_rename_keeps_overlapping_align_keys(self) -> None:
+ table = PrettyTable()
+ table.field_names = ["a", "b", "c"]
+ table.add_row([1, 2, 3])
+ table.field_names = ["x", "b", "z"]
+ assert "b" in table._align
+ assert "a" not in table._align
+ assert "c" not in table._align
+
class TestOptionOverride:
"""Make sure all options are properly overwritten by get_string."""
@@ -415,11 +464,42 @@ class TestOptionAttribute:
assert city_data.get_string() == city_data[:].get_string()
def test_set_for_one_column(self, city_data: PrettyTable) -> None:
- city_data.align["Rainfall"] = "l"
- city_data.max_width["Name"] = 10
- city_data.int_format["Population"] = "4"
- city_data.float_format["Area"] = "2.2"
- assert city_data.get_string() == city_data[:].get_string()
+ city_data.min_width["City name"] = 15
+ city_data.int_format["Area"] = "6"
+ city_data.float_format["Annual Rainfall"] = "6.2"
+ city_data.int_format["Population"] = "10"
+ city_data.align["Annual Rainfall"] = "l"
+ assert city_data.get_string() == """
++-----------------+--------+------------+-----------------+
+| 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_set_for_one_column2(self, city_data: PrettyTable) -> None:
+ city_data.min_width = {"City name": 15}
+ city_data.int_format = {"Area": "6"}
+ city_data.float_format = {"Annual Rainfall": "6.2"}
+ city_data.int_format = {"Population": "10"}
+ city_data.align = {"Annual Rainfall": "l"}
+ assert city_data.get_string() == """
++-----------------+--------+------------+-----------------+
+| 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_preserve_internal_border(self) -> None:
table = PrettyTable(preserve_internal_border=True)
@@ -429,16 +509,13 @@ class TestOptionAttribute:
helper_table.border = False
helper_table.preserve_internal_border = True
- assert (
- helper_table.get_string().strip()
- == """
+ assert helper_table.get_string().strip() == """
| Field 1 | Field 2 | Field 3
---+---------+---------+---------
1 | value 1 | value2 | value3
4 | value 4 | value5 | value6
7 | value 7 | value8 | value9
""".strip() # noqa: W291
- )
@pytest.fixture(scope="module")
@@ -481,6 +558,15 @@ class TestBasic:
table.add_rows([])
assert str(city_data) == str(table)
+ def test_add_row_toolong(self) -> None:
+ table = PrettyTable(CITY_DATA_HEADER)
+ with pytest.raises(ValueError):
+ table.add_row(["Geelong", 1, 308915, 123.1, 1])
+
+ def test_add_column_invalid(self, city_data: PrettyTable) -> None:
+ with pytest.raises(ValueError):
+ city_data.add_column("City name", ["Geelong", 2, 3, 4, 5, 6, 7, 8, 9, 10])
+
def _test_no_blank_lines(self, table: PrettyTable) -> None:
string = table.get_string()
lines = string.split("\n")
@@ -515,6 +601,33 @@ class TestBasic:
city_data.title = "My table (75 characters wide) " + "=" * 45
self._test_all_length_equal(city_data)
+ def test_multiline_title(self, city_data: PrettyTable) -> None:
+ """A title with \\n should produce multiple bordered title lines."""
+ city_data.title = "Line 1\nLine 2"
+ assert city_data.get_string() == """
++-------------------------------------------------+
+| Line 1 |
+| Line 2 |
++-----------+------+------------+-----------------+
+| 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_multiline_title_html(self, city_data: PrettyTable) -> None:
+ """Multiline titles should use <br> in HTML caption."""
+ city_data.title = "Line 1\nLine 2"
+ html = city_data.get_html_string()
+ assert "<br>" in html
+ assert "<caption>" in html
+
def test_no_blank_lines_without_border(self, city_data: PrettyTable) -> None:
"""No table should ever have blank lines in it."""
city_data.border = False
@@ -577,12 +690,12 @@ class TestBasic:
def test_no_blank_lines_with_float_format(self, city_data: PrettyTable) -> None:
"""No table should ever have blank lines in it."""
- city_data.float_format = "6.2f"
+ city_data.float_format = "6.2"
self._test_no_blank_lines(city_data)
def test_all_lengths_equal_with_float_format(self, city_data: PrettyTable) -> None:
"""All lines in a table should be of the same length."""
- city_data.float_format = "6.2f"
+ city_data.float_format = "6.2"
self._test_all_length_equal(city_data)
def test_no_blank_lines_from_csv(self, city_data_from_csv: PrettyTable) -> None:
@@ -611,6 +724,10 @@ class TestBasic:
def test_colcount(self, city_data: PrettyTable) -> None:
assert city_data.colcount == 4
+ def test_colcount_empty(self) -> None:
+ table = PrettyTable()
+ assert table.colcount == 0
+
def test_getitem(self, city_data: PrettyTable) -> None:
assert (
city_data[1].get_string()
@@ -625,6 +742,83 @@ class TestBasic:
with pytest.raises(IndexError):
assert city_data[10]
+ def test_header_style_upper(self) -> None:
+ table = PrettyTable(header_style="upper")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+---------+
+| FIELD 1 | FIELD 2 | FIELD 3 |
++---------+---------+---------+
+| 1 | 2 | 3 |
++---------+---------+---------+
+""".strip()
+
+ def test_header_style_upper_setter(self) -> None:
+ table = PrettyTable()
+ table.header_style = "upper"
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+---------+
+| FIELD 1 | FIELD 2 | FIELD 3 |
++---------+---------+---------+
+| 1 | 2 | 3 |
++---------+---------+---------+
+""".strip()
+
+ def test_header_style_cap(self) -> None:
+ table = PrettyTable(["cOl oNE", "col two", "COL THREE"], header_style="cap")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+-----------+
+| Col one | Col two | Col three |
++---------+---------+-----------+
+| 1 | 2 | 3 |
++---------+---------+-----------+
+""".strip()
+
+ def test_header_style_title(self) -> None:
+ table = PrettyTable(["cOl oNE", "col two", "COL THREE"], header_style="title")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+-----------+
+| Col One | Col Two | Col Three |
++---------+---------+-----------+
+| 1 | 2 | 3 |
++---------+---------+-----------+
+""".strip()
+
+ def test_header_style_lower(self) -> None:
+ table = PrettyTable(header_style="lower")
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++---------+---------+---------+
+| field 1 | field 2 | field 3 |
++---------+---------+---------+
+| 1 | 2 | 3 |
++---------+---------+---------+
+""".strip()
+
+ def test_horizontal_align_char_setter(self) -> None:
+ table = PrettyTable()
+ table.horizontal_align_char = "!"
+ table.add_row([1, 2, 3])
+ assert table.get_string() == """
++ !-----! + !-----! + !-----! +
+| Field 1 | Field 2 | Field 3 |
++ !-----! + !-----! + !-----! +
+| 1 | 2 | 3 |
++ !-----! + !-----! + !-----! +
+""".strip()
+
+ def test_horizontal_align_char_getter(self) -> None:
+ table = PrettyTable()
+ table.horizontal_align_char = ":"
+ assert table.horizontal_align_char == ":"
+
+ def test_header_style_invalid(self) -> None:
+ with pytest.raises(ValueError):
+ PrettyTable(header_style="FooBar")
+
@pytest.mark.usefixtures("init_db")
def test_no_blank_lines_from_db(self, db_cursor: sqlite3.Cursor) -> None:
"""No table should ever have blank lines in it."""
@@ -641,10 +835,38 @@ class TestBasic:
assert table is not None
self._test_all_length_equal(table)
+ def test_border(self) -> None:
+ table = PrettyTable(border=False)
+ table.add_row([1, 2, "3"])
+ assert table.get_string().strip() == """
+Field 1 Field 2 Field 3
+ 1 2 3 """.strip() # noqa: W291
+
+ def test_reversesort(self) -> None:
+ table = PrettyTable(["A", "B", "C"], sortby="A", reversesort=True)
+ table.add_row([1, 2, "3"])
+ table.add_row([3, 4, "5"])
+ assert table.get_string().strip() == """
++---+---+---+
+| A | B | C |
++---+---+---+
+| 3 | 4 | 5 |
+| 1 | 2 | 3 |
++---+---+---+""".strip()
+
class TestEmptyTable:
"""Make sure the print_empty option works"""
+ def test_print_empty(self) -> None:
+ table = PrettyTable(print_empty=False)
+ assert table.get_string().strip() == ""
+
+ def test_print_empty_setter(self) -> None:
+ table = PrettyTable()
+ table.print_empty = False
+ assert table.get_string().strip() == ""
+
def test_print_empty_true(self, city_data: PrettyTable) -> None:
table = PrettyTable()
table.field_names = CITY_DATA_HEADER
@@ -742,12 +964,24 @@ def float_pt() -> PrettyTable:
class TestFloatFormat:
+ def test_empty(self, float_pt: PrettyTable) -> None:
+ float_pt.float_format = ""
+ assert float_pt.get_string() == """
++----------+----------+
+| Constant | Value |
++----------+----------+
+| Pi | 3.141593 |
+| e | 2.718282 |
+| sqrt(2) | 1.414214 |
++----------+----------+
+""".strip()
+
def test_no_decimals(self, float_pt: PrettyTable) -> None:
- float_pt.float_format = ".0f"
+ float_pt.float_format = ".0"
assert "." not in float_pt.get_string()
def test_round_to_5dp(self, float_pt: PrettyTable) -> None:
- float_pt.float_format = ".5f"
+ float_pt.float_format = ".5"
string = float_pt.get_string()
assert "3.14159" in string
assert "3.141592" not in string
@@ -758,7 +992,7 @@ class TestFloatFormat:
assert "1.414213" not in string
def test_pad_with_2zeroes(self, float_pt: PrettyTable) -> None:
- float_pt.float_format = "06.2f"
+ float_pt.float_format = "06.2"
string = float_pt.get_string()
assert "003.14" in string
assert "002.72" in string
@@ -768,9 +1002,7 @@ class TestFloatFormat:
class TestColumnFormattingfromDict:
def test_set_align_format(self, city_data: PrettyTable) -> None:
city_data.align = {"Annual Rainfall": "r"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -783,7 +1015,6 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.4 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_set_valign_format(self, city_data: PrettyTable) -> None:
table = PrettyTable(
@@ -802,9 +1033,7 @@ class TestColumnFormattingfromDict:
]
)
- assert (
- table.get_string()
- == """
+ assert table.get_string() == """
+---------+----------------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+----------------------+---------+------------+---------+------------+
@@ -818,7 +1047,6 @@ class TestColumnFormattingfromDict:
| | | | diam | | diam |
+---------+----------------------+---------+------------+---------+------------+
""".strip()
- )
def test_max_width(
self,
@@ -838,9 +1066,7 @@ class TestColumnFormattingfromDict:
]
)
- assert (
- table.get_string()
- == """
+ assert table.get_string() == """
+---------+----------------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+----------------------+---------+------------+---------+------------+
@@ -854,7 +1080,6 @@ class TestColumnFormattingfromDict:
| | | | diam | | diam |
+---------+----------------------+---------+------------+---------+------------+
""".strip()
- )
def test_min_width(self, city_data: PrettyTable) -> None:
city_data.min_width = {
@@ -863,9 +1088,7 @@ class TestColumnFormattingfromDict:
"Population": 20,
"Annual Rainfall": 20,
}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+----------------------+------------+----------------------+----------------------+
| City name | Area | Population | Annual Rainfall |
+----------------------+------------+----------------------+----------------------+
@@ -878,13 +1101,10 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.4 |
+----------------------+------------+----------------------+----------------------+
""".strip()
- )
def test_set_int_format(self, city_data: PrettyTable) -> None:
city_data.int_format = {"Population": "20"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+----------------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+----------------------+-----------------+
@@ -897,13 +1117,77 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.4 |
+-----------+------+----------------------+-----------------+
""".strip()
- )
+
+ def test_set_int_format_overwrite_dict(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Population": lambda f, v: f"'{v}'"}
+ city_data.int_format = {"Population": "20"}
+ assert city_data.get_string() == """
++-----------+------+----------------------+-----------------+
+| 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_set_int_format_overwrite(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Population": lambda f, v: f"'{v}'"}
+ city_data.int_format = "20"
+ assert city_data.get_string() == """
++-----------+----------------------+----------------------+-----------------+
+| 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_set_float_format(self, city_data: PrettyTable) -> None:
city_data.float_format = {"Annual Rainfall": "4.2"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
++-----------+------+------------+-----------------+
+| 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_set_float_format_overwrite_dict(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Annual Rainfall": lambda f, v: f"'{v}'"}
+ city_data.float_format = {"Annual Rainfall": "4.2"}
+ assert city_data.get_string() == """
++-----------+------+------------+-----------------+
+| 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_set_float_format_overwrite(self, city_data: PrettyTable) -> None:
+ city_data.custom_format = {"Annual Rainfall": lambda f, v: f"'{v}'"}
+ city_data.float_format = "4.2"
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -916,13 +1200,10 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.40 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_set_custom_format(self, city_data: PrettyTable) -> None:
city_data.custom_format = {"Annual Rainfall": lambda f, v: f"{v:.2f}"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -935,22 +1216,18 @@ class TestColumnFormattingfromDict:
| Perth | 5386 | 1554769 | 869.40 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_set_none_format(self, city_data: PrettyTable) -> None:
city_data.clear_rows()
city_data.add_row([None, None, None, None])
city_data.none_format = {"Annual Rainfall": "N/A"}
- assert (
- city_data.get_string()
- == """
+ assert city_data.get_string() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
| None | None | None | N/A |
+-----------+------+------------+-----------------+
""".strip()
- )
class TestBreakLine:
@@ -1050,6 +1327,22 @@ class TestCsvOutput:
"value 7,value9\r\n"
)
+ def test_csv_formatting(self) -> None:
+ table = PrettyTable()
+ table.add_column("Name", ["Alice", "Bob", "Charlie"])
+ table.add_column("int_format", [7, 0, -42])
+ table.add_column("float_format", [3.14159, 10.0, -2.71828])
+ table.add_column("custom_format", [1234, 0, -5678])
+ table.int_format["int_format"] = "05"
+ table.float_format["float_format"] = ".1"
+ table.custom_format["custom_format"] = lambda f, v: f"{v:,}"
+ assert table.get_csv_string() == (
+ "Name,int_format,float_format,custom_format\r\n"
+ 'Alice,00007,3.1,"1,234"\r\n'
+ "Bob,00000,10.0,0\r\n"
+ 'Charlie,-0042,-2.7,"-5,678"\r\n'
+ )
+
def test_paginate(city_data: PrettyTable) -> None:
expected_page_1 = """
@@ -1168,9 +1461,7 @@ class TestCustomFormatter:
def test_use_custom_formatter_for_int(self, city_data: PrettyTable) -> None:
city_data.custom_format["Annual Rainfall"] = lambda n, v: f"{v:.2f}"
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -1183,7 +1474,6 @@ class TestCustomFormatter:
| Perth | 5386 | 1554769 | 869.40 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_custom_format_multi_type(self) -> None:
table = PrettyTable(["col_date", "col_str", "col_float", "col_int"])
@@ -1192,9 +1482,7 @@ class TestCustomFormatter:
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()
- == """
+ assert table.get_string().strip() == """
+-------------+----------+-----------+------------+
| col_date | col_str | col_float | col_int |
+-------------+----------+-----------+------------+
@@ -1202,7 +1490,6 @@ class TestCustomFormatter:
| 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"])
@@ -1219,9 +1506,7 @@ class TestCustomFormatter:
return str(value)
table.custom_format = my_format
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+-------------+----------+-----------+------------+
| col_date | col_str | col_float | col_int |
+-------------+----------+-----------+------------+
@@ -1229,7 +1514,41 @@ class TestCustomFormatter:
| 01 Feb 2021 | February | 54321.123 | 87,654,321 |
+-------------+----------+-----------+------------+
""".strip()
- )
+
+ def test_overwrite_default_format(self, city_data: PrettyTable) -> None:
+ city_data.float_format = "6.2"
+ city_data.int_format = "10"
+ city_data.custom_format["Annual Rainfall"] = lambda f, v: f"+++{v:.4f}+++"
+ assert city_data.get_string() == """
++-----------+------------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | +++600.5000+++ |
+| Brisbane | 5905 | 1857594 | +++1146.4000+++ |
+| Darwin | 112 | 120900 | +++1714.7000+++ |
+| Hobart | 1357 | 205556 | +++619.5000+++ |
+| Sydney | 2058 | 4336374 | +++1214.8000+++ |
+| Melbourne | 1566 | 3806092 | +++646.9000+++ |
+| Perth | 5386 | 1554769 | +++869.4000+++ |
++-----------+------------+------------+-----------------+""".strip()
+
+ def test_overwrite_default_format_dict(self, city_data: PrettyTable) -> None:
+ city_data.none_format = "N/A"
+ city_data.float_format = "6.2"
+ city_data.int_format = "10"
+ city_data.custom_format = {"Annual Rainfall": lambda f, v: f"+++{v:.4f}+++"}
+ assert city_data.get_string() == """
++-----------+------------+------------+-----------------+
+| City name | Area | Population | Annual Rainfall |
++-----------+------------+------------+-----------------+
+| Adelaide | 1295 | 1158259 | +++600.5000+++ |
+| Brisbane | 5905 | 1857594 | +++1146.4000+++ |
+| Darwin | 112 | 120900 | +++1714.7000+++ |
+| Hobart | 1357 | 205556 | +++619.5000+++ |
+| Sydney | 2058 | 4336374 | +++1214.8000+++ |
+| Melbourne | 1566 | 3806092 | +++646.9000+++ |
+| Perth | 5386 | 1554769 | +++869.4000+++ |
++-----------+------------+------------+-----------------+""".strip()
class TestRepr:
@@ -1289,9 +1608,7 @@ class TestWidth:
table.add_row([self.colored, self.colored])
table.add_row(["nothing", "neither"])
result = table.get_string()
- assert (
- result.strip()
- == f"""
+ assert result.strip() == f"""
+---------+---------+
| Field 1 | Field 2 |
+---------+---------+
@@ -1299,16 +1616,13 @@ class TestWidth:
| 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()
- == """
+ assert result.strip() == """
+---------+---------+
| Field 1 | Field 2 |
+---------+---------+
@@ -1316,7 +1630,15 @@ class TestWidth:
| nothing | neither |
+---------+---------+
""".strip()
- )
+
+ def test_tab_expansion_aligns_columns(self) -> None:
+ """Ensure expandtabs() is used in PrettyTable._format_value()."""
+ table = PrettyTable(["code", "note"])
+ table.add_row(["if x:\n\treturn 1", "tab-indented"])
+ result = table.get_string()
+ assert "\t" not in result
+ line_widths = {len(line) for line in result.splitlines()}
+ assert len(line_widths) == 1
@pytest.mark.parametrize(
"loops, fields, desired_width, border, internal_border",
@@ -1380,16 +1702,13 @@ class TestWidth:
table.add_row([0])
# FIXME: Table is wider than table.max_table_width
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+----+
| Fi |
+----+
| 0 |
+----+
""".strip()
- )
def test_max_table_width_wide(self) -> None:
table = PrettyTable()
@@ -1407,9 +1726,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---+---+---+---+---+------------------------------+
| F | F | F | F | F | Field 6 |
+---+---+---+---+---+------------------------------+
@@ -1420,7 +1737,6 @@ class TestWidth:
| | | | | | dolore magna aliquyam erat, |
| | | | | | sed diam voluptua |
+---+---+---+---+---+------------------------------+""".strip()
- )
def test_max_table_width_wide2(self) -> None:
table = PrettyTable()
@@ -1436,9 +1752,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---+-----------------+---+-----------------+---+-----------------+
| F | Field 2 | F | Field 4 | F | Field 6 |
+---+-----------------+---+-----------------+---+-----------------+
@@ -1448,7 +1762,6 @@ class TestWidth:
| e | sadipscing | u | sadipscing | o | sadipscing |
| m | elitr, sed diam | m | elitr, sed diam | r | elitr, sed diam |
+---+-----------------+---+-----------------+---+-----------------+""".strip()
- )
@pytest.mark.parametrize("set_width_parameter", [True, False])
def test_table_max_width_wo_header_width(self, set_width_parameter: bool) -> None:
@@ -1496,9 +1809,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+------------+---------+------------+---------+------------+
@@ -1511,7 +1822,6 @@ class TestWidth:
| | elitr, sed | | elitr, sed | | elitr, sed |
| | diam | | diam | | diam |
+---------+------------+---------+------------+---------+------------+""".strip()
- )
def test_table_width_on_init_with_columns(self) -> None:
"""See also #272"""
@@ -1530,9 +1840,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+---------+------------+---------+------------+---------+------------+
| Field 1 | Field 2 | Field 3 | Field 4 | Field 5 | Field 6 |
+---------+------------+---------+------------+---------+------------+
@@ -1545,7 +1853,6 @@ class TestWidth:
| | elitr, sed | | elitr, sed | | elitr, sed |
| | diam | | diam | | diam |
+---------+------------+---------+------------+---------+------------+""".strip()
- )
def test_table_minwidth_on_init_with_columns(self) -> None:
table = PrettyTable(["Field 1", "Field 2"], min_width=20)
@@ -1562,7 +1869,7 @@ class TestWidth:
| Field 1 | Field 2 |
+----------------------+--------------------------------------------------------------------+
| Lorem | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam |
-+----------------------+--------------------------------------------------------------------+""" # noqa: E501
++----------------------+--------------------------------------------------------------------+"""
)
def test_table_min_max_width_on_init_with_columns(self) -> None:
@@ -1590,15 +1897,12 @@ class TestWidth:
table.field_names = ["Metric", "Initial sol.", "Best sol."]
table.add_rows([["foo", 1.0 / 3.0, 1.0 / 3.0]])
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+--------+--------------+------------+
| Metric | Initial sol. | Best sol. |
+--------+--------------+------------+
| foo | 0.33 | 0.33 |
+--------+--------------+------------+""".strip()
- )
def test_max_table_width_wide_vrules_frame(self) -> None:
table = PrettyTable()
@@ -1617,9 +1921,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
+--------------------------------------------------+
| F F F F F Field 6 |
+--------------------------------------------------+
@@ -1630,7 +1932,6 @@ class TestWidth:
| dolore magna aliquyam erat, |
| sed diam voluptua |
+--------------------------------------------------+""".strip()
- )
def test_max_table_width_wide_vrules_none(self) -> None:
table = PrettyTable()
@@ -1649,9 +1950,7 @@ class TestWidth:
]
)
- assert (
- table.get_string().strip()
- == """
+ assert table.get_string().strip() == """
----------------------------------------------------
F F F F F Field 6
----------------------------------------------------
@@ -1662,7 +1961,6 @@ class TestWidth:
dolore magna aliquyam erat,
sed diam voluptua
----------------------------------------------------""".strip() # noqa: W291
- )
class TestFields:
@@ -1673,8 +1971,7 @@ class TestFields:
)
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+-----------------+
+ assert """+-----------+-----------------+
| City name | Annual Rainfall |
+-----------+-----------------+
| Adelaide | 600.5 |
@@ -1684,9 +1981,7 @@ class TestFields:
| Sydney | 1214.8 |
| Melbourne | 646.9 |
| Perth | 869.4 |
-+-----------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+-----------------+""" == table.get_string().strip()
def test_fields(self) -> None:
table = PrettyTable()
@@ -1694,8 +1989,7 @@ class TestFields:
table.fields = ["City name", "Annual Rainfall"]
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+-----------------+
+ assert """+-----------+-----------------+
| City name | Annual Rainfall |
+-----------+-----------------+
| Adelaide | 600.5 |
@@ -1705,9 +1999,7 @@ class TestFields:
| Sydney | 1214.8 |
| Melbourne | 646.9 |
| Perth | 869.4 |
-+-----------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+-----------------+""" == table.get_string().strip()
class TestGeneralOutput:
@@ -1815,3 +2107,15 @@ class TestDeprecations:
) -> None:
with pytest.deprecated_call(match=f"the '{name}' constant is deprecated"):
exec(f"from {module_name} import {name}")
+
+ def test_options_type_deprecation(self) -> None:
+ with pytest.deprecated_call(match="OptionsType"):
+ from prettytable.prettytable import OptionsType
+
+ assert OptionsType is not None
+
+ def test_table_handler_deprecation(self) -> None:
+ with pytest.deprecated_call(match="TableHandler is deprecated"):
+ from prettytable import TableHandler
+
+ TableHandler()
diff --git a/contrib/python/prettytable/py3/tests/test_rst.py b/contrib/python/prettytable/py3/tests/test_rst.py
new file mode 100644
index 00000000000..bc4e4e8498c
--- /dev/null
+++ b/contrib/python/prettytable/py3/tests/test_rst.py
@@ -0,0 +1,97 @@
+from __future__ import annotations
+
+from prettytable import PrettyTable, TableStyle
+
+
+def test_rst_output_without_header(helper_table: PrettyTable) -> None:
+ helper_table.set_style(TableStyle.RST)
+ assert helper_table.get_string(header=False).strip() == """
++---+---------+--------+--------+
+| 1 | value 1 | value2 | value3 |
++---+---------+--------+--------+
+| 4 | value 4 | value5 | value6 |
++---+---------+--------+--------+
+| 7 | value 7 | value8 | value9 |
++---+---------+--------+--------+
+""".strip()
+
+
+def test_rst_output_with_fields(helper_table: PrettyTable) -> None:
+ helper_table.set_style(TableStyle.RST)
+ assert helper_table.get_string(fields=["Field 1", "Field 3"]).strip() == """
++---------+---------+
+| Field 1 | Field 3 |
++=========+=========+
+| value 1 | value3 |
++---------+---------+
+| value 4 | value6 |
++---------+---------+
+| value 7 | value9 |
++---------+---------+
+""".strip()
+
+
+def test_rst_header_uses_equals(helper_table: PrettyTable) -> None:
+ # Arrange
+ helper_table.set_style(TableStyle.RST)
+
+ # Act
+ result = helper_table.get_string()
+
+ # Assert
+ lines = result.splitlines()
+ # The header separator (line after header row) should use "="
+ header_sep = lines[2]
+ assert "=" in header_sep
+ assert header_sep.startswith("+")
+ assert header_sep.endswith("+")
+ # Data separators should use "-"
+ data_sep = lines[4]
+ assert "=" not in data_sep
+ assert "-" in data_sep
+
+
+def test_rst_style_does_not_leak(helper_table: PrettyTable) -> None:
+ # Arrange
+ original = helper_table.get_string()
+
+ # Act
+ helper_table.set_style(TableStyle.RST)
+ helper_table.set_style(TableStyle.DEFAULT)
+
+ # Assert
+ assert helper_table.get_string() == original
+
+
+def test_rst_output_with_multiline_title(helper_table: PrettyTable) -> None:
+ # Arrange
+ helper_table.set_style(TableStyle.RST)
+ # Act
+ helper_table.title = "Line 1\nLine 2"
+ # Assert
+ assert helper_table.get_string() == """
++---------------------------------+
+| Line 1 |
+| Line 2 |
++---+---------+---------+---------+
+| | Field 1 | Field 2 | Field 3 |
++===+=========+=========+=========+
+| 1 | value 1 | value2 | value3 |
++---+---------+---------+---------+
+| 4 | value 4 | value5 | value6 |
++---+---------+---------+---------+
+| 7 | value 7 | value8 | value9 |
++---+---------+---------+---------+
+""".strip()
+
+
+def test_markdown_to_rst_does_not_leak(helper_table: PrettyTable) -> None:
+ # Arrange
+ helper_table.set_style(TableStyle.MARKDOWN)
+ helper_table.set_style(TableStyle.RST)
+
+ # Act
+ result = helper_table.get_string()
+
+ # Assert: Markdown's ":" alignment char should not appear in RST output
+ assert ":" not in result
diff --git a/contrib/python/prettytable/py3/tests/test_sorting.py b/contrib/python/prettytable/py3/tests/test_sorting.py
index 1968c718f40..172e6347450 100644
--- a/contrib/python/prettytable/py3/tests/test_sorting.py
+++ b/contrib/python/prettytable/py3/tests/test_sorting.py
@@ -34,9 +34,7 @@ class TestSorting:
city_data.sortby = "City name"
city_data.sort_key = key
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
@@ -49,7 +47,6 @@ class TestSorting:
| Melbourne | 1566 | 3806092 | 646.9 |
+-----------+------+------------+-----------------+
""".strip()
- )
def test_sort_key_at_class_declaration(self) -> None:
# Test sorting by length of city name
@@ -65,8 +62,7 @@ class TestSorting:
assert table.sort_key == key
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+------+------------+-----------------+
+ assert """+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
| Perth | 5386 | 1554769 | 869.4 |
@@ -76,11 +72,9 @@ class TestSorting:
| Adelaide | 1295 | 1158259 | 600.5 |
| Brisbane | 5905 | 1857594 | 1146.4 |
| Melbourne | 1566 | 3806092 | 646.9 |
-+-----------+------+------------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+------+------------+-----------------+""" == table.get_string().strip()
- def test_sort_slice(self) -> None:
+ def test_sort_slice_get_string(self) -> None:
"""Make sure sorting and slicing interact in the expected way"""
table = PrettyTable(["Foo"])
for i in range(20, 0, -1):
@@ -92,6 +86,44 @@ class TestSorting:
assert "10" not in oldstyle
assert "20" in oldstyle
+ def test_sort_slice(self) -> None:
+ """Make sure sorting and slicing interact in the expected way"""
+ # Arrange
+ table_new = PrettyTable(["Foo"])
+ table_old = PrettyTable(["Foo"], oldsortslice=True)
+ for i in range(20, 0, -1):
+ table_new.add_row([i])
+ table_old.add_row([i])
+
+ # Act
+ new_style = table_new.get_string(sortby="Foo", end=10)
+ old_style = table_old.get_string(sortby="Foo", end=10)
+
+ # Assert
+ assert "10" in new_style
+ assert "20" not in new_style
+ assert "10" not in old_style
+ assert "20" in old_style
+
+ def test_sort_slice_setter(self) -> None:
+ # Arrange
+ table_new = PrettyTable(["Foo"])
+ table_old = PrettyTable(["Foo"])
+ table_old.oldsortslice = True
+ for i in range(20, 0, -1):
+ table_new.add_row([i])
+ table_old.add_row([i])
+
+ # Act
+ new_style = table_new.get_string(sortby="Foo", end=10)
+ old_style = table_old.get_string(sortby="Foo", end=10)
+
+ # Assert
+ assert "10" in new_style
+ assert "20" not in new_style
+ assert "10" not in old_style
+ assert "20" in old_style
+
def test_sortby_at_class_declaration(self) -> None:
"""
Fix #354 where initialization of a table with sortby fails
@@ -103,8 +135,7 @@ class TestSorting:
assert table.sortby == "Area"
for row in CITY_DATA:
table.add_row(row)
- assert (
- """+-----------+------+------------+-----------------+
+ assert """+-----------+------+------------+-----------------+
| City name | Area | Population | Annual Rainfall |
+-----------+------+------------+-----------------+
| Darwin | 112 | 120900 | 1714.7 |
@@ -114,6 +145,4 @@ class TestSorting:
| Sydney | 2058 | 4336374 | 1214.8 |
| Perth | 5386 | 1554769 | 869.4 |
| Brisbane | 5905 | 1857594 | 1146.4 |
-+-----------+------+------------+-----------------+"""
- == table.get_string().strip()
- )
++-----------+------+------------+-----------------+""" == table.get_string().strip()
diff --git a/contrib/python/prettytable/py3/tests/test_style.py b/contrib/python/prettytable/py3/tests/test_style.py
index 2ae696e4bc3..1887fa0d40e 100644
--- a/contrib/python/prettytable/py3/tests/test_style.py
+++ b/contrib/python/prettytable/py3/tests/test_style.py
@@ -1,6 +1,8 @@
from __future__ import annotations
+import os
import random
+from typing import Literal
import pytest
from pytest_lazy_fixtures import lf
@@ -8,6 +10,14 @@ from pytest_lazy_fixtures import lf
from prettytable import HRuleStyle, PrettyTable, TableStyle, VRuleStyle
from prettytable.prettytable import _str_block_width
+# these tables don't display well outside of raw dump to a terminal, so they are
+# moved to external files, where they may be very easy to visually align, by command:
+#
+# $ cat tests/data/*.txt
+#
+import yatest.common as yc
+DATA_DIR = os.path.join(os.path.dirname(yc.source_path(__file__)), "data")
+
class TestPositionalJunctions:
"""Verify different cases for positional-junction characters"""
@@ -15,9 +25,7 @@ class TestPositionalJunctions:
def test_default(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═══════════╦══════╦════════════╦═════════════════╗
║ City name ║ Area ║ Population ║ Annual Rainfall ║
╠═══════════╬══════╬════════════╬═════════════════╣
@@ -29,15 +37,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
- )
def test_no_header(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.header = False
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═══════════╦══════╦═════════╦════════╗
║ Adelaide ║ 1295 ║ 1158259 ║ 600.5 ║
║ Brisbane ║ 5905 ║ 1857594 ║ 1146.4 ║
@@ -47,15 +52,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩═════════╩════════╝""".strip()
- )
def test_with_title(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.title = "Title"
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════════════════╗
║ Title ║
╠═══════════╦══════╦════════════╦═════════════════╣
@@ -69,15 +71,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
- )
def test_with_title_no_header(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.title = "Title"
city_data.header = False
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════╗
║ Title ║
╠═══════════╦══════╦═════════╦════════╣
@@ -89,15 +88,12 @@ class TestPositionalJunctions:
║ Melbourne ║ 1566 ║ 3806092 ║ 646.9 ║
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩═════════╩════════╝""".strip()
- )
def test_hrule_all(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.title = "Title"
city_data.hrules = HRuleStyle.ALL
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════════════════╗
║ Title ║
╠═══════════╦══════╦════════════╦═════════════════╣
@@ -117,7 +113,6 @@ class TestPositionalJunctions:
╠═══════════╬══════╬════════════╬═════════════════╣
║ Perth ║ 5386 ║ 1554769 ║ 869.4 ║
╚═══════════╩══════╩════════════╩═════════════════╝""".strip()
- )
def test_vrules_none(self, city_data: PrettyTable) -> None:
city_data.set_style(TableStyle.DOUBLE_BORDER)
@@ -141,9 +136,7 @@ class TestPositionalJunctions:
city_data.set_style(TableStyle.DOUBLE_BORDER)
city_data.vrules = VRuleStyle.FRAME
city_data.title = "Title"
- assert (
- city_data.get_string().strip()
- == """
+ assert city_data.get_string().strip() == """
╔═════════════════════════════════════════════════╗
║ Title ║
╠═════════════════════════════════════════════════╣
@@ -157,7 +150,6 @@ class TestPositionalJunctions:
║ Melbourne 1566 3806092 646.9 ║
║ Perth 5386 1554769 869.4 ║
╚═════════════════════════════════════════════════╝""".strip()
- )
class TestStyle:
@@ -167,6 +159,8 @@ class TestStyle:
pytest.param(
TableStyle.DEFAULT,
"""
++---------------------------------+
+| Table Caption |
+---+---------+---------+---------+
| | Field 1 | Field 2 | Field 3 |
+---+---------+---------+---------+
@@ -180,17 +174,21 @@ class TestStyle:
pytest.param(
TableStyle.MARKDOWN, # TODO fix
"""
+**Table Caption**
+
| | Field 1 | Field 2 | Field 3 |
| :-: | :-----: | :-----: | :-----: |
| 1 | value 1 | value2 | value3 |
| 4 | value 4 | value5 | value6 |
| 7 | value 7 | value8 | value9 |
-""",
+""".strip(),
id="MARKDOWN",
),
pytest.param(
TableStyle.MSWORD_FRIENDLY,
"""
++---------------------------------+
+| Table Caption |
| | Field 1 | Field 2 | Field 3 |
| 1 | value 1 | value2 | value3 |
| 4 | value 4 | value5 | value6 |
@@ -201,6 +199,8 @@ class TestStyle:
pytest.param(
TableStyle.ORGMODE,
"""
+|---------------------------------|
+| Table Caption |
|---+---------+---------+---------|
| | Field 1 | Field 2 | Field 3 |
|---+---------+---------+---------|
@@ -214,6 +214,7 @@ class TestStyle:
pytest.param(
TableStyle.PLAIN_COLUMNS,
"""
+Table Caption
Field 1 Field 2 Field 3
1 value 1 value2 value3
4 value 4 value5 value6
@@ -225,6 +226,8 @@ class TestStyle:
TableStyle.RANDOM,
"""
'^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^'
+% Table Caption %
+'^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^'
% 1 value 1 value2 value3%
% 4 value 4 value5 value6%
% 7 value 7 value8 value9%
@@ -235,7 +238,9 @@ class TestStyle:
pytest.param(
TableStyle.DOUBLE_BORDER,
"""
-╔═══╦═════════╦═════════╦═════════╗
+╔═════════════════════════════════╗
+║ Table Caption ║
+╠═══╦═════════╦═════════╦═════════╣
║ ║ Field 1 ║ Field 2 ║ Field 3 ║
╠═══╬═════════╬═════════╬═════════╣
║ 1 ║ value 1 ║ value2 ║ value3 ║
@@ -247,7 +252,9 @@ class TestStyle:
pytest.param(
TableStyle.SINGLE_BORDER,
"""
-┌───┬─────────┬─────────┬─────────┐
+┌─────────────────────────────────┐
+│ Table Caption │
+├───┬─────────┬─────────┬─────────┤
│ │ Field 1 │ Field 2 │ Field 3 │
├───┼─────────┼─────────┼─────────┤
│ 1 │ value 1 │ value2 │ value3 │
@@ -256,12 +263,30 @@ class TestStyle:
└───┴─────────┴─────────┴─────────┘
""",
),
+ pytest.param(
+ TableStyle.RST,
+ """
++---------------------------------+
+| Table Caption |
++---+---------+---------+---------+
+| | Field 1 | Field 2 | Field 3 |
++===+=========+=========+=========+
+| 1 | value 1 | value2 | value3 |
++---+---------+---------+---------+
+| 4 | value 4 | value5 | value6 |
++---+---------+---------+---------+
+| 7 | value 7 | value8 | value9 |
++---+---------+---------+---------+
+""",
+ id="RST",
+ ),
],
)
def test_style(
self, helper_table: PrettyTable, style: TableStyle, expected: str
) -> None:
random.seed(1234)
+ helper_table.title = "Table Caption"
helper_table.set_style(style)
assert helper_table.get_string().strip() == expected.strip()
@@ -375,6 +400,22 @@ class TestStyle:
""",
id="SINGLE_BORDER",
),
+ pytest.param(
+ TableStyle.MARKDOWN,
+ TableStyle.RST,
+ """
++---+---------+---------+---------+
+| | Field 1 | Field 2 | Field 3 |
++===+=========+=========+=========+
+| 1 | value 1 | value2 | value3 |
++---+---------+---------+---------+
+| 4 | value 4 | value5 | value6 |
++---+---------+---------+---------+
+| 7 | value 7 | value8 | value9 |
++---+---------+---------+---------+
+""",
+ id="RST",
+ ),
],
)
def test_style_reset(
@@ -523,7 +564,7 @@ class TestMultiPattern:
| \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",
),
],
@@ -539,15 +580,12 @@ class TestMultiPattern:
def test_colored_table() -> None:
table = PrettyTable(field_names=["Namespace", "Count"])
table.title = "\x1b[34mHere be Table caption\x1b[39m"
- assert (
- table.get_string()
- == """+-----------------------+
+ assert table.get_string() == """+-----------------------+
| \x1b[34mHere be Table caption\x1b[39m |
+-------------+---------+
| Namespace | Count |
+-------------+---------+
+-------------+---------+"""
- )
def test_link_and_color() -> None:
@@ -560,9 +598,7 @@ def test_link_and_color() -> None:
text = "Click \x1b[34mhere\x1b[39m"
table.add_row([f"\033]8;;https://example.com\033\\{text}\033]8;;\033\\", "3"])
- assert (
- table.get_string()
- == """\
+ assert table.get_string() == """\
+------------+-------+
| Link | Count |
+------------+-------+
@@ -570,7 +606,6 @@ def test_link_and_color() -> None:
| No link | 2 |
| \033]8;;https://example.com\033\\Click \x1b[34mhere\x1b[39m\033]8;;\033\\ | 3 |
+------------+-------+"""
- )
@pytest.mark.parametrize(
@@ -585,7 +620,123 @@ def test_link_and_color() -> None:
("\033]8;;https://example.com\033\\\x1b[34mblue link\x1b[39m\033]8;;\033\\", 9),
# link inside colour
("\x1b[34m\033]8;;https://example.com\033\\blue link\033]8;;\033\\\x1b[39m", 9),
+ # 中文 CJK wide characters
+ ("\u4e2d\u6587", 4),
+ # café (combining acute accent)
+ ("cafe\u0301", 4),
+ # 👨‍👩‍👧 ZWJ family
+ ("\U0001f468\u200d\U0001f469\u200d\U0001f467", 2),
+ # ☺️ VS16 emoji
+ ("\u263a\ufe0f", 2),
+ # 🇺🇸 regional flag
+ ("\U0001f1fa\U0001f1f8", 2),
+ # control code (bell)
+ ("abc\x07def", 6),
],
)
def test__str_block_width(test_input: str, expected: int) -> None:
assert _str_block_width(test_input) == expected
+
+
+ ["fields", "rows", "expected_file"],
+ [
+ (
+ ["Emoji", "Name"],
+ [
+ ["\U0001f468\u200d\U0001f469\u200d\U0001f467", "Family"],
+ ["\U0001f1fa\U0001f1f8", "USA"],
+ ["Hi", "Text"],
+ ],
+ "table_complex_emoji.txt",
+ ),
+ (
+ ["Word", "Lang"],
+ [["cafe\u0301", "FR"], ["cafe", "EN"]],
+ "table_combining_chars.txt",
+ ),
+ (
+ ["CJK", "Width"],
+ [["\u4e2d\u6587", "4"], ["Test", "4"]],
+ "table_cjk.txt",
+ ),
+ (
+ ["Status", "Count"],
+ [
+ ["\x1b[32mOK\x1b[0m", "10"],
+ ["\x1b[31mFailed\x1b[0m", "2"],
+ ["Normal", "5"],
+ ],
+ "table_ansi_colors.txt",
+ ),
+ ],
+)
+def test_table_unicode_width(
+ fields: list[str],
+ rows: list[list[str]],
+ expected_file: str,
+) -> None:
+ table = PrettyTable(fields)
+ for row in rows:
+ table.add_row(row)
+ with open(os.path.join(DATA_DIR, expected_file), encoding="utf-8") as fin:
+ expected_from_file = fin.read()
+ assert table.get_string().rstrip() == expected_from_file.rstrip()
+
+
+ ["align", "expected_file"],
+ [
+ ("l", "table_align_left.txt"),
+ ("r", "table_align_right.txt"),
+ ("c", "table_align_center.txt"),
+ ],
+)
+def test_table_alignment_with_emoji(
+ align: Literal["l", "c", "r"], expected_file: str
+) -> None:
+ table = PrettyTable(["Name"])
+ table.align["Name"] = align
+ table.add_row(["\U0001f468\u200d\U0001f469\u200d\U0001f467"]) # 👨‍👩‍👧
+ table.add_row(["Hi"])
+ with open(os.path.join(DATA_DIR, expected_file), encoding="utf-8") as fin:
+ expected_from_file = fin.read()
+ assert table.get_string().rstrip() == expected_from_file.strip()
+
+
+def test_ansi_wrap_width():
+ """ANSI escape sequences should not count toward column width when wrapping."""
+ table = PrettyTable(["Key", "Value"])
+ table.header = False
+ table.max_width["Value"] = 35
+ table.add_row(
+ [
+ "NEW_ENVIRON",
+ "\x1b[38;5;208mOversharing: HOME, PWD, SHELL,"
+ " SSH_AUTH_SOCK, XDG_SESSION_PATH\x1b[0m",
+ ]
+ )
+ result = table.get_string()
+ for line in result.split("\n"):
+ if "|" not in line:
+ continue
+ parts = line.split("|")
+ value_cell = parts[2] if len(parts) > 2 else ""
+ assert "\x1b[38;5;208m" not in value_cell or "\x1b[0m" in value_cell
+
+
+def test_ansi_wrap_no_bleed():
+ """Wrapped ANSI-colored text must close SGR on every line."""
+ table = PrettyTable(["Key", "Value"])
+ table.header = False
+ table.max_width["Value"] = 30
+ table.add_row(
+ [
+ "TEST",
+ "\x1b[31mRed text that is long enough to wrap across lines\x1b[0m",
+ ]
+ )
+ result = table.get_string()
+ for line in result.split("\n"):
+ if "\x1b[31m" in line:
+ assert "\x1b[0m" in line
diff --git a/contrib/python/prettytable/py3/ya.make b/contrib/python/prettytable/py3/ya.make
index 487fd84e582..1e97cf3a7d0 100644
--- a/contrib/python/prettytable/py3/ya.make
+++ b/contrib/python/prettytable/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(3.17.0)
+VERSION(3.18.0)
LICENSE(BSD-3-Clause)