From af40a322d6b889c9c1c2e5161a004c389401e754 Mon Sep 17 00:00:00 2001 From: jerome provensal Date: Mon, 26 Dec 2022 19:20:01 -0800 Subject: [PATCH 01/20] Fix bug/issue #231: SEPARATING_LINE feature doesn't work when the requested format pads columns --- tabulate/__init__.py | 9 +++++++-- test/test_output.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 3b1a1e1..bf2ed78 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -101,12 +101,17 @@ def _is_file(f): ) +def _is_separating_line_value(value): + return type(value) == str and value.strip() == SEPARATING_LINE + + def _is_separating_line(row): row_type = type(row) is_sl = (row_type == list or row_type == str) and ( - (len(row) >= 1 and row[0] == SEPARATING_LINE) - or (len(row) >= 2 and row[1] == SEPARATING_LINE) + (len(row) >= 1 and _is_separating_line_value(row[0])) + or (len(row) >= 2 and _is_separating_line_value(row[1])) ) + return is_sl diff --git a/test/test_output.py b/test/test_output.py index 9043aed..b58b187 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2881,6 +2881,27 @@ def test_list_of_lists_with_index_with_sep_line(): assert_equal(expected, result) +def test_with_padded_columns_with_sep_line(): + table = [ + ["1", "one"], # "1" as a str on purpose + [1_000, "one K"], + SEPARATING_LINE, + [1_000_000, "one M"], + ] + expected = "\n".join( + [ + "+---------+-------+", + "| 1 | one |", + "| 1000 | one K |", + "|---------+-------|", + "| 1000000 | one M |", + "+---------+-------+", + ] + ) + result = tabulate(table, tablefmt="psql") + assert_equal(expected, result) + + def test_list_of_lists_with_supplied_index(): "Output: a table with a supplied index" dd = zip(*[list(range(3)), list(range(101, 104))]) From 72ac2ab05cbe6b7a56d6f456e07ed57bd3648483 Mon Sep 17 00:00:00 2001 From: I Bo Date: Thu, 29 Dec 2022 16:18:47 +0300 Subject: [PATCH 02/20] Fix #207. Update README: headers for dictionaries. --- README.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d64b99a..ea8df18 100644 --- a/README.md +++ b/README.md @@ -121,10 +121,22 @@ dictionaries or named tuples: ```pycon >>> print(tabulate({"Name": ["Alice", "Bob"], ... "Age": [24, 19]}, headers="keys")) - Age Name ------ ------ - 24 Alice - 19 Bob +Name Age +------ ----- +Alice 24 +Bob 19 +``` + +When data is a list of dictionaries, a dictionary can be passed as `headers` +to replace the keys with other column labels: + +```pycon +>>> print(tabulate([{1: "Alice", 2: 24}, {1: "Bob", 2: 19}], +... headers={1: "Name", 2: "Age"})) +Name Age +------ ----- +Alice 24 +Bob 19 ``` ### Row Indices From 475d0da54b4c1e40c207fbdb022e5e8c15dadf20 Mon Sep 17 00:00:00 2001 From: Arpit Jain <3242828+arpitjain099@users.noreply.github.com> Date: Sat, 29 Apr 2023 17:58:38 +0900 Subject: [PATCH 03/20] Update common.py --- test/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/common.py b/test/common.py index d95e84f..e331e98 100644 --- a/test/common.py +++ b/test/common.py @@ -1,7 +1,6 @@ import pytest # noqa from pytest import skip, raises # noqa - def assert_equal(expected, result): print("Expected:\n%s\n" % expected) print("Got:\n%s\n" % result) From a8672a525e4144840120f275fcafc791728f5e8a Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Sat, 29 Apr 2023 18:10:53 +0900 Subject: [PATCH 04/20] add codeql file --- test/common.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/common.py b/test/common.py index e331e98..8cf09cd 100644 --- a/test/common.py +++ b/test/common.py @@ -6,7 +6,6 @@ def assert_equal(expected, result): print("Got:\n%s\n" % result) assert expected == result - def assert_in(result, expected_set): nums = range(1, len(expected_set) + 1) for i, expected in zip(nums, expected_set): From d38740ca77a3b8efed9f8b3fcc82e121bde84205 Mon Sep 17 00:00:00 2001 From: Arpit Jain Date: Sat, 29 Apr 2023 18:16:05 +0900 Subject: [PATCH 05/20] move libraries --- tabulate/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 3b1a1e1..872e863 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -10,6 +10,7 @@ import math import textwrap import dataclasses +import sys try: import wcwidth # optional wide-character (CJK) support @@ -1352,9 +1353,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): try: bool(headers) - is_headers2bool_broken = False # noqa except ValueError: # numpy.ndarray, pandas.core.index.Index, ... - is_headers2bool_broken = True # noqa headers = list(headers) index = None @@ -2646,8 +2645,6 @@ def _main(): (default: simple) """ import getopt - import sys - import textwrap usage = textwrap.dedent(_main.__doc__) try: From 4a8778008b984b93762f2b6f4cefb9443905a798 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 21 Dec 2023 12:12:43 +0200 Subject: [PATCH 06/20] Add support for Python 3.12 --- appveyor.yml | 1 + tox.ini | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4eb2dd8..318a1d6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,6 +16,7 @@ environment: - PYTHON: "C:\\Python39-x64" - PYTHON: "C:\\Python310-x64" - PYTHON: "C:\\Python311-x64" + - PYTHON: "C:\\Python312-x64" install: # Newer setuptools is needed for proper support of pyproject.toml diff --git a/tox.ini b/tox.ini index c6260d2..4c4fe06 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ # for testing and it is disabled by default. [tox] -envlist = lint, py{37, 38, 39, 310, 311} +envlist = lint, py{37, 38, 39, 310, 311, 312} isolated_build = True [testenv] @@ -105,6 +105,21 @@ deps = pandas wcwidth +[testenv:py312] +basepython = python3.12 +commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +deps = + pytest + +[testenv:py312-extra] +basepython = python3.12 +setenv = PYTHONDEVMODE = 1 +commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +deps = + pytest + numpy + pandas + wcwidth [flake8] max-complexity = 22 From 733be92058815b4295d8ed5858fa14d7b0aad890 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 21 Dec 2023 12:18:54 +0200 Subject: [PATCH 07/20] Drop support for EOL Python 3.7 --- HOWTOPUBLISH | 2 +- README.md | 16 ++++++++-------- appveyor.yml | 9 +-------- pyproject.toml | 3 +-- tabulate/__init__.py | 6 +++--- tox.ini | 17 +---------------- 6 files changed, 15 insertions(+), 38 deletions(-) diff --git a/HOWTOPUBLISH b/HOWTOPUBLISH index 5be16ed..795fc73 100644 --- a/HOWTOPUBLISH +++ b/HOWTOPUBLISH @@ -1,7 +1,7 @@ # update contributors and CHANGELOG in README # tag version release python3 benchmark.py # then update README -tox -e py37-extra,py38-extra,py39-extra,py310-extra +tox -e py38-extra,py39-extra,py310-extra,py311-extra,py312-extra python3 -m build -nswx . twine upload --repository-url https://test.pypi.org/legacy/ dist/* twine upload dist/* diff --git a/README.md b/README.md index 07ab28c..944a260 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ The following tabular data types are supported: - list of lists or another iterable of iterables - list or another iterable of dicts (keys as columns) - dict of iterables (keys as columns) -- list of dataclasses (Python 3.7+ only, field names as columns) +- list of dataclasses (field names as columns) - two-dimensional NumPy array - NumPy record arrays (names as columns) - pandas.DataFrame @@ -1074,14 +1074,14 @@ To run tests on all supported Python versions, make sure all Python interpreters, `pytest` and `tox` are installed, then run `tox` in the root of the project source tree. -On Linux `tox` expects to find executables like `python3.7`, `python3.8` etc. -On Windows it looks for `C:\Python37\python.exe`, `C:\Python38\python.exe` etc. respectively. +On Linux `tox` expects to find executables like `python3.11`, `python3.12` etc. +On Windows it looks for `C:\Python311\python.exe`, `C:\Python312\python.exe` etc. respectively. One way to install all the required versions of the Python interpreter is to use [pyenv](https://github.com/pyenv/pyenv). All versions can then be easily installed with something like: - pyenv install 3.7.12 - pyenv install 3.8.12 + pyenv install 3.11.7 + pyenv install 3.12.1 ... Don't forget to change your `PATH` so that `tox` knows how to find all the installed versions. Something like @@ -1089,10 +1089,10 @@ Don't forget to change your `PATH` so that `tox` knows how to find all the insta export PATH="${PATH}:${HOME}/.pyenv/shims" To test only some Python environments, use `-e` option. For example, to -test only against Python 3.7 and Python 3.10, run: +test only against Python 3.11 and Python 3.12, run: ```shell -tox -e py37,py310 +tox -e py311,py312 ``` in the root of the project source tree. @@ -1100,7 +1100,7 @@ in the root of the project source tree. To enable NumPy and Pandas tests, run: ```shell -tox -e py37-extra,py310-extra +tox -e py311-extra,py312-extra ``` (this may take a long time the first time, because NumPy and Pandas will diff --git a/appveyor.yml b/appveyor.yml index 318a1d6..d36b8c9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,10 +8,8 @@ environment: # The list here is complete (excluding Python 2.6, which # isn't covered by this document) at the time of writing. - - PYTHON: "C:\\Python37" - PYTHON: "C:\\Python38" - PYTHON: "C:\\Python39" - - PYTHON: "C:\\Python37-x64" - PYTHON: "C:\\Python38-x64" - PYTHON: "C:\\Python39-x64" - PYTHON: "C:\\Python310-x64" @@ -30,9 +28,6 @@ build: off test_script: # Put your test command here. - # If you don't need to build C extensions on 64-bit Python 3.3 or 3.4, - # you can remove "build.cmd" from the front of the command, as it's - # only needed to support those cases. # Note that you must use the environment variable %PYTHON% to refer to # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. @@ -41,9 +36,7 @@ test_script: after_test: # This step builds your wheels. - # Again, you only need build.cmd if you're building C extensions for - # 64-bit Python 3.3/3.4. And you need to use %PYTHON% to get the correct - # interpreter + # Again, you need to use %PYTHON% to get the correct interpreter #- "build.cmd %PYTHON%\\python.exe setup.py bdist_wheel" - "%PYTHON%\\python.exe -m build -nswx ." diff --git a/pyproject.toml b/pyproject.toml index 5a8c1fd..837747d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,14 +13,13 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries", ] -requires-python = ">=3.7" +requires-python = ">=3.8" dynamic = ["version"] [project.urls] diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..dbbe45d 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1331,7 +1331,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): * list of OrderedDicts (usually used with headers="keys") - * list of dataclasses (Python 3.7+ only, usually used with headers="keys") + * list of dataclasses (usually used with headers="keys") * 2D NumPy arrays @@ -1457,7 +1457,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): and len(rows) > 0 and dataclasses.is_dataclass(rows[0]) ): - # Python 3.7+'s dataclass + # Python's dataclass field_names = [field.name for field in dataclasses.fields(rows[0])] if headers == "keys": headers = field_names @@ -1600,7 +1600,7 @@ def tabulate( The first required argument (`tabular_data`) can be a list-of-lists (or another iterable of iterables), a list of named tuples, a dictionary of iterables, an iterable of dictionaries, - an iterable of dataclasses (Python 3.7+), a two-dimensional NumPy array, + an iterable of dataclasses, a two-dimensional NumPy array, NumPy record array, or a Pandas' dataframe. diff --git a/tox.ini b/tox.ini index 4c4fe06..7fd65a7 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ # for testing and it is disabled by default. [tox] -envlist = lint, py{37, 38, 39, 310, 311, 312} +envlist = lint, py{38, 39, 310, 311, 312} isolated_build = True [testenv] @@ -25,21 +25,6 @@ commands = python -m pre_commit run -a deps = pre-commit -[testenv:py37] -basepython = python3.7 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} -deps = - pytest - -[testenv:py37-extra] -basepython = python3.7 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} -deps = - pytest - numpy - pandas - wcwidth - [testenv:py38] basepython = python3.8 commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} From 36fb3088a85c0855a0985a2f2206ed872e0c71e1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 21 Dec 2023 12:21:58 +0200 Subject: [PATCH 08/20] Fix Black and Flake8 --- tabulate/__init__.py | 41 +++++++++++++----- test/common.py | 3 +- test/test_output.py | 98 +++++++++++++++++++++++++------------------- 3 files changed, 87 insertions(+), 55 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index dbbe45d..4b4cf2c 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1271,7 +1271,7 @@ def _align_header( def _remove_separating_lines(rows): - if type(rows) == list: + if isinstance(rows, list): separating_lines = [] sans_rows = [] for index, row in enumerate(rows): @@ -1319,7 +1319,8 @@ def _bool(val): def _normalize_tabular_data(tabular_data, headers, showindex="default"): - """Transform a supported data type to a list of lists, and a list of headers, with headers padding. + """Transform a supported data type to a list of lists, and a list of headers, + with headers padding. Supported tabular data types: @@ -2202,15 +2203,19 @@ def tabulate( # align columns # first set global alignment - if colglobalalign is not None: # if global alignment provided + if colglobalalign is not None: # if global alignment provided aligns = [colglobalalign] * len(cols) - else: # default + else: # default aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] # then specific alignements if colalign is not None: assert isinstance(colalign, Iterable) if isinstance(colalign, str): - warnings.warn(f"As a string, `colalign` is interpreted as {[c for c in colalign]}. Did you mean `colglobalalign = \"{colalign}\"` or `colalign = (\"{colalign}\",)`?", stacklevel=2) + warnings.warn( + f"As a string, `colalign` is interpreted as {[c for c in colalign]}. " + f'Did you mean `colglobalalign = "{colalign}"` or `colalign = ("{colalign}",)`?', + stacklevel=2, + ) for idx, align in enumerate(colalign): if not idx < len(aligns): break @@ -2229,20 +2234,25 @@ def tabulate( # align headers and add headers t_cols = cols or [[""]] * len(headers) # first set global alignment - if headersglobalalign is not None: # if global alignment provided + if headersglobalalign is not None: # if global alignment provided aligns_headers = [headersglobalalign] * len(t_cols) - else: # default + else: # default aligns_headers = aligns or [stralign] * len(headers) # then specific header alignements if headersalign is not None: assert isinstance(headersalign, Iterable) if isinstance(headersalign, str): - warnings.warn(f"As a string, `headersalign` is interpreted as {[c for c in headersalign]}. Did you mean `headersglobalalign = \"{headersalign}\"` or `headersalign = (\"{headersalign}\",)`?", stacklevel=2) + warnings.warn( + f"As a string, `headersalign` is interpreted as {[c for c in headersalign]}. " + f'Did you mean `headersglobalalign = "{headersalign}"` ' + f'or `headersalign = ("{headersalign}",)`?', + stacklevel=2, + ) for idx, align in enumerate(headersalign): hidx = headers_pad + idx if not hidx < len(aligns_headers): break - elif align == "same" and hidx < len(aligns): # same as column align + elif align == "same" and hidx < len(aligns): # same as column align aligns_headers[hidx] = aligns[hidx] elif align != "global": aligns_headers[hidx] = align @@ -2267,7 +2277,14 @@ def tabulate( _reinsert_separating_lines(rows, separating_lines) return _format_table( - tablefmt, headers, aligns_headers, rows, minwidths, aligns, is_multiline, rowaligns=rowaligns + tablefmt, + headers, + aligns_headers, + rows, + minwidths, + aligns, + is_multiline, + rowaligns=rowaligns, ) @@ -2398,7 +2415,9 @@ def str(self): return self -def _format_table(fmt, headers, headersaligns, rows, colwidths, colaligns, is_multiline, rowaligns): +def _format_table( + fmt, headers, headersaligns, rows, colwidths, colaligns, is_multiline, rowaligns +): """Produce a plain-text representation of the table.""" lines = [] hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else [] diff --git a/test/common.py b/test/common.py index 4cd3709..fe33259 100644 --- a/test/common.py +++ b/test/common.py @@ -2,6 +2,7 @@ from pytest import skip, raises # noqa import warnings + def assert_equal(expected, result): print("Expected:\n%s\n" % expected) print("Got:\n%s\n" % result) @@ -28,6 +29,7 @@ def rows_to_pipe_table_str(rows): return "\n".join(lines) + def check_warnings(func_args_kwargs, *, num=None, category=None, contain=None): func, args, kwargs = func_args_kwargs with warnings.catch_warnings(record=True) as W: @@ -41,4 +43,3 @@ def check_warnings(func_args_kwargs, *, num=None, category=None, contain=None): assert all([issubclass(w.category, category) for w in W]) if contain is not None: assert all([contain in str(w.message) for w in W]) - diff --git a/test/test_output.py b/test/test_output.py index d572498..8a60809 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2680,60 +2680,72 @@ def test_colalign_multi_with_sep_line(): expected = " one two\n\nthree four" assert_equal(expected, result) + def test_column_global_and_specific_alignment(): - """ Test `colglobalalign` and `"global"` parameter for `colalign`. """ - table = [[1,2,3,4],[111,222,333,444]] - colglobalalign = 'center' - colalign = ('global','left', 'right') + """Test `colglobalalign` and `"global"` parameter for `colalign`.""" + table = [[1, 2, 3, 4], [111, 222, 333, 444]] + colglobalalign = "center" + colalign = ("global", "left", "right") result = tabulate(table, colglobalalign=colglobalalign, colalign=colalign) - expected = '\n'.join([ - "--- --- --- ---", - " 1 2 3 4", - "111 222 333 444", - "--- --- --- ---"]) + expected = "\n".join( + [ + "--- --- --- ---", + " 1 2 3 4", + "111 222 333 444", + "--- --- --- ---", + ] + ) assert_equal(expected, result) + def test_headers_global_and_specific_alignment(): - """ Test `headersglobalalign` and `headersalign`. """ - table = [[1,2,3,4,5,6],[111,222,333,444,555,666]] - colglobalalign = 'center' - colalign = ('left',) - headers = ['h', 'e', 'a', 'd', 'e', 'r'] - headersglobalalign = 'right' - headersalign = ('same', 'same', 'left', 'global', 'center') - result = tabulate(table, headers=headers, colglobalalign=colglobalalign, colalign=colalign, headersglobalalign=headersglobalalign, headersalign=headersalign) - expected = '\n'.join([ - "h e a d e r", - "--- --- --- --- --- ---", - "1 2 3 4 5 6", - "111 222 333 444 555 666"]) + """Test `headersglobalalign` and `headersalign`.""" + table = [[1, 2, 3, 4, 5, 6], [111, 222, 333, 444, 555, 666]] + colglobalalign = "center" + colalign = ("left",) + headers = ["h", "e", "a", "d", "e", "r"] + headersglobalalign = "right" + headersalign = ("same", "same", "left", "global", "center") + result = tabulate( + table, + headers=headers, + colglobalalign=colglobalalign, + colalign=colalign, + headersglobalalign=headersglobalalign, + headersalign=headersalign, + ) + expected = "\n".join( + [ + "h e a d e r", + "--- --- --- --- --- ---", + "1 2 3 4 5 6", + "111 222 333 444 555 666", + ] + ) assert_equal(expected, result) + def test_colalign_or_headersalign_too_long(): - """ Test `colalign` and `headersalign` too long. """ - table = [[1,2],[111,222]] - colalign = ('global', 'left', 'center') - headers = ['h'] - headersalign = ('center', 'right', 'same') - result = tabulate(table, headers=headers, colalign=colalign, headersalign=headersalign) - expected = '\n'.join([ - " h", - "--- ---", - " 1 2", - "111 222"]) + """Test `colalign` and `headersalign` too long.""" + table = [[1, 2], [111, 222]] + colalign = ("global", "left", "center") + headers = ["h"] + headersalign = ("center", "right", "same") + result = tabulate( + table, headers=headers, colalign=colalign, headersalign=headersalign + ) + expected = "\n".join([" h", "--- ---", " 1 2", "111 222"]) assert_equal(expected, result) + def test_warning_when_colalign_or_headersalign_is_string(): - """ Test user warnings when `colalign` or `headersalign` is a string. """ - table = [[1,"bar"]] - opt = { - 'colalign': "center", - 'headers': ['foo', '2'], - 'headersalign': "center"} - check_warnings((tabulate, [table], opt), - num = 2, - category = UserWarning, - contain = "As a string") + """Test user warnings when `colalign` or `headersalign` is a string.""" + table = [[1, "bar"]] + opt = {"colalign": "center", "headers": ["foo", "2"], "headersalign": "center"} + check_warnings( + (tabulate, [table], opt), num=2, category=UserWarning, contain="As a string" + ) + def test_float_conversions(): "Output: float format parsed" From a37d34af8ea72a6b3a5afb18862fe2dedb391690 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 21 Dec 2023 12:22:26 +0200 Subject: [PATCH 09/20] Upgrade Python syntax with pyupgrade --py38-plus --- tabulate/__init__.py | 4 ++-- test/test_input.py | 2 +- test/test_textwrapper.py | 1 - 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 4b4cf2c..6adacde 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -224,7 +224,7 @@ def make_header_line(is_header, colwidths, colaligns): colwidths, [alignment[colalign] for colalign in colaligns] ) asciidoc_column_specifiers = [ - "{:d}{}".format(width, align) for width, align in asciidoc_alignments + f"{width:d}{align}" for width, align in asciidoc_alignments ] header_list = ['cols="' + (",".join(asciidoc_column_specifiers)) + '"'] @@ -1297,7 +1297,7 @@ def _prepend_row_index(rows, index): if isinstance(index, Sized) and len(index) != len(rows): raise ValueError( "index must be as long as the number of data rows: " - + "len(index)={} len(rows)={}".format(len(index), len(rows)) + + f"len(index)={len(index)} len(rows)={len(rows)}" ) sans_rows, separating_lines = _remove_separating_lines(rows) new_rows = [] diff --git a/test/test_input.py b/test/test_input.py index a178bd9..721d03a 100644 --- a/test/test_input.py +++ b/test/test_input.py @@ -522,7 +522,7 @@ def test_py37orlater_list_of_dataclasses_headers(): def test_list_bytes(): "Input: a list of bytes. (issue #192)" - lb = [["你好".encode("utf-8")], ["你好"]] + lb = [["你好".encode()], ["你好"]] expected = "\n".join( ["bytes", "---------------------------", r"b'\xe4\xbd\xa0\xe5\xa5\xbd'", "你好"] ) diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py index f3070b1..c8feded 100644 --- a/test/test_textwrapper.py +++ b/test/test_textwrapper.py @@ -1,5 +1,4 @@ """Discretely test functionality of our custom TextWrapper""" -from __future__ import unicode_literals import datetime From 92cb7096adaf162de9cbf7da6b432bc72cf68819 Mon Sep 17 00:00:00 2001 From: Israel Roldan Date: Sat, 13 Apr 2024 00:24:13 -0600 Subject: [PATCH 10/20] Fixed the problem with the intfmt argument. --- tabulate/__init__.py | 2 ++ test/test_output.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..03880ee 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1229,6 +1229,8 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): if valtype is str: return f"{val}" elif valtype is int: + if isinstance(val, str): + intfmt = "" return format(val, intfmt) elif valtype is bytes: try: diff --git a/test/test_output.py b/test/test_output.py index d572498..872e274 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1,4 +1,5 @@ """Test output of the various forms of tabular data.""" +import pytest import tabulate as tabulate_module from common import assert_equal, raises, skip, check_warnings @@ -2638,6 +2639,44 @@ def test_intfmt(): assert_equal(expected, result) +def test_intfmt_with_string_as_integer(): + "Output: integer format" + result = tabulate([[82642], ["1500"], [2463]], intfmt=",", tablefmt="plain") + expected = "82,642\n 1500\n 2,463" + assert_equal(expected, result) + + +@pytest.mark.skip(reason="It detects all values as floats but there are strings and integers.") +def test_intfmt_with_string_with_floats(): + "Output: integer format" + result = tabulate([[82000.38], ["1500.47"], ["2463"], [92165]], intfmt=",", tablefmt="plain") + expected = "82000.4\n 1500.47\n 2463\n92,165" + assert_equal(expected, result) + + +def test_intfmt_with_colors(): + "Regression: Align ANSI-colored values as if they were colorless." + colortable = [ + ("abcd", 42, "\x1b[31m42\x1b[0m"), + ("elfy", 1010, "\x1b[32m1010\x1b[0m"), + ] + colorheaders = ("test", "\x1b[34mtest\x1b[0m", "test") + formatted = tabulate(colortable, colorheaders, "grid", intfmt=",") + expected = "\n".join( + [ + "+--------+--------+--------+", + "| test | \x1b[34mtest\x1b[0m | test |", + "+========+========+========+", + "| abcd | 42 | \x1b[31m42\x1b[0m |", + "+--------+--------+--------+", + "| elfy | 1,010 | \x1b[32m1010\x1b[0m |", + "+--------+--------+--------+", + ] + ) + print(f"expected: {expected!r}\n\ngot: {formatted!r}\n") + assert_equal(expected, formatted) + + def test_empty_data_with_headers(): "Output: table with empty data and headers as firstrow" expected = "" From e45cac16676f0955b2fd05389d97b3e6f71fd66c Mon Sep 17 00:00:00 2001 From: Israel Roldan Date: Sat, 13 Apr 2024 16:09:47 -0600 Subject: [PATCH 11/20] Added validation for integer numbers format when the number is colored. --- tabulate/__init__.py | 9 +++++++++ test/test_output.py | 18 +++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 03880ee..9e96606 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1230,6 +1230,15 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): return f"{val}" elif valtype is int: if isinstance(val, str): + val_striped = val.encode('unicode_escape').decode('utf-8') + colored = re.search(r'(\\[xX]+[0-9a-fA-F]+\[\d+[mM]+)([0-9.]+)(\\.*)$', val_striped) + if colored: + total_groups = len(colored.groups()) + if total_groups == 3: + digits = colored.group(2) + if digits.isdigit(): + val_new = colored.group(1) + format(int(digits), intfmt) + colored.group(3) + val = val_new.encode('utf-8').decode('unicode_escape') intfmt = "" return format(val, intfmt) elif valtype is bytes: diff --git a/test/test_output.py b/test/test_output.py index 872e274..8d80d95 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2657,20 +2657,20 @@ def test_intfmt_with_string_with_floats(): def test_intfmt_with_colors(): "Regression: Align ANSI-colored values as if they were colorless." colortable = [ - ("abcd", 42, "\x1b[31m42\x1b[0m"), - ("elfy", 1010, "\x1b[32m1010\x1b[0m"), + ("\x1b[33mabc\x1b[0m", 42, "\x1b[31m42\x1b[0m"), + ("\x1b[35mdef\x1b[0m", 987654321, "\x1b[32m987654321\x1b[0m"), ] colorheaders = ("test", "\x1b[34mtest\x1b[0m", "test") formatted = tabulate(colortable, colorheaders, "grid", intfmt=",") expected = "\n".join( [ - "+--------+--------+--------+", - "| test | \x1b[34mtest\x1b[0m | test |", - "+========+========+========+", - "| abcd | 42 | \x1b[31m42\x1b[0m |", - "+--------+--------+--------+", - "| elfy | 1,010 | \x1b[32m1010\x1b[0m |", - "+--------+--------+--------+", + "+--------+-------------+-------------+", + "| test | \x1b[34mtest\x1b[0m | test |", + "+========+=============+=============+", + "| \x1b[33mabc\x1b[0m | 42 | \x1b[31m42\x1b[0m |", + "+--------+-------------+-------------+", + "| \x1b[35mdef\x1b[0m | 987,654,321 | \x1b[32m987,654,321\x1b[0m |", + "+--------+-------------+-------------+", ] ) print(f"expected: {expected!r}\n\ngot: {formatted!r}\n") From 23b3cc6537d2ca5cabad662a6ebf9dc371873d6c Mon Sep 17 00:00:00 2001 From: Israel Roldan Date: Sun, 14 Apr 2024 09:54:46 -0600 Subject: [PATCH 12/20] Updated the import of the PyTest jump decorator. --- test/test_output.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_output.py b/test/test_output.py index 8d80d95..8d0207c 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1,5 +1,5 @@ """Test output of the various forms of tabular data.""" -import pytest +from pytest import mark import tabulate as tabulate_module from common import assert_equal, raises, skip, check_warnings @@ -2646,7 +2646,7 @@ def test_intfmt_with_string_as_integer(): assert_equal(expected, result) -@pytest.mark.skip(reason="It detects all values as floats but there are strings and integers.") +@mark.skip(reason="It detects all values as floats but there are strings and integers.") def test_intfmt_with_string_with_floats(): "Output: integer format" result = tabulate([[82000.38], ["1500.47"], ["2463"], [92165]], intfmt=",", tablefmt="plain") From 277a900e710aa30864e32b5d91fa4c8d3a42f243 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 10:16:29 +0200 Subject: [PATCH 13/20] Remove Python 3.8 from appveyor.yml, add x86 configurations for Python 3.10, 3.11, 3.12 --- appveyor.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d36b8c9..eb255ed 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,9 +8,10 @@ environment: # The list here is complete (excluding Python 2.6, which # isn't covered by this document) at the time of writing. - - PYTHON: "C:\\Python38" - PYTHON: "C:\\Python39" - - PYTHON: "C:\\Python38-x64" + - PYTHON: "C:\\Python310" + - PYTHON: "C:\\Python311" + - PYTHON: "C:\\Python312" - PYTHON: "C:\\Python39-x64" - PYTHON: "C:\\Python310-x64" - PYTHON: "C:\\Python311-x64" From 275ff0f8aa11aca6ae419675e2101807ff5c4551 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 10:20:15 +0200 Subject: [PATCH 14/20] Remove Python 3.8 support from pyproject.toml Python 3.8 EOL is a month from now (31 Oct 2024). --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 837747d..8cb6216 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,13 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dynamic = ["version"] [project.urls] From dffb4679d8956eac6c78938e5293a008eee3160e Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 10:25:27 +0200 Subject: [PATCH 15/20] Update circleci config to use Python 3.12 image --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ac9c525..cd8bbc1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: docker: # specify the version you desire here # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - - image: circleci/python:3.8 + - image: circleci/python:3.12 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images From fc3f4e8eec4b8989465f737c8d29a60f0900d581 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 10:35:36 +0200 Subject: [PATCH 16/20] appveyor.yml - comment x86 Python 3.9 configuration because it is missing prebuilt Pandas dependencies --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index eb255ed..e92d9e4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,7 @@ environment: # The list here is complete (excluding Python 2.6, which # isn't covered by this document) at the time of writing. - - PYTHON: "C:\\Python39" + #- PYTHON: "C:\\Python39" - PYTHON: "C:\\Python310" - PYTHON: "C:\\Python311" - PYTHON: "C:\\Python312" From b35a36267f27b715758406a1bd38f66c965bb032 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 11:31:09 +0200 Subject: [PATCH 17/20] add .circleci github key fingerprint --- .circleci/config.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cd8bbc1..d5d3647 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,9 +1,16 @@ -# Python CircleCI 2.0 configuration file +# Python CircleCI 2.1 configuration file # -# Check https://circleci.com/docs/2.0/language-python/ for more details +# Check https://circleci.com/docs/language-python/ for more details # -version: 2 +version: 2.1 jobs: + # https://circleci.com/docs/github-integration/#create-additional-github-ssh-keys + deploy-job: + steps: + - add_ssh_keys: + fingerprints: + - "SHA256:f54rX6EuLbjtby+J672u/YhJ4iyTgAqOejxdcvVQf64" + build: docker: # specify the version you desire here From 6dff98017aedca304676436294219a0bd496e81b Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 11:36:25 +0200 Subject: [PATCH 18/20] remove Circle-CI integration At this point it's easier to migrate to GitHub Actions than to rewrite .cicleci/config.yml according to Circle CI 2.1 syntax --- .circleci/config.yml | 67 -------------------------------------- .circleci/requirements.txt | 10 ------ README.md | 2 +- 3 files changed, 1 insertion(+), 78 deletions(-) delete mode 100644 .circleci/config.yml delete mode 100644 .circleci/requirements.txt diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index d5d3647..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,67 +0,0 @@ -# Python CircleCI 2.1 configuration file -# -# Check https://circleci.com/docs/language-python/ for more details -# -version: 2.1 -jobs: - # https://circleci.com/docs/github-integration/#create-additional-github-ssh-keys - deploy-job: - steps: - - add_ssh_keys: - fingerprints: - - "SHA256:f54rX6EuLbjtby+J672u/YhJ4iyTgAqOejxdcvVQf64" - - build: - docker: - # specify the version you desire here - # use `-browsers` prefix for selenium tests, e.g. `3.6.1-browsers` - - image: circleci/python:3.12 - - # Specify service dependencies here if necessary - # CircleCI maintains a library of pre-built images - # documented at https://circleci.com/docs/2.0/circleci-images/ - # - image: circleci/postgres:9.4 - - working_directory: ~/repo - - steps: - - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: - name: install dependencies - command: | - python3 -m venv venv - . venv/bin/activate - pip install -r .circleci/requirements.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum ".circleci/requirements.txt" }} - - - run: - name: build wheel - command: | - . venv/bin/activate - python -m build -nwx . - - - store_artifacts: - path: dist - destination: dist - - - run: - name: run tests - command: | - . venv/bin/activate - tox -e py38-extra - - - store_artifacts: - path: test-reports - destination: test-reports diff --git a/.circleci/requirements.txt b/.circleci/requirements.txt deleted file mode 100644 index ea5e80c..0000000 --- a/.circleci/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -pytest -tox -numpy -pandas -wcwidth -setuptools -pip -build -wheel -setuptools_scm diff --git a/README.md b/README.md index 944a260..12309db 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ pip install tabulate Build status ------------ -[![Build status](https://circleci.com/gh/astanin/python-tabulate.svg?style=svg)](https://circleci.com/gh/astanin/python-tabulate/tree/master) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master) +[![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master) Library usage ------------- From 4440c1ed7842fd3c652b884361dbbead905cca4a Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 15:31:05 +0200 Subject: [PATCH 19/20] add GitHub Actions workflow based on tox-gh-actions, create a map of tox environments --- .github/workflows/github-actions-tox.yml | 25 ++++++++++++++++++++++++ README.md | 2 +- tox.ini | 7 +++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/github-actions-tox.yml diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/github-actions-tox.yml new file mode 100644 index 0000000..35be5cd --- /dev/null +++ b/.github/workflows/github-actions-tox.yml @@ -0,0 +1,25 @@ +name: python-tabulate + +on: + - push + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9', '3.10', '3.11', '3.12'] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox diff --git a/README.md b/README.md index 12309db..0023806 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ pip install tabulate Build status ------------ -[![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master) +[![python-tabulate](https://github.com/astanin/python-tabulate/actions/workflows/github-actions-tox.yml/badge.svg)](https://github.com/astanin/python-tabulate/actions/workflows/github-actions-tox.yml) [![Build status](https://ci.appveyor.com/api/projects/status/8745yksvvol7h3d7/branch/master?svg=true)](https://ci.appveyor.com/project/astanin/python-tabulate/branch/master) Library usage ------------- diff --git a/tox.ini b/tox.ini index 7fd65a7..92939c2 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,13 @@ envlist = lint, py{38, 39, 310, 311, 312} isolated_build = True +[gh-actions] +python = + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + [testenv] commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} deps = From 34310479571d2a1d067883bc9b90e4358c1040fe Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 15:39:47 +0200 Subject: [PATCH 20/20] skip all x86 builds on appveyor because they lack dependencies --- appveyor.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index e92d9e4..12bd772 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,9 +9,9 @@ environment: # isn't covered by this document) at the time of writing. #- PYTHON: "C:\\Python39" - - PYTHON: "C:\\Python310" - - PYTHON: "C:\\Python311" - - PYTHON: "C:\\Python312" + #- PYTHON: "C:\\Python310" + #- PYTHON: "C:\\Python311" + #- PYTHON: "C:\\Python312" - PYTHON: "C:\\Python39-x64" - PYTHON: "C:\\Python310-x64" - PYTHON: "C:\\Python311-x64"