From 422b36ec1191f07ff0816a0e3701ee3874afdb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladimir=20Vrzi=C4=87?= Date: Tue, 23 Feb 2021 19:27:21 +0100 Subject: [PATCH 01/78] Replace constant PRESERVE_WHITESPACE with an argument to tabulate() --- tabulate.py | 17 ++++++++--------- test/test_output.py | 6 ++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/tabulate.py b/tabulate.py index 55b896e..541cf29 100644 --- a/tabulate.py +++ b/tabulate.py @@ -68,9 +68,6 @@ def _is_file(f): # minimum extra space in headers MIN_PADDING = 2 -# Whether or not to preserve leading/trailing whitespace in data. -PRESERVE_WHITESPACE = False - _DEFAULT_FLOATFMT = "g" _DEFAULT_MISSINGVAL = "" # default align will be overwritten by "left", "center" or "decimal" @@ -810,13 +807,13 @@ def _choose_width_fn(has_invisible, enable_widechars, is_multiline): return width_fn -def _align_column_choose_padfn(strings, alignment, has_invisible): +def _align_column_choose_padfn(strings, alignment, has_invisible, preserve_whitespace): if alignment == "right": - if not PRESERVE_WHITESPACE: + if not preserve_whitespace: strings = [s.strip() for s in strings] padfn = _padleft elif alignment == "center": - if not PRESERVE_WHITESPACE: + if not preserve_whitespace: strings = [s.strip() for s in strings] padfn = _padboth elif alignment == "decimal": @@ -830,7 +827,7 @@ def _align_column_choose_padfn(strings, alignment, has_invisible): elif not alignment: padfn = _padnone else: - if not PRESERVE_WHITESPACE: + if not preserve_whitespace: strings = [s.strip() for s in strings] padfn = _padright return strings, padfn @@ -873,9 +870,10 @@ def _align_column( has_invisible=True, enable_widechars=False, is_multiline=False, + preserve_whitespace=False ): """[string] -> [padded_string]""" - strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible) + strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible, preserve_whitespace) width_fn = _align_column_choose_width_fn( has_invisible, enable_widechars, is_multiline ) @@ -1224,6 +1222,7 @@ def tabulate( showindex="default", disable_numparse=False, colalign=None, + preserve_whitespace=False ): """Format a fixed width table for pretty printing. @@ -1603,7 +1602,7 @@ def tabulate( [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) ) cols = [ - _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) + _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline, preserve_whitespace) for c, a, minw in zip(cols, aligns, minwidths) ] diff --git a/test/test_output.py b/test/test_output.py index abab439..660f047 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1598,18 +1598,16 @@ def test_disable_numparse_list(): def test_preserve_whitespace(): "Output: Default table output, but with preserved leading whitespace." - tabulate_module.PRESERVE_WHITESPACE = True table_headers = ["h1", "h2", "h3"] test_table = [[" foo", " bar ", "foo"]] expected = "\n".join( ["h1 h2 h3", "----- ------- ----", " foo bar foo"] ) - result = tabulate(test_table, table_headers) + result = tabulate(test_table, table_headers, preserve_whitespace=True) assert_equal(expected, result) - tabulate_module.PRESERVE_WHITESPACE = False table_headers = ["h1", "h2", "h3"] test_table = [[" foo", " bar ", "foo"]] expected = "\n".join(["h1 h2 h3", "---- ---- ----", "foo bar foo"]) - result = tabulate(test_table, table_headers) + result = tabulate(test_table, table_headers, preserve_whitespace=False) assert_equal(expected, result) From fbdb1fd702d9c5f7c03dbd091d5208141697683e Mon Sep 17 00:00:00 2001 From: Racerroar888 Date: Sat, 5 Nov 2022 08:40:39 -0600 Subject: [PATCH 02/78] fix maxcolwidths doesn't accept tuple --- README.md | 2 +- tabulate/__init__.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d64b99a..3996f7d 100644 --- a/README.md +++ b/README.md @@ -1123,5 +1123,5 @@ Bart Broere, Vilhelm Prytz, Alexander Gažo, Hugo van Kemenade, jamescooke, Matt Warner, Jérôme Provensal, Kevin Deldycke, Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH, Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan, -Dimitri Papadopoulos. +Dimitri Papadopoulos, Racerroar. diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 3b1a1e1..2309905 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2071,6 +2071,8 @@ def tabulate( list_of_lists, separating_lines = _remove_separating_lines(list_of_lists) if maxcolwidths is not None: + if type(maxcolwidths) is tuple: # Check if tuple, convert to list if so + maxcolwidths = list(maxcolwidths) if len(list_of_lists): num_cols = len(list_of_lists[0]) else: From b50e9ce3bc3087d12821ac89b1cfa917c8877b68 Mon Sep 17 00:00:00 2001 From: Phill Zarfos Date: Thu, 22 Dec 2022 16:42:58 -0500 Subject: [PATCH 03/78] added regression test for maxcolwidths doesn't accept tuple issue --- test/test_regression.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/test_regression.py b/test/test_regression.py index 8f60ce7..29f275e 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -488,6 +488,27 @@ def test_preserve_line_breaks_with_maxcolwidths(): assert_equal(expected, result) +def test_maxcolwidths_accepts_list_or_tuple(): + "Regression: maxcolwidths can accept a list or a tuple (github issue #214)" + table = [["lorem ipsum dolor sit amet"]*3] + expected = "\n".join( + [ + "+-------------+----------+----------------------------+", + "| lorem ipsum | lorem | lorem ipsum dolor sit amet |", + "| dolor sit | ipsum | |", + "| amet | dolor | |", + "| | sit amet | |", + "+-------------+----------+----------------------------+", + ] + ) + # test with maxcolwidths as a list + result = tabulate(table, tablefmt="grid", maxcolwidths=[12, 8]) + assert_equal(expected, result) + # test with maxcolwidths as a tuple + result = tabulate(table, tablefmt="grid", maxcolwidths=(12, 8)) + assert_equal(expected, result) + + def test_exception_on_empty_data_with_maxcolwidths(): "Regression: exception on empty data when using maxcolwidths (github issue #180)" result = tabulate([], maxcolwidths=5) From af40a322d6b889c9c1c2e5161a004c389401e754 Mon Sep 17 00:00:00 2001 From: jerome provensal Date: Mon, 26 Dec 2022 19:20:01 -0800 Subject: [PATCH 04/78] 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 96761ee53c01418cc347ae43deb829c5bcd6cd28 Mon Sep 17 00:00:00 2001 From: I Bo Date: Wed, 28 Dec 2022 16:52:04 +0300 Subject: [PATCH 05/78] Fix #124. Improve error reporting when non-iterable data provided. --- tabulate/__init__.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 3b1a1e1..0dbea61 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1357,15 +1357,24 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): is_headers2bool_broken = True # noqa headers = list(headers) + err_msg = ( + "\n\nTo build a table python-tabulate requires two-dimensional data " + "like a list of lists or similar." + "\nDid you forget a pair of extra [] or ',' in ()?" + ) index = None if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"): # dict-like and pandas.DataFrame? if hasattr(tabular_data.values, "__call__"): # likely a conventional dict keys = tabular_data.keys() - rows = list( - izip_longest(*tabular_data.values()) - ) # columns have to be transposed + try: + rows = list( + izip_longest(*tabular_data.values()) + ) # columns have to be transposed + except TypeError: # not iterable + raise TypeError(err_msg) + elif hasattr(tabular_data, "index"): # values is a property, has .index => it's likely a pandas.DataFrame (pandas 0.11.0) keys = list(tabular_data) @@ -1388,7 +1397,10 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): headers = list(map(str, keys)) # headers should be strings else: # it's a usual iterable of iterables, or a NumPy array, or an iterable of dataclasses - rows = list(tabular_data) + try: + rows = list(tabular_data) + except TypeError: # not iterable + raise TypeError(err_msg) if headers == "keys" and not rows: # an empty table (issue #81) From 72ac2ab05cbe6b7a56d6f456e07ed57bd3648483 Mon Sep 17 00:00:00 2001 From: I Bo Date: Thu, 29 Dec 2022 16:18:47 +0300 Subject: [PATCH 06/78] 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 07/78] 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 08/78] 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 09/78] 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 b1ed1fda6f62b9069ef7d5871925399159d9dfc9 Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Thu, 20 Jul 2023 14:50:40 -0600 Subject: [PATCH 10/78] Fix support for separating lines --- tabulate/__init__.py | 13 ++++++------- test/test_output.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..7bdeabd 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2414,7 +2414,6 @@ def _format_table(fmt, headers, headersaligns, rows, colwidths, colaligns, is_mu append_row = _append_basic_row padded_headers = pad_row(headers, pad) - padded_rows = [pad_row(row, pad) for row in rows] if fmt.lineabove and "lineabove" not in hidden: _append_line(lines, padded_widths, colaligns, fmt.lineabove) @@ -2424,17 +2423,17 @@ def _format_table(fmt, headers, headersaligns, rows, colwidths, colaligns, is_mu if fmt.linebelowheader and "linebelowheader" not in hidden: _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) - if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: + if rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: # initial rows with a line below - for row, ralign in zip(padded_rows[:-1], rowaligns): + for row, ralign in zip(rows[:-1], rowaligns): append_row( - lines, row, padded_widths, colaligns, fmt.datarow, rowalign=ralign + lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow, rowalign=ralign ) _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) # the last row without a line below append_row( lines, - padded_rows[-1], + pad_row(rows[-1], pad), padded_widths, colaligns, fmt.datarow, @@ -2448,13 +2447,13 @@ def _format_table(fmt, headers, headersaligns, rows, colwidths, colaligns, is_mu or fmt.lineabove or Line("", "", "", "") ) - for row in padded_rows: + for row in rows: # test to see if either the 1st column or the 2nd column (account for showindex) has # the SEPARATING_LINE flag if _is_separating_line(row): _append_line(lines, padded_widths, colaligns, separating_line) else: - append_row(lines, row, padded_widths, colaligns, fmt.datarow) + append_row(lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow) if fmt.linebelow and "linebelow" not in hidden: _append_line(lines, padded_widths, colaligns, fmt.linebelow) diff --git a/test/test_output.py b/test/test_output.py index d572498..c6e0c4b 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -257,6 +257,21 @@ def test_simple_with_sep_line(): assert_equal(expected, result) +def test_orgtbl_with_sep_line(): + "Output: orgtbl with headers and separating line" + expected = "\n".join( + [ + "| strings | numbers |", + "|-----------+-----------|", + "| spam | 41.9999 |", + "|-----------+-----------|", + "| eggs | 451 |", + ] + ) + result = tabulate(_test_table_with_sep_line, _test_table_headers, tablefmt="orgtbl") + assert_equal(expected, result) + + def test_readme_example_with_sep(): table = [["Earth", 6371], ["Mars", 3390], SEPARATING_LINE, ["Moon", 1737]] expected = "\n".join( @@ -311,6 +326,28 @@ def test_simple_multiline_2_with_sep_line(): assert_equal(expected, result) +def test_orgtbl_multiline_2_with_sep_line(): + "Output: simple with multiline cells" + expected = "\n".join( + [ + "| key | value |", + "|-------+-----------|", + "| foo | bar |", + "|-------+-----------|", + "| spam | multiline |", + "| | world |", + ] + ) + table = [ + ["key", "value"], + ["foo", "bar"], + SEPARATING_LINE, + ["spam", "multiline\nworld"], + ] + result = tabulate(table, headers="firstrow", stralign="center", tablefmt="orgtbl") + assert_equal(expected, result) + + def test_simple_headerless(): "Output: simple without headers" expected = "\n".join( From 6ea2a05552352ea78247ee791e1cf0d8574c13ef Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Wed, 11 Jan 2023 12:10:01 +0100 Subject: [PATCH 11/78] No need to import Unicode from future in Python 3 --- test/test_textwrapper.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py index f3070b1..88420be 100644 --- a/test/test_textwrapper.py +++ b/test/test_textwrapper.py @@ -1,6 +1,4 @@ """Discretely test functionality of our custom TextWrapper""" -from __future__ import unicode_literals - import datetime from tabulate import _CustomTextWrap as CTW, tabulate From f078669f38e14a44ea85f725e226f7ec7f3093a8 Mon Sep 17 00:00:00 2001 From: Phill Zarfos Date: Sat, 21 Oct 2023 12:48:26 -0400 Subject: [PATCH 12/78] fix merge conflict in README.md file --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3996f7d..0feb58a 100644 --- a/README.md +++ b/README.md @@ -1123,5 +1123,5 @@ Bart Broere, Vilhelm Prytz, Alexander Gažo, Hugo van Kemenade, jamescooke, Matt Warner, Jérôme Provensal, Kevin Deldycke, Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH, Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan, -Dimitri Papadopoulos, Racerroar. +Dimitri Papadopoulos, Élie Goudout, Racerroar, Phill Zarfos. From 4a8778008b984b93762f2b6f4cefb9443905a798 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Thu, 21 Dec 2023 12:12:43 +0200 Subject: [PATCH 13/78] 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 14/78] 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 15/78] 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 16/78] 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 4a5feba2346b294047222d8db3e7a483f4942cb9 Mon Sep 17 00:00:00 2001 From: Andrew Coffey Date: Tue, 2 Jan 2024 21:19:18 -0600 Subject: [PATCH 17/78] empty aligns no longer gives a KeyError --- tabulate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..0b0d99b 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2207,7 +2207,7 @@ def tabulate( else: # default aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] # then specific alignements - if colalign is not None: + if colalign is not None and aligns: 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) From 216a36ea30c37018641f54cc110579bc944c7574 Mon Sep 17 00:00:00 2001 From: Andrew Coffey Date: Tue, 2 Jan 2024 21:33:03 -0600 Subject: [PATCH 18/78] added regression test --- test/test_regression.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/test_regression.py b/test/test_regression.py index 8f60ce7..d2fc3a0 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -512,3 +512,14 @@ def test_numpy_int64_as_integer(): assert_equal(expected, result) except ImportError: raise skip("") + +def test_empty_table_with_colalign(): + "Regression: empty table with colalign kwarg" + table = tabulate([], ["a", "b", "c"], colalign=("center", "left", "left", "center")) + expected = "\n".join( + [ + "a b c", + "--- --- ---", + ] + ) + assert_equal(expected, table) From d19c2fbe1468bc5c4634e90e2823cfa970874930 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Fri, 12 Jan 2024 23:55:55 -0300 Subject: [PATCH 19/78] WIP: Try to fix tabulate._CustomTextWrap._handle_long_word breaking up ANSI escape codes. --- tabulate/__init__.py | 14 +++++++++++--- test/test_textwrapper.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..8dca8d6 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2540,10 +2540,18 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): # take each charcter's width into account chunk = reversed_chunks[-1] i = 1 - while self._len(chunk[:i]) <= space_left: + while len(_strip_ansi(chunk)[:i]) <= space_left: i = i + 1 - cur_line.append(chunk[: i - 1]) - reversed_chunks[-1] = chunk[i - 1 :] + # Consider escape codes when breaking words up + total_escape_len = 0 + if _ansi_codes.search(chunk) is not None: + for group, _, _, _ in _ansi_codes.findall(chunk): + escape_len = len(group) + # FIXME: Needs to keep track of found groups and search from there + if group in chunk[: i + total_escape_len + escape_len - 1]: + total_escape_len += escape_len + cur_line.append(chunk[: i + total_escape_len - 1]) + reversed_chunks[-1] = chunk[i + total_escape_len - 1 :] # Otherwise, we have to preserve the long word intact. Only add # it to the current line if there's nothing already there -- diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py index f3070b1..b02f5d1 100644 --- a/test/test_textwrapper.py +++ b/test/test_textwrapper.py @@ -3,7 +3,7 @@ import datetime -from tabulate import _CustomTextWrap as CTW, tabulate +from tabulate import _CustomTextWrap as CTW, tabulate, _strip_ansi from textwrap import TextWrapper as OTW from common import skip, assert_equal @@ -158,6 +158,42 @@ def test_wrap_color_line_splillover(): assert_equal(expected, result) +def test_wrap_color_line_longword(): + """TextWrapper: Wrap a line - preserve internal color tags and wrap them to + other lines when required, requires adding the colors tags to other lines as appropriate + and avoiding splitting escape codes.""" + data = "This_is_a_\033[31mtest_string_for_testing_TextWrap\033[0m_with_colors" + + expected = [ + "This_is_a_\033[31mte\033[0m", + "\033[31mst_string_fo\033[0m", + "\033[31mr_testing_Te\033[0m", + "\033[31mxtWrap\033[0m_with_", + "colors", + ] + wrapper = CTW(width=12) + result = wrapper.wrap(data) + assert_equal(expected, result) + + +def test_wrap_color_line_multiple_escapes(): + data = '012345(\x1b[32ma\x1b[0mbc\x1b[32mdefghij\x1b[0m)' + expected = [ + "012345(\x1b[32ma\x1b[0mbc\x1b[32mdefg\x1b[0m", + "\x1b[32mhij\x1b[0m)", + ] + wrapper = CTW(width=10) + result = wrapper.wrap(data) + assert_equal(expected, result) + clean_data = _strip_ansi(data) + for width in range(2, len(clean_data)): + # Currently fails with 14, 15 and 16, because a escape code gets split at the end + wrapper = CTW(width=width) + result = wrapper.wrap(data) + # print(width, result) + # Comparing after stripping ANSI should be enough to catch broken escape codes + assert_equal(clean_data, _strip_ansi("".join(result))) + def test_wrap_datetime(): """TextWrapper: Show that datetimes can be wrapped without crashing""" data = [ From d27d3668f5c06c7bfd85e0c51f88a65d63fbed34 Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Sun, 14 Jan 2024 14:58:05 -0300 Subject: [PATCH 20/78] Fix breaking up of long words so it doesn't mess up ANSI escape codes. --- tabulate/__init__.py | 7 +++++-- test/test_textwrapper.py | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 8dca8d6..f156177 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2540,16 +2540,19 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): # take each charcter's width into account chunk = reversed_chunks[-1] i = 1 + # Only count printable characters, so strip_ansi first, index later. while len(_strip_ansi(chunk)[:i]) <= space_left: i = i + 1 # Consider escape codes when breaking words up total_escape_len = 0 + last_group = 0 if _ansi_codes.search(chunk) is not None: for group, _, _, _ in _ansi_codes.findall(chunk): escape_len = len(group) - # FIXME: Needs to keep track of found groups and search from there - if group in chunk[: i + total_escape_len + escape_len - 1]: + if group in chunk[last_group: i + total_escape_len + escape_len - 1]: total_escape_len += escape_len + found = _ansi_codes.search(chunk[last_group:]) + last_group += found.end() cur_line.append(chunk[: i + total_escape_len - 1]) reversed_chunks[-1] = chunk[i + total_escape_len - 1 :] diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py index b02f5d1..bdae8c8 100644 --- a/test/test_textwrapper.py +++ b/test/test_textwrapper.py @@ -177,23 +177,23 @@ def test_wrap_color_line_longword(): def test_wrap_color_line_multiple_escapes(): - data = '012345(\x1b[32ma\x1b[0mbc\x1b[32mdefghij\x1b[0m)' + data = "012345(\x1b[32ma\x1b[0mbc\x1b[32mdefghij\x1b[0m)" expected = [ - "012345(\x1b[32ma\x1b[0mbc\x1b[32mdefg\x1b[0m", - "\x1b[32mhij\x1b[0m)", + "012345(\x1b[32ma\x1b[0mbc\x1b[32m\x1b[0m", + "\x1b[32mdefghij\x1b[0m)", ] wrapper = CTW(width=10) result = wrapper.wrap(data) assert_equal(expected, result) + clean_data = _strip_ansi(data) for width in range(2, len(clean_data)): - # Currently fails with 14, 15 and 16, because a escape code gets split at the end wrapper = CTW(width=width) result = wrapper.wrap(data) - # print(width, result) # Comparing after stripping ANSI should be enough to catch broken escape codes assert_equal(clean_data, _strip_ansi("".join(result))) + def test_wrap_datetime(): """TextWrapper: Show that datetimes can be wrapped without crashing""" data = [ From e1c59c325081e10f4ef9db86a357eb9cb5be448e Mon Sep 17 00:00:00 2001 From: devdanzin <74280297+devdanzin@users.noreply.github.com> Date: Mon, 15 Jan 2024 08:08:05 -0300 Subject: [PATCH 21/78] Make assert_equal() print repr() instead of str(). --- test/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/common.py b/test/common.py index 4cd3709..75524ec 100644 --- a/test/common.py +++ b/test/common.py @@ -3,8 +3,8 @@ import warnings def assert_equal(expected, result): - print("Expected:\n%s\n" % expected) - print("Got:\n%s\n" % result) + print("Expected:\n%r\n" % expected) + print("Got:\n%r\n" % result) assert expected == result From 92cb7096adaf162de9cbf7da6b432bc72cf68819 Mon Sep 17 00:00:00 2001 From: Israel Roldan Date: Sat, 13 Apr 2024 00:24:13 -0600 Subject: [PATCH 22/78] 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 23/78] 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 24/78] 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 0655054b4115f59607bbbd37e60f0897dd267261 Mon Sep 17 00:00:00 2001 From: cdar Date: Tue, 25 Jun 2024 23:23:16 +0200 Subject: [PATCH 25/78] Fix support for separating lines --- tabulate/__init__.py | 9 ++++++--- test/test_output.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..ff8591c 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2304,6 +2304,8 @@ def _expand_iterable(original, num_desired, default): def _pad_row(cells, padding): if cells: + if cells == SEPARATING_LINE: + return SEPARATING_LINE pad = " " * padding padded_cells = [pad + cell + pad for cell in cells] return padded_cells @@ -2427,9 +2429,10 @@ def _format_table(fmt, headers, headersaligns, rows, colwidths, colaligns, is_mu if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: # initial rows with a line below for row, ralign in zip(padded_rows[:-1], rowaligns): - append_row( - lines, row, padded_widths, colaligns, fmt.datarow, rowalign=ralign - ) + if row != SEPARATING_LINE: + append_row( + lines, row, padded_widths, colaligns, fmt.datarow, rowalign=ralign + ) _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) # the last row without a line below append_row( diff --git a/test/test_output.py b/test/test_output.py index d572498..1768720 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -335,6 +335,36 @@ def test_simple_headerless_with_sep_line(): assert_equal(expected, result) +def test_simple_headerless_with_sep_line_with_padding_in_tablefmt(): + "Output: simple without headers with sep line with padding in tablefmt" + expected = "\n".join( + [ + "|------|----------|", + "| spam | 41.9999 |", + "|------|----------|", + "| eggs | 451 |", + ] + ) + result = tabulate(_test_table_with_sep_line, tablefmt="github") + assert_equal(expected, result) + + +def test_simple_headerless_with_sep_line_with_linebetweenrows_in_tablefmt(): + "Output: simple without headers with sep line with linebetweenrows in tablefmt" + expected = "\n".join( + [ + "+------+----------+", + "| spam | 41.9999 |", + "+------+----------+", + "+------+----------+", + "| eggs | 451 |", + "+------+----------+", + ] + ) + result = tabulate(_test_table_with_sep_line, tablefmt="grid") + assert_equal(expected, result) + + def test_simple_multiline_headerless(): "Output: simple with multiline cells without headers" table = [["foo bar\nbaz\nbau", "hello"], ["", "multiline\nworld"]] From b49b98eaa1aaecf44ab2842c30d826e4b17ef184 Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Wed, 25 Sep 2024 14:59:28 +0100 Subject: [PATCH 26/78] Add colon_grid table format --- tabulate/__init__.py | 74 ++++++++++++++++++++++++++++++++++++++++++-- test/test_output.py | 13 ++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..bfc6fa0 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -134,6 +134,30 @@ def _pipe_line_with_colons(colwidths, colaligns): return "|" + "|".join(segments) + "|" +def _grid_segment_with_colons(colwidth, align): + """Return a segment of a horizontal line with optional colons which indicate + column's alignment in a grid table.""" + width = colwidth + if align == "right": + return ("=" * (width - 1)) + ":" + elif align == "center": + return ":" + ("=" * (width - 2)) + ":" + elif align == "left": + return ":" + ("=" * (width - 1)) + else: + return "=" * width + + +def _grid_line_with_colons(colwidths, colaligns): + """Return a horizontal line with optional colons to indicate column's alignment + in a grid table.""" + if not colaligns: + colaligns = [""] * len(colwidths) + segments = [_grid_segment_with_colons(w, a) for a, w in zip(colaligns, colwidths)] + return "+" + "+".join(segments) + "+" + + + def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): alignment = { "left": "", @@ -400,6 +424,16 @@ def escape_empty(val): padding=1, with_header_hide=None, ), + "colon_grid": TableFormat( + lineabove=Line("+", "-", "+", "+"), + linebelowheader=_grid_line_with_colons, + linebetweenrows=Line("+", "-", "+", "+"), + linebelow=Line("+", "-", "+", "+"), + headerrow=DataRow("|", "|", "|"), + datarow=DataRow("|", "|", "|"), + padding=1, + with_header_hide=None, + ), "outline": TableFormat( lineabove=Line("+", "-", "+", "+"), linebelowheader=Line("+", "=", "+", "+"), @@ -693,6 +727,7 @@ def escape_empty(val): "mixed_grid": "mixed_grid", "double_grid": "double_grid", "fancy_grid": "fancy_grid", + "colon_grid": "colon_grid", "pipe": "pipe", "orgtbl": "orgtbl", "jira": "jira", @@ -1822,6 +1857,32 @@ def tabulate( │ eggs │ 451 │ ╘═══════════╧═══════════╛ + "colon_grid" is similar to "grid" but uses colons only to define + columnwise content alignment, with no whitespace padding: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "colon_grid")) + + +-----------+-----------+ + | strings | numbers | + +:==========+:==========+ + | spam | 41.9999 | + +-----------+-----------+ + | eggs | 451 | + +-----------+-----------+ + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "colon_grid", + ... colalign=["right, left"])) + + +-----------+-----------+ + | strings | numbers | + +==========:+:==========+ + | spam | 41.9999 | + +-----------+-----------+ + | eggs | 451 | + +-----------+-----------+ + "outline" is the same as the "grid" format but doesn't draw lines between rows: >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], @@ -2138,6 +2199,10 @@ def tabulate( numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign stralign = "left" if stralign == _DEFAULT_ALIGN else stralign + if tablefmt == "colon_grid": + colglobalalign = "left" + headersglobalalign = "left" + # optimization: look for ANSI control codes once, # enable smart width functions only if a control code is found # @@ -2206,7 +2271,7 @@ def tabulate( aligns = [colglobalalign] * len(cols) else: # default aligns = [numalign if ct in [int, float] else stralign for ct in coltypes] - # then specific alignements + # then specific alignments if colalign is not None: assert isinstance(colalign, Iterable) if isinstance(colalign, str): @@ -2219,9 +2284,12 @@ def tabulate( minwidths = ( [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) ) + aligns_copy = aligns.copy() + if tablefmt == "colon_grid": + aligns_copy = ["left"] * len(cols) cols = [ _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline) - for c, a, minw in zip(cols, aligns, minwidths) + for c, a, minw in zip(cols, aligns_copy, minwidths) ] aligns_headers = None @@ -2233,7 +2301,7 @@ def tabulate( aligns_headers = [headersglobalalign] * len(t_cols) else: # default aligns_headers = aligns or [stralign] * len(headers) - # then specific header alignements + # then specific header alignments if headersalign is not None: assert isinstance(headersalign, Iterable) if isinstance(headersalign, str): diff --git a/test/test_output.py b/test/test_output.py index d572498..491d260 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1413,6 +1413,19 @@ def test_fancy_grid_multiline_row_align(): result = tabulate(table, tablefmt="fancy_grid", rowalign=[None, "center", "bottom"]) assert_equal(expected, result) +def test_colon_grid(): + expected = "\n".join( + [ + "+------+------+", + "| H1 | H2 |", + "+=====:+:====:+", + "| 3 | 4 |", + "+------+------+", + ] + ) + result = tabulate([[3, 4]], headers=("H1", "H2"), tablefmt="colon_grid", colalign=["right", "center"]) + assert_equal(expected, result) + def test_outline(): "Output: outline with headers" From 277a900e710aa30864e32b5d91fa4c8d3a42f243 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 10:16:29 +0200 Subject: [PATCH 27/78] 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 28/78] 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 29/78] 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 30/78] 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 31/78] 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 32/78] 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 277e649579842878059464bc32f0e97aff287d29 Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Thu, 26 Sep 2024 14:11:24 +0100 Subject: [PATCH 33/78] Correct alignment of text in docstring examples --- tabulate/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index bfc6fa0..e61e57c 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1864,11 +1864,11 @@ def tabulate( ... ["strings", "numbers"], "colon_grid")) +-----------+-----------+ - | strings | numbers | + | strings | numbers | +:==========+:==========+ - | spam | 41.9999 | + | spam | 41.9999 | +-----------+-----------+ - | eggs | 451 | + | eggs | 451 | +-----------+-----------+ >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], @@ -1876,11 +1876,11 @@ def tabulate( ... colalign=["right, left"])) +-----------+-----------+ - | strings | numbers | + | strings | numbers | +==========:+:==========+ - | spam | 41.9999 | + | spam | 41.9999 | +-----------+-----------+ - | eggs | 451 | + | eggs | 451 | +-----------+-----------+ "outline" is the same as the "grid" format but doesn't draw lines between rows: From 4a17bc776fbc911e2d54f800e1d66bb545fdb314 Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Thu, 26 Sep 2024 14:17:10 +0100 Subject: [PATCH 34/78] Document changes required for colon_grid format --- tabulate/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index e61e57c..5acc98f 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2199,6 +2199,9 @@ def tabulate( numalign = "decimal" if numalign == _DEFAULT_ALIGN else numalign stralign = "left" if stralign == _DEFAULT_ALIGN else stralign + # 'colon_grid' uses colons in the line beneath the header to represent a column's + # alignment instead of literally aligning the text differently. Hence, + # left alignment of the data in the text output is enforced. if tablefmt == "colon_grid": colglobalalign = "left" headersglobalalign = "left" @@ -2285,6 +2288,8 @@ def tabulate( [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols) ) aligns_copy = aligns.copy() + # Reset alignments in copy of alignments list to "left" for 'colon_grid' format, + # which enforces left alignment in the text output of the data. if tablefmt == "colon_grid": aligns_copy = ["left"] * len(cols) cols = [ From 4440c1ed7842fd3c652b884361dbbead905cca4a Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 15:31:05 +0200 Subject: [PATCH 35/78] 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 36/78] 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" From 5a7ab7fb482f990bc7bb2061b0d8c7d579c01188 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:05:43 +0200 Subject: [PATCH 37/78] enable extra tests in GitHub Actions --- tox.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index 92939c2..b37e0aa 100644 --- a/tox.ini +++ b/tox.ini @@ -13,10 +13,10 @@ isolated_build = True [gh-actions] python = - 3.9: py39 - 3.10: py310 - 3.11: py311 - 3.12: py312 + 3.9: py39-extra + 3.10: py310-extra + 3.11: py311-extra + 3.12: py312-extra [testenv] commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} From 18d7b50edd4e6d05d8ee77ab5a043717fe1ef966 Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Thu, 26 Sep 2024 15:07:50 +0100 Subject: [PATCH 38/78] Expand tests for colon_grid --- test/test_output.py | 78 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/test/test_output.py b/test/test_output.py index 491d260..25b99a9 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1413,7 +1413,9 @@ def test_fancy_grid_multiline_row_align(): result = tabulate(table, tablefmt="fancy_grid", rowalign=[None, "center", "bottom"]) assert_equal(expected, result) + def test_colon_grid(): + "Output: colon_grid with two columns aligned left and center" expected = "\n".join( [ "+------+------+", @@ -1427,6 +1429,82 @@ def test_colon_grid(): assert_equal(expected, result) +def test_colon_grid_wide_characters(): + "Output: colon_grid with wide chars in header" + try: + import wcwidth # noqa + except ImportError: + skip("test_colon_grid_wide_characters is skipped") + headers = list(_test_table_headers) + headers[1] = "配列" + expected = "\n".join( + [ + "+-----------+---------+", + "| strings | 配列 |", + "+:==========+========:+", + "| spam | 41.9999 |", + "+-----------+---------+", + "| eggs | 451 |", + "+-----------+---------+", + ] + ) + result = tabulate(_test_table, headers, tablefmt="colon_grid", colalign=["left", "right"]) + assert_equal(expected, result) + + +def test_colon_grid_headerless(): + "Output: colon_grid without headers" + expected = "\n".join( + [ + "+------+---------+", + "| spam | 41.9999 |", + "+------+---------+", + "| eggs | 451 |", + "+------+---------+", + ] + ) + result = tabulate(_test_table, tablefmt="colon_grid") + assert_equal(expected, result) + + +def test_colon_grid_multiline(): + "Output: colon_grid with multiline cells" + table = [["Data\n5", "33\n3"]] + headers = ["H1\n1", "H2\n2"] + expected = "\n".join( + [ + "+------+------+", + "| H1 | H2 |", + "| 1 | 2 |", + "+:=====+:=====+", + "| Data | 33 |", + "| 5 | 3 |", + "+------+------+", + ] + ) + result = tabulate(table, headers, tablefmt="colon_grid") + assert_equal(expected, result) + + +def test_colon_grid_with_empty_cells(): + table = [["A", ""], ["", "B"]] + headers = ["H1", "H2"] + alignments = ["center", "right"] + expected = "\n".join( + [ + "+------+------+", + "| H1 | H2 |", + "+:====:+=====:+", + "| A | |", + "+------+------+", + "| | B |", + "+------+------+", + ] + ) + result = tabulate(table, headers, tablefmt="colon_grid", colalign=alignments) + assert_equal(expected, result) + + def test_outline(): "Output: outline with headers" expected = "\n".join( From db5cdd0cc88f2e1a4b29dfa4ea08df07bf24388c Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:09:31 +0200 Subject: [PATCH 39/78] install pytest, numpy and pandas in Github Actions workflow --- .github/workflows/github-actions-tox.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/github-actions-tox.yml index 35be5cd..98c814a 100644 --- a/.github/workflows/github-actions-tox.yml +++ b/.github/workflows/github-actions-tox.yml @@ -20,6 +20,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + python -m pip install pytest numpy pandas python -m pip install tox tox-gh-actions - name: Test with tox run: tox From bdb639e650d9033a07d17e8eded9a908f08e101d Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:21:55 +0200 Subject: [PATCH 40/78] rewrite GitHub Action using explicit reference to tox environment with -extra suffix --- .github/workflows/github-actions-tox.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/github-actions-tox.yml index 98c814a..178b212 100644 --- a/.github/workflows/github-actions-tox.yml +++ b/.github/workflows/github-actions-tox.yml @@ -12,15 +12,13 @@ jobs: 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 pytest numpy pandas - python -m pip install tox tox-gh-actions - - name: Test with tox - run: tox + - uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install tox and any other packages + run: pip install tox pytest numpy pandas + - name: Run tox + # Run tox using the version of Python in `PATH` + run: tox -e py${{ matrix.python}}-extra From ec79fe0381d6d89afd0d4d4075282ecb26349f19 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:32:00 +0200 Subject: [PATCH 41/78] configure GitHub Action using tox-gh instead of tox-gh-actions --- .github/workflows/github-actions-tox.yml | 22 ++++++++++++---------- tox.ini | 2 +- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/github-actions-tox.yml index 178b212..0b89058 100644 --- a/.github/workflows/github-actions-tox.yml +++ b/.github/workflows/github-actions-tox.yml @@ -12,13 +12,15 @@ jobs: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - - name: Install tox and any other packages - run: pip install tox pytest numpy pandas - - name: Run tox - # Run tox using the version of Python in `PATH` - run: tox -e py${{ matrix.python}}-extra + - uses: actions/checkout@v34 + - 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 pytest numpy pandas + python -m pip install tox tox-gh + - name: Test with tox + run: tox diff --git a/tox.ini b/tox.ini index b37e0aa..a7fe513 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ envlist = lint, py{38, 39, 310, 311, 312} isolated_build = True -[gh-actions] +[gh] python = 3.9: py39-extra 3.10: py310-extra From 4bd335c585b9d9445ca3bdc3efb7a41e6defc1c7 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:33:16 +0200 Subject: [PATCH 42/78] fix typo in github-actions-tox.yml --- .github/workflows/github-actions-tox.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/github-actions-tox.yml index 0b89058..e39e44f 100644 --- a/.github/workflows/github-actions-tox.yml +++ b/.github/workflows/github-actions-tox.yml @@ -12,7 +12,7 @@ jobs: python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - - uses: actions/checkout@v34 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: From 8cd28a3a36bae2daacbc3333b19fdbb344a39177 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:36:58 +0200 Subject: [PATCH 43/78] rewrite GitHub Action using pytest --- .github/workflows/github-actions-tox.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/github-actions-tox.yml index e39e44f..9adabb7 100644 --- a/.github/workflows/github-actions-tox.yml +++ b/.github/workflows/github-actions-tox.yml @@ -21,6 +21,6 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install pytest numpy pandas - python -m pip install tox tox-gh - - name: Test with tox - run: tox + - name: Run tests + run: | + pytest -v --doctest-modules --ignore benchmark.py From 968f25851eea82bff2c4f10d46035164ff54418e Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:40:59 +0200 Subject: [PATCH 44/78] apply black --- tabulate/__init__.py | 2 +- test/test_cli.py | 1 - test/test_internal.py | 20 +++++++++++++++++--- test/test_output.py | 5 ++++- test/test_textwrapper.py | 3 ++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 874ad90..704bc5b 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2762,7 +2762,7 @@ def _main(): print(usage) sys.exit(0) files = [sys.stdin] if not args else args - with (sys.stdout if outfile == "-" else open(outfile, "w")) as out: + with sys.stdout if outfile == "-" else open(outfile, "w") as out: for f in files: if f == "-": f = sys.stdin diff --git a/test/test_cli.py b/test/test_cli.py index ce85f19..e71572d 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -2,7 +2,6 @@ """ - import os import sys diff --git a/test/test_internal.py b/test/test_internal.py index 64e1d12..17107c6 100644 --- a/test/test_internal.py +++ b/test/test_internal.py @@ -180,9 +180,16 @@ def test_wrap_text_wide_chars(): except ImportError: skip("test_wrap_text_wide_chars is skipped") - rows = [["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"]] + rows = [ + ["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"] + ] widths = [5, 20] - expected = [["청자\n청자\n청자\n청자\n청자", "약간 감싸면 더 잘\n보일 수있는 다소 긴\n설명입니다"]] + expected = [ + [ + "청자\n청자\n청자\n청자\n청자", + "약간 감싸면 더 잘\n보일 수있는 다소 긴\n설명입니다", + ] + ] result = T._wrap_text_to_colwidths(rows, widths) assert_equal(expected, result) @@ -239,7 +246,14 @@ def test_wrap_text_to_colwidths_colors_wide_char(): except ImportError: skip("test_wrap_text_to_colwidths_colors_wide_char is skipped") - data = [[("\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m")]] + data = [ + [ + ( + "\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" + " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m" + ) + ] + ] result = T._wrap_text_to_colwidths(data, [30]) expected = [ diff --git a/test/test_output.py b/test/test_output.py index 260ed31..fe9e486 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -136,7 +136,10 @@ def test_plain_maxcolwidth_autowraps_wide_chars(): table = [ ["hdr", "fold"], - ["1", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다 설명입니다 설명입니다 설명입니다 설명"], + [ + "1", + "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다 설명입니다 설명입니다 설명입니다 설명", + ], ] expected = "\n".join( [ diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py index c8feded..1f134fd 100644 --- a/test/test_textwrapper.py +++ b/test/test_textwrapper.py @@ -143,7 +143,8 @@ def test_wrap_color_in_single_line(): def test_wrap_color_line_splillover(): """TextWrapper: Wrap a line - preserve internal color tags and wrap them to - other lines when required, requires adding the colors tags to other lines as appropriate""" + other lines when required, requires adding the colors tags to other lines as appropriate + """ # This has both a text color and a background color data = "This is a \033[31mtest string for testing TextWrap\033[0m with colors" From 4e97640cbc53d7dfa7f5a1e635f104852debb2bf Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:42:10 +0200 Subject: [PATCH 45/78] fix E721 warning in tabulate/__init__.py --- tabulate/__init__.py | 2 +- test/test_internal.py | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 704bc5b..0660072 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -103,7 +103,7 @@ def _is_file(f): def _is_separating_line_value(value): - return type(value) == str and value.strip() == SEPARATING_LINE + return type(value) is str and value.strip() == SEPARATING_LINE def _is_separating_line(row): diff --git a/test/test_internal.py b/test/test_internal.py index 17107c6..e7564d3 100644 --- a/test/test_internal.py +++ b/test/test_internal.py @@ -180,9 +180,7 @@ def test_wrap_text_wide_chars(): except ImportError: skip("test_wrap_text_wide_chars is skipped") - rows = [ - ["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"] - ] + rows = [["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"]] widths = [5, 20] expected = [ [ @@ -246,14 +244,7 @@ def test_wrap_text_to_colwidths_colors_wide_char(): except ImportError: skip("test_wrap_text_to_colwidths_colors_wide_char is skipped") - data = [ - [ - ( - "\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" - " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m" - ) - ] - ] + data = [[("\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m")]] result = T._wrap_text_to_colwidths(data, [30]) expected = [ From 10f7b758fdcace9bcce3d14225ddadbe0f17e40e Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:43:16 +0200 Subject: [PATCH 46/78] rename GitHub Action workflow --- .github/workflows/{github-actions-tox.yml => tabulate.yml} | 0 README.md | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{github-actions-tox.yml => tabulate.yml} (100%) diff --git a/.github/workflows/github-actions-tox.yml b/.github/workflows/tabulate.yml similarity index 100% rename from .github/workflows/github-actions-tox.yml rename to .github/workflows/tabulate.yml diff --git a/README.md b/README.md index 0023806..543576d 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ pip install tabulate Build status ------------ -[![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) +[![python-tabulate](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.yml/badge.svg)](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.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 ------------- From 97b67f374871c5d1dc706072573f08f3acad7295 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 16:54:23 +0200 Subject: [PATCH 47/78] add GitHub Action workflow lint which runs pre_commit actions --- .github/workflows/lint.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..7e7c906 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,26 @@ +name: lint + +on: + - push + - pull_request + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.12'] + + steps: + - uses: actions/checkout@v4 + - 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 + - name: Run linters + run: | + tox -e lint From 2855363cf7d8ad088b55a45bd16116f1edef24e8 Mon Sep 17 00:00:00 2001 From: Dan Nicholson Date: Thu, 26 Sep 2024 16:49:11 +0100 Subject: [PATCH 48/78] Fix doctests for colon_grid --- tabulate/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 5acc98f..b0d9612 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1862,7 +1862,6 @@ def tabulate( >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], ... ["strings", "numbers"], "colon_grid")) - +-----------+-----------+ | strings | numbers | +:==========+:==========+ @@ -1873,8 +1872,7 @@ def tabulate( >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], ... ["strings", "numbers"], "colon_grid", - ... colalign=["right, left"])) - + ... colalign=["right", "left"])) +-----------+-----------+ | strings | numbers | +==========:+:==========+ From 049d7d9701d70a0b05c405b574f0b69925ac51fa Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 18:31:44 +0200 Subject: [PATCH 49/78] apply black to the PR branch and fix lint errors --- README.md | 2 +- tabulate/__init__.py | 42 ++++++++++++++++++++++++++++++++--------- test/common.py | 2 ++ test/test_output.py | 16 ++++++++++++---- test/test_regression.py | 1 + 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b596cf5..52c7644 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ 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}], +>>> print(tabulate([{1: "Alice", 2: 24}, {1: "Bob", 2: 19}], ... headers={1: "Name", 2: "Age"})) Name Age ------ ----- diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 63d8d32..598cb4e 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -160,7 +160,6 @@ def _grid_line_with_colons(colwidths, colaligns): return "+" + "+".join(segments) + "+" - def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns): alignment = { "left": "", @@ -1160,10 +1159,12 @@ def _align_column( has_invisible=True, enable_widechars=False, is_multiline=False, - preserve_whitespace=False + preserve_whitespace=False, ): """[string] -> [padded_string]""" - strings, padfn = _align_column_choose_padfn(strings, alignment, has_invisible, preserve_whitespace) + strings, padfn = _align_column_choose_padfn( + strings, alignment, has_invisible, preserve_whitespace + ) width_fn = _align_column_choose_width_fn( has_invisible, enable_widechars, is_multiline ) @@ -1269,15 +1270,21 @@ 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) + 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') + 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: @@ -2322,7 +2329,15 @@ def tabulate( if tablefmt == "colon_grid": aligns_copy = ["left"] * len(cols) cols = [ - _align_column(c, a, minw, has_invisible, enable_widechars, is_multiline, preserve_whitespace) + _align_column( + c, + a, + minw, + has_invisible, + enable_widechars, + is_multiline, + preserve_whitespace, + ) for c, a, minw in zip(cols, aligns_copy, minwidths) ] @@ -2819,7 +2834,16 @@ def _main(): opts, args = getopt.getopt( sys.argv[1:], "h1o:s:F:I:f:", - ["help", "header", "output=", "sep=", "float=", "int=", "colalign=", "format="], + [ + "help", + "header", + "output=", + "sep=", + "float=", + "int=", + "colalign=", + "format=", + ], ) except getopt.GetoptError as e: print(e) diff --git a/test/common.py b/test/common.py index ca5a069..fe33259 100644 --- a/test/common.py +++ b/test/common.py @@ -2,11 +2,13 @@ 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) assert expected == result + def assert_in(result, expected_set): nums = range(1, len(expected_set) + 1) for i, expected in zip(nums, expected_set): diff --git a/test/test_output.py b/test/test_output.py index c90f90c..059742c 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1,7 +1,6 @@ """Test output of the various forms of tabular data.""" from pytest import mark -import tabulate as tabulate_module from common import assert_equal, raises, skip, check_warnings from tabulate import tabulate, simple_separated_format, SEPARATING_LINE @@ -1459,7 +1458,12 @@ def test_colon_grid(): "+------+------+", ] ) - result = tabulate([[3, 4]], headers=("H1", "H2"), tablefmt="colon_grid", colalign=["right", "center"]) + result = tabulate( + [[3, 4]], + headers=("H1", "H2"), + tablefmt="colon_grid", + colalign=["right", "center"], + ) assert_equal(expected, result) @@ -1482,7 +1486,9 @@ def test_colon_grid_wide_characters(): "+-----------+---------+", ] ) - result = tabulate(_test_table, headers, tablefmt="colon_grid", colalign=["left", "right"]) + result = tabulate( + _test_table, headers, tablefmt="colon_grid", colalign=["left", "right"] + ) assert_equal(expected, result) @@ -2773,7 +2779,9 @@ def test_intfmt_with_string_as_integer(): @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") + 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) diff --git a/test/test_regression.py b/test/test_regression.py index d2fc3a0..5289db0 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -513,6 +513,7 @@ def test_numpy_int64_as_integer(): except ImportError: raise skip("") + def test_empty_table_with_colalign(): "Regression: empty table with colalign kwarg" table = tabulate([], ["a", "b", "c"], colalign=("center", "left", "left", "center")) From 54ac10757ac23a5b9bd4f483f35730a5d9dea437 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 18:33:50 +0200 Subject: [PATCH 50/78] add preserve_whitespace to test_tabulate_signature --- test/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_api.py b/test/test_api.py index e658e82..062573c 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -50,6 +50,7 @@ def test_tabulate_signature(): ("disable_numparse", False), ("colglobalalign", None), ("colalign", None), + ("preserve_whitespace", False), ("maxcolwidths", None), ("headersglobalalign", None), ("headersalign", None), From 0d5d30e81a98caed4fc867e76ec5ca10cb5ff20f Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 18:38:36 +0200 Subject: [PATCH 51/78] update README according to the new preserve_whitespace usage --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 52c7644..089f20e 100644 --- a/README.md +++ b/README.md @@ -739,13 +739,8 @@ column, in which case every column may have different number formatting: ### Text formatting By default, `tabulate` removes leading and trailing whitespace from text -columns. To disable whitespace removal, set the global module-level flag -`PRESERVE_WHITESPACE`: - -```python -import tabulate -tabulate.PRESERVE_WHITESPACE = True -``` +columns. To disable whitespace removal, pass `preserve_whitespace=True`. +Older versions of the library used a global module-level flag PRESERVE_WHITESPACE. ### Wide (fullwidth CJK) symbols From 09550a7b2eeaf7de723c10bbffe4c941a2d860db Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:44:25 +0200 Subject: [PATCH 52/78] Apply ruff/Perflint rule PERF402 https://docs.astral.sh/ruff/rules/manual-list-copy/ --- tabulate/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 11bb865..9e76a52 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1108,8 +1108,7 @@ def _flat_list(nested_list): ret = [] for item in nested_list: if isinstance(item, list): - for subitem in item: - ret.append(subitem) + ret.extend(item) else: ret.append(item) return ret From e8b511d7bcd547deb8b9d623394a44b8c3960ea7 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Thu, 26 Sep 2024 18:47:33 +0200 Subject: [PATCH 53/78] rename unitest workflow --- .github/workflows/tabulate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml index 9adabb7..eeb96b7 100644 --- a/.github/workflows/tabulate.yml +++ b/.github/workflows/tabulate.yml @@ -1,4 +1,4 @@ -name: python-tabulate +name: pytest on: - push From 1f49977cce900e9414600ae11a25750bec8e5801 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 29 Sep 2023 10:07:58 +0200 Subject: [PATCH 54/78] Fix typos found by codespell --- tabulate/__init__.py | 2 +- test/test_input.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index b7510b2..2ff8f8f 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2647,7 +2647,7 @@ def _update_lines(self, lines, new_line): else: # A single reset code resets everything self._active_codes = [] - # Always ensure each line is color terminted if any colors are + # Always ensure each line is color terminated if any colors are # still active, otherwise colors will bleed into other cells on the console if len(self._active_codes) > 0: new_line = new_line + _ansi_color_reset_code diff --git a/test/test_input.py b/test/test_input.py index 721d03a..b910a34 100644 --- a/test/test_input.py +++ b/test/test_input.py @@ -11,7 +11,7 @@ def test_iterable_of_iterables(): - "Input: an interable of iterables." + "Input: an iterable of iterables." ii = iter(map(lambda x: iter(x), [range(5), range(5, 0, -1)])) expected = "\n".join( ["- - - - -", "0 1 2 3 4", "5 4 3 2 1", "- - - - -"] @@ -21,7 +21,7 @@ def test_iterable_of_iterables(): def test_iterable_of_iterables_headers(): - "Input: an interable of iterables with headers." + "Input: an iterable of iterables with headers." ii = iter(map(lambda x: iter(x), [range(5), range(5, 0, -1)])) expected = "\n".join( [ @@ -36,7 +36,7 @@ def test_iterable_of_iterables_headers(): def test_iterable_of_iterables_firstrow(): - "Input: an interable of iterables with the first row as headers" + "Input: an iterable of iterables with the first row as headers" ii = iter(map(lambda x: iter(x), ["abcde", range(5), range(5, 0, -1)])) expected = "\n".join( [ From ca385d2ed23a405ea18bc0fb9ce1fe5327867e8a Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:46:29 +0200 Subject: [PATCH 55/78] A round of black to pass CI tests --- test/test_internal.py | 13 +++++++++++-- test/test_output.py | 1 + test/test_regression.py | 2 +- test/test_textwrapper.py | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/test/test_internal.py b/test/test_internal.py index e7564d3..17107c6 100644 --- a/test/test_internal.py +++ b/test/test_internal.py @@ -180,7 +180,9 @@ def test_wrap_text_wide_chars(): except ImportError: skip("test_wrap_text_wide_chars is skipped") - rows = [["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"]] + rows = [ + ["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"] + ] widths = [5, 20] expected = [ [ @@ -244,7 +246,14 @@ def test_wrap_text_to_colwidths_colors_wide_char(): except ImportError: skip("test_wrap_text_to_colwidths_colors_wide_char is skipped") - data = [[("\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m")]] + data = [ + [ + ( + "\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" + " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m" + ) + ] + ] result = T._wrap_text_to_colwidths(data, [30]) expected = [ diff --git a/test/test_output.py b/test/test_output.py index 059742c..68b5e55 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -1,4 +1,5 @@ """Test output of the various forms of tabular data.""" + from pytest import mark from common import assert_equal, raises, skip, check_warnings diff --git a/test/test_regression.py b/test/test_regression.py index 584ea39..bf26247 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -490,7 +490,7 @@ def test_preserve_line_breaks_with_maxcolwidths(): def test_maxcolwidths_accepts_list_or_tuple(): "Regression: maxcolwidths can accept a list or a tuple (github issue #214)" - table = [["lorem ipsum dolor sit amet"]*3] + table = [["lorem ipsum dolor sit amet"] * 3] expected = "\n".join( [ "+-------------+----------+----------------------------+", diff --git a/test/test_textwrapper.py b/test/test_textwrapper.py index b315a5c..1f134fd 100644 --- a/test/test_textwrapper.py +++ b/test/test_textwrapper.py @@ -1,4 +1,5 @@ """Discretely test functionality of our custom TextWrapper""" + import datetime from tabulate import _CustomTextWrap as CTW, tabulate From de466cb48fc2eaeb033b283d0081093dc51b5dd2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:20:35 -0700 Subject: [PATCH 56/78] Add support for Python 3.13 --- .github/workflows/tabulate.yml | 3 ++- pyproject.toml | 1 + tox.ini | 19 ++++++++++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml index eeb96b7..e75466b 100644 --- a/.github/workflows/tabulate.yml +++ b/.github/workflows/tabulate.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: actions/checkout@v4 @@ -17,6 +17,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + allow-prereleases: true - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/pyproject.toml b/pyproject.toml index 8cb6216..4144f9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries", ] requires-python = ">=3.9" diff --git a/tox.ini b/tox.ini index a7fe513..4ab8244 100644 --- a/tox.ini +++ b/tox.ini @@ -8,7 +8,7 @@ # for testing and it is disabled by default. [tox] -envlist = lint, py{38, 39, 310, 311, 312} +envlist = lint, py{38, 39, 310, 311, 312, 313} isolated_build = True [gh] @@ -17,6 +17,7 @@ python = 3.10: py310-extra 3.11: py311-extra 3.12: py312-extra + 3.13: py313-extra [testenv] commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} @@ -113,6 +114,22 @@ deps = pandas wcwidth +[testenv:py313] +basepython = python3.13 +commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +deps = + pytest + +[testenv:py313-extra] +basepython = python3.13 +setenv = PYTHONDEVMODE = 1 +commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +deps = + pytest + numpy + pandas + wcwidth + [flake8] max-complexity = 22 max-line-length = 99 From 571bc370aa22729440194e6fb7dc149cd169d5fa Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:21:41 -0700 Subject: [PATCH 57/78] Bump GitHub Actions --- .github/workflows/lint.yml | 2 +- .github/workflows/tabulate.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7e7c906..0093303 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml index e75466b..fd6004b 100644 --- a/.github/workflows/tabulate.yml +++ b/.github/workflows/tabulate.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} allow-prereleases: true From 29e0b1f97f012cdc97390b714ebc9c2e2c399774 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 26 Sep 2024 12:22:19 -0700 Subject: [PATCH 58/78] Fix lint --- test/test_regression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_regression.py b/test/test_regression.py index 584ea39..bf26247 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -490,7 +490,7 @@ def test_preserve_line_breaks_with_maxcolwidths(): def test_maxcolwidths_accepts_list_or_tuple(): "Regression: maxcolwidths can accept a list or a tuple (github issue #214)" - table = [["lorem ipsum dolor sit amet"]*3] + table = [["lorem ipsum dolor sit amet"] * 3] expected = "\n".join( [ "+-------------+----------+----------------------------+", From 813edee611c0b007940c3287a888408f33949ddd Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 09:55:46 +0200 Subject: [PATCH 59/78] update README and doctest descriptions of colon_grid introduced in pr #341 --- README.md | 16 ++++++++++++++++ tabulate/__init__.py | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 666537a..cab553e 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,22 @@ corresponds to the `pipe` format without alignment colons: ╘════════╧═══════╛ ``` +`colon_grid` is similar to `grid` but uses colons only to define +columnwise content alignment , without whitespace padding, +similar the alignment specification of Pandoc `grid_tables`: + + >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], + ... ["strings", "numbers"], "colon_grid", + ... colalign=["right", "left"])) + +-----------+-----------+ + | strings | numbers | + +==========:+:==========+ + | spam | 41.9999 | + +-----------+-----------+ + | eggs | 451 | + +-----------+-----------+ + + `outline` is the same as the `grid` format but doesn't draw lines between rows: >>> print(tabulate(table, headers, tablefmt="outline")) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index b7510b2..125c477 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1891,7 +1891,8 @@ def tabulate( ╘═══════════╧═══════════╛ "colon_grid" is similar to "grid" but uses colons only to define - columnwise content alignment, with no whitespace padding: + columnwise content alignment, without whitespace padding, + similar to the alignment specification of Pandoc `grid_tables`: >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], ... ["strings", "numbers"], "colon_grid")) From 3cee6568c7a2e8dd6379f11bba2c532f6acc5e3d Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 10:58:47 +0200 Subject: [PATCH 60/78] update the list of contributors in README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cab553e..3af3caa 100644 --- a/README.md +++ b/README.md @@ -1156,7 +1156,9 @@ endolith, Dominic Davis-Foster, pavlocat, Daniel Aslau, paulc, Felix Yan, Shane Loretz, Frank Busse, Harsh Singh, Derek Weitzel, Vladimir Vrzić, 서승우 (chrd5273), Georgy Frolov, Christian Cwienk, Bart Broere, Vilhelm Prytz, Alexander Gažo, Hugo van Kemenade, -jamescooke, Matt Warner, Jérôme Provensal, Kevin Deldycke, +jamescooke, Matt Warner, Jérôme Provensal, Michał Górny, Kevin Deldycke, Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH, Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan, -Dimitri Papadopoulos, Élie Goudout, Racerroar888, Phill Zarfos. +Dimitri Papadopoulos, Élie Goudout, Racerroar888, Phill Zarfos, +Keyacom, Andrew Coffey, Arpit Jain, Israel Roldan, ilya112358, +Dan Nicholson, Frederik Scheerer, cdar07 (cdar), Racerroar888. \ No newline at end of file From 68292bd4c85557551c4f664f42705baa9d70243f Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 09:57:12 +0200 Subject: [PATCH 61/78] update CHANGELOG for 0.10.0 version --- CHANGELOG | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index a18a5a1..2737441 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,10 @@ -- 0.9.1: Future version. +- 0.10.0: Add support for Python 3.11, 3.12, 3.13. + Drop support for Python 3.7, 3.8. + PRESERVE_STERILITY global is replaced with preserve_sterility function argument. + New formatting options: headersglobalalign, headersalign, colglobalalign. + New output format: ``colon_grid`` (Pandoc grid_tables with alignment) + Various bug fixes. + Improved error messages. - 0.9.0: Drop support for Python 2.7, 3.5, 3.6. Migrate to pyproject.toml project layout (PEP 621). New output formats: `asciidoc`, various `*grid` and `*outline` formats. From 22683f924c2afe28533863e70dd9e8c8611b7e49 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 10:14:02 +0200 Subject: [PATCH 62/78] remove asciitable from benchmark.py (not installable on Python 3.11), add benchmark_requirements.txt --- benchmark.py | 9 --------- benchmark_requirements.txt | 2 ++ 2 files changed, 2 insertions(+), 9 deletions(-) create mode 100644 benchmark_requirements.txt diff --git a/benchmark.py b/benchmark.py index 8422f5c..a89b709 100644 --- a/benchmark.py +++ b/benchmark.py @@ -1,6 +1,5 @@ from timeit import timeit import tabulate -import asciitable import prettytable import texttable import sys @@ -9,7 +8,6 @@ from csv import writer from io import StringIO import tabulate -import asciitable import prettytable import texttable @@ -34,12 +32,6 @@ def run_prettytable(table): return str(pp) -def run_asciitable(table): - buf = StringIO() - asciitable.write(table, output=buf, Writer=asciitable.FixedWidth) - return buf.getvalue() - - def run_texttable(table): pp = texttable.Texttable() pp.set_cols_align(["l"] + ["r"]*9) @@ -61,7 +53,6 @@ def run_tabulate(table, widechars=False): methods = [ ("join with tabs and newlines", "join_table(table)"), ("csv to StringIO", "csv_table(table)"), - ("asciitable (%s)" % asciitable.__version__, "run_asciitable(table)"), ("tabulate (%s)" % tabulate.__version__, "run_tabulate(table)"), ( "tabulate (%s, WIDE_CHARS_MODE)" % tabulate.__version__, diff --git a/benchmark_requirements.txt b/benchmark_requirements.txt new file mode 100644 index 0000000..81086ef --- /dev/null +++ b/benchmark_requirements.txt @@ -0,0 +1,2 @@ +prettytable +texttable \ No newline at end of file From da896a81031fcb558e3e330503d979b57e3a3dd1 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 10:16:12 +0200 Subject: [PATCH 63/78] move benchmark files to benchmark/ directory --- benchmark.py => benchmark/benchmark.py | 0 benchmark_requirements.txt => benchmark/requirements.txt | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename benchmark.py => benchmark/benchmark.py (100%) rename benchmark_requirements.txt => benchmark/requirements.txt (100%) diff --git a/benchmark.py b/benchmark/benchmark.py similarity index 100% rename from benchmark.py rename to benchmark/benchmark.py diff --git a/benchmark_requirements.txt b/benchmark/requirements.txt similarity index 100% rename from benchmark_requirements.txt rename to benchmark/requirements.txt From dbfd4219da5bb7fccd50014c8afca4e42bab889a Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 10:16:26 +0200 Subject: [PATCH 64/78] update HOWTOPUBLISH instructions --- HOWTOPUBLISH | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/HOWTOPUBLISH b/HOWTOPUBLISH index 795fc73..ea0bdca 100644 --- a/HOWTOPUBLISH +++ b/HOWTOPUBLISH @@ -1,7 +1,14 @@ # update contributors and CHANGELOG in README +python -m pre_commit run -a # and then commit changes +tox -e py39-extra,py310-extra,py311-extra,py312-extra,py313-extra # tag version release -python3 benchmark.py # then update README -tox -e py38-extra,py39-extra,py310-extra,py311-extra,py312-extra -python3 -m build -nswx . +python -m build -s # this will update tabulate/version.py +python -m pip install . # install tabulate in the current venv +python -m pip install -r benchmark/requirements.txt +python benchmark/benchmark.py # then update README +# move tag to the last commit +python -m build -s # update tabulate/version.py +python -m build -nswx . +git push # wait for all CI builds to succeed twine upload --repository-url https://test.pypi.org/legacy/ dist/* twine upload dist/* From c3fd38802e463686c58534233e7014df9fe684fe Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 10:24:04 +0200 Subject: [PATCH 65/78] update benchmark table in the README.md for 0.10.0 --- README.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3af3caa..3a75f26 100644 --- a/README.md +++ b/README.md @@ -1059,21 +1059,19 @@ simply joining lists of values with a tab, comma, or other separator. At the same time, `tabulate` is comparable to other table pretty-printers. Given a 10x10 table (a list of lists) of mixed text and -numeric data, `tabulate` appears to be slower than `asciitable`, and -faster than `PrettyTable` and `texttable` The following mini-benchmark -was run in Python 3.9.13 on Windows 10: - - ================================= ========== =========== - Table formatter time, μs rel. time - ================================= ========== =========== - csv to StringIO 12.5 1.0 - join with tabs and newlines 14.6 1.2 - asciitable (0.8.0) 192.0 15.4 - tabulate (0.9.0) 483.5 38.7 - tabulate (0.9.0, WIDE_CHARS_MODE) 637.6 51.1 - PrettyTable (3.4.1) 1080.6 86.6 - texttable (1.6.4) 1390.3 111.4 - ================================= ========== =========== +numeric data, `tabulate` appears to be faster than `PrettyTable` and `texttable`. +The following mini-benchmark was run in Python 3.11.9 on Windows 11 (x64): + + ================================== ========== =========== + Table formatter time, μs rel. time + ================================== ========== =========== + join with tabs and newlines 6.3 1.0 + csv to StringIO 6.6 1.0 + tabulate (0.10.0) 249.2 39.3 + tabulate (0.10.0, WIDE_CHARS_MODE) 325.6 51.4 + texttable (1.7.0) 579.3 91.5 + PrettyTable (3.11.0) 605.5 95.6 + ================================== ========== =========== Version history From 174a16326951fcf0111ab23352c811ac42214e59 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 11:07:45 +0200 Subject: [PATCH 66/78] update HOWTOPUBLISH (remember to push tags) --- HOWTOPUBLISH | 1 + 1 file changed, 1 insertion(+) diff --git a/HOWTOPUBLISH b/HOWTOPUBLISH index ea0bdca..ce7dcc7 100644 --- a/HOWTOPUBLISH +++ b/HOWTOPUBLISH @@ -10,5 +10,6 @@ python benchmark/benchmark.py # then update README python -m build -s # update tabulate/version.py python -m build -nswx . git push # wait for all CI builds to succeed +git push --tags # if CI builds succeed twine upload --repository-url https://test.pypi.org/legacy/ dist/* twine upload dist/* From 9ef6b8bf9e4176a36d255c34dee25cc8defb6142 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 11:18:14 +0200 Subject: [PATCH 67/78] ignore benchmark/benchmark.py in all CI tests --- .github/workflows/tabulate.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml index fd6004b..64cfbd8 100644 --- a/.github/workflows/tabulate.yml +++ b/.github/workflows/tabulate.yml @@ -24,4 +24,4 @@ jobs: python -m pip install pytest numpy pandas - name: Run tests run: | - pytest -v --doctest-modules --ignore benchmark.py + pytest -v --doctest-modules --ignore benchmark/benchmark.py diff --git a/appveyor.yml b/appveyor.yml index 12bd772..64463bd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,7 @@ test_script: # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. #- "build.cmd %PYTHON%\\python.exe setup.py test" - - "%PYTHON%\\python.exe -m pytest -v --doctest-modules --ignore benchmark.py" + - "%PYTHON%\\python.exe -m pytest -v --doctest-modules --ignore benchmark\benchmark.py" after_test: # This step builds your wheels. From 46b35b44a206bc2e84208fe7159fba632a301ca8 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Fri, 27 Sep 2024 11:25:09 +0200 Subject: [PATCH 68/78] ignore benchmark folder in all tox environments --- tox.ini | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index 4ab8244..9605e79 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ python = 3.13: py313-extra [testenv] -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest passenv = @@ -35,13 +35,13 @@ deps = [testenv:py38] basepython = python3.8 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest [testenv:py38-extra] basepython = python3.8 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest numpy @@ -51,13 +51,13 @@ deps = [testenv:py39] basepython = python3.9 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest [testenv:py39-extra] basepython = python3.9 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest numpy @@ -67,14 +67,14 @@ deps = [testenv:py310] basepython = python3.10 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest [testenv:py310-extra] basepython = python3.10 setenv = PYTHONDEVMODE = 1 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest numpy @@ -84,14 +84,14 @@ deps = [testenv:py311] basepython = python3.11 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest [testenv:py311-extra] basepython = python3.11 setenv = PYTHONDEVMODE = 1 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest numpy @@ -100,14 +100,14 @@ deps = [testenv:py312] basepython = python3.12 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest [testenv:py312-extra] basepython = python3.12 setenv = PYTHONDEVMODE = 1 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest numpy @@ -116,14 +116,14 @@ deps = [testenv:py313] basepython = python3.13 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest [testenv:py313-extra] basepython = python3.13 setenv = PYTHONDEVMODE = 1 -commands = pytest -v --doctest-modules --ignore benchmark.py {posargs} +commands = pytest -v --doctest-modules --ignore benchmark {posargs} deps = pytest numpy From 8c9c9e75c9a7da858e8ecf72236ee021c69e2d36 Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Thu, 20 Jul 2023 14:16:55 -0600 Subject: [PATCH 69/78] Support better type deduction o Empty/None values are ignored for deducing the type of a column o Comma-separated numbers are allowed in for int and float types --- tabulate/__init__.py | 35 ++++++++++++++++++++++++++++------- test/test_output.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index c349a79..8642c8d 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -933,8 +933,13 @@ def _isbool(string): def _type(string, has_invisible=True, numparse=True): """The least generic type (type(None), int, float, str, unicode). + Treats empty string as missing for the purposes of type deduction, so as to not influence + the type of an otherwise complete column; does *not* result in missingval replacement! + >>> _type(None) is type(None) True + >>> _type("") is type(None) + True >>> _type("foo") is type("") True >>> _type("1") is type(1) @@ -949,15 +954,26 @@ def _type(string, has_invisible=True, numparse=True): if has_invisible and isinstance(string, (str, bytes)): string = _strip_ansi(string) - if string is None: + if string is None or (isinstance(string, (bytes, str)) and not string): return type(None) elif hasattr(string, "isoformat"): # datetime.datetime, date, and time return str elif _isbool(string): return bool - elif _isint(string) and numparse: + elif numparse and ( + _isint(string) or ( + isinstance(string, str) + and _isnumber_with_thousands_separator(string) + and '.' not in string + ) + ): return int - elif _isnumber(string) and numparse: + elif numparse and ( + _isnumber(string) or ( + isinstance(string, str) + and _isnumber_with_thousands_separator(string) + ) + ): return float elif isinstance(string, bytes): return bytes @@ -1251,7 +1267,7 @@ def _column_type(strings, has_invisible=True, numparse=True): def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): - """Format a value according to its type. + """Format a value according to its deduced type. Empty values are deemed valid for any type. Unicode is supported: @@ -1264,6 +1280,8 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): """ # noqa if val is None: return missingval + if isinstance(val, (bytes, str)) and not val: + return "" if valtype is str: return f"{val}" @@ -1298,6 +1316,8 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): formatted_val = format(float(raw_val), floatfmt) return val.replace(raw_val, formatted_val) else: + if isinstance(val,str) and ',' in val: + val = val.replace(',', '') # handle thousands-separators return format(float(val), floatfmt) else: return f"{val}" @@ -1592,9 +1612,10 @@ def _wrap_text_to_colwidths(list_of_lists, colwidths, numparses=True): if width is not None: wrapper = _CustomTextWrap(width=width) - # Cast based on our internal type handling - # Any future custom formatting of types (such as datetimes) - # may need to be more explicit than just `str` of the object + # Cast based on our internal type handling. Any future custom + # formatting of types (such as datetimes) may need to be more + # explicit than just `str` of the object. Also doesn't work for + # custom floatfmt/intfmt, nor with any missing/blank cells. casted_cell = ( str(cell) if _isnumber(cell) else _type(cell, numparse)(cell) ) diff --git a/test/test_output.py b/test/test_output.py index 68b5e55..5b94e82 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2824,6 +2824,13 @@ def test_floatfmt(): assert_equal(expected, result) +def test_floatfmt_thousands(): + "Output: floating point format" + result = tabulate([["1.23456789"], [1.0], ["1,234.56"]], floatfmt=".3f", tablefmt="plain") + expected = " 1.235\n 1.000\n1234.560" + assert_equal(expected, result) + + def test_floatfmt_multi(): "Output: floating point format different for each column" result = tabulate( @@ -2964,6 +2971,32 @@ def test_missingval_multi(): assert_equal(expected, result) +def test_column_emptymissing_deduction(): + "Missing or empty/blank values shouldn't change type deduction of rest of column" + from fractions import Fraction + + test_table = [ + [None, "1.23423515351", Fraction(1, 3)], + [Fraction(56789, 1000000), 12345.1, b"abc"], + ["", b"", None], + [Fraction(10000, 3), None, ""], + ] + result = tabulate( + test_table, + floatfmt=",.5g", + missingval="?", + ) + print(f"\n{result}") + expected = """\ +------------ ----------- --- + ? 1.2342 1/3 + 0.056789 12,345 abc + ? +3,333.3 ? +------------ ----------- ---""" + assert_equal(expected, result) + + def test_column_alignment(): "Output: custom alignment for text and numbers" expected = "\n".join(["----- ---", "Alice 1", " Bob 333", "----- ---"]) From b39a162ecbb2e9898c2f759e01ddd21f4af7f316 Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Mon, 7 Oct 2024 18:01:46 -0600 Subject: [PATCH 70/78] Add back in check for SEPARATING_LINE --- tabulate/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 6cf6ea3..296974d 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2561,9 +2561,10 @@ def _format_table( if rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: # initial rows with a line below for row, ralign in zip(rows[:-1], rowaligns): - append_row( - lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow, rowalign=ralign - ) + if row != SEPARATING_LINE: + append_row( + lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow, rowalign=ralign + ) _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) # the last row without a line below append_row( From 588f4121f669d3cde4a5eae741f40bd014eac331 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 8 Oct 2024 11:36:19 +0200 Subject: [PATCH 71/78] fix backslash escaping in appveyor.yml --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 64463bd..85881d4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -33,7 +33,7 @@ test_script: # the interpreter you're using - Appveyor does not do anything special # to put the Python version you want to use on PATH. #- "build.cmd %PYTHON%\\python.exe setup.py test" - - "%PYTHON%\\python.exe -m pytest -v --doctest-modules --ignore benchmark\benchmark.py" + - "%PYTHON%\\python.exe -m pytest -v --doctest-modules --ignore benchmark\\benchmark.py" after_test: # This step builds your wheels. From ae3d485c734e732d9ace6eefc4fb433ef8ae1005 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 8 Oct 2024 11:53:07 +0200 Subject: [PATCH 72/78] add windows build to github actions job matrix --- .github/workflows/tabulate.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml index 64cfbd8..9b113c9 100644 --- a/.github/workflows/tabulate.yml +++ b/.github/workflows/tabulate.yml @@ -6,10 +6,11 @@ on: jobs: build: - runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + os: ["ubuntu-latest", "windows-latest"] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 From cf45298f3f01ad05264b9ab9a2ad4cbd2c32d9aa Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 8 Oct 2024 11:56:26 +0200 Subject: [PATCH 73/78] apply black to fix linting errors --- HOWTOPUBLISH | 4 ++-- tabulate/__init__.py | 33 +++++++++++++++++++++------------ test/test_internal.py | 13 ++----------- test/test_output.py | 4 +++- 4 files changed, 28 insertions(+), 26 deletions(-) diff --git a/HOWTOPUBLISH b/HOWTOPUBLISH index ce7dcc7..29c4545 100644 --- a/HOWTOPUBLISH +++ b/HOWTOPUBLISH @@ -4,12 +4,12 @@ tox -e py39-extra,py310-extra,py311-extra,py312-extra,py313-extra # tag version release python -m build -s # this will update tabulate/version.py python -m pip install . # install tabulate in the current venv -python -m pip install -r benchmark/requirements.txt +python -m pip install -r benchmark/requirements.txt python benchmark/benchmark.py # then update README # move tag to the last commit python -m build -s # update tabulate/version.py python -m build -nswx . -git push # wait for all CI builds to succeed +git push # wait for all CI builds to succeed git push --tags # if CI builds succeed twine upload --repository-url https://test.pypi.org/legacy/ dist/* twine upload dist/* diff --git a/tabulate/__init__.py b/tabulate/__init__.py index d6a977a..0d249ac 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -961,18 +961,17 @@ def _type(string, has_invisible=True, numparse=True): elif _isbool(string): return bool elif numparse and ( - _isint(string) or ( + _isint(string) + or ( isinstance(string, str) and _isnumber_with_thousands_separator(string) - and '.' not in string + and "." not in string ) ): return int elif numparse and ( - _isnumber(string) or ( - isinstance(string, str) - and _isnumber_with_thousands_separator(string) - ) + _isnumber(string) + or (isinstance(string, str) and _isnumber_with_thousands_separator(string)) ): return float elif isinstance(string, bytes): @@ -1316,8 +1315,8 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True): formatted_val = format(float(raw_val), floatfmt) return val.replace(raw_val, formatted_val) else: - if isinstance(val,str) and ',' in val: - val = val.replace(',', '') # handle thousands-separators + if isinstance(val, str) and "," in val: + val = val.replace(",", "") # handle thousands-separators return format(float(val), floatfmt) else: return f"{val}" @@ -2584,7 +2583,12 @@ def _format_table( for row, ralign in zip(rows[:-1], rowaligns): if row != SEPARATING_LINE: append_row( - lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow, rowalign=ralign + lines, + pad_row(row, pad), + padded_widths, + colaligns, + fmt.datarow, + rowalign=ralign, ) _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) # the last row without a line below @@ -2610,7 +2614,9 @@ def _format_table( if _is_separating_line(row): _append_line(lines, padded_widths, colaligns, separating_line) else: - append_row(lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow) + append_row( + lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow + ) if fmt.linebelow and "linebelow" not in hidden: _append_line(lines, padded_widths, colaligns, fmt.linebelow) @@ -2705,11 +2711,14 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width): if _ansi_codes.search(chunk) is not None: for group, _, _, _ in _ansi_codes.findall(chunk): escape_len = len(group) - if group in chunk[last_group: i + total_escape_len + escape_len - 1]: + if ( + group + in chunk[last_group : i + total_escape_len + escape_len - 1] + ): total_escape_len += escape_len found = _ansi_codes.search(chunk[last_group:]) last_group += found.end() - cur_line.append(chunk[: i + total_escape_len - 1]) + cur_line.append(chunk[: i + total_escape_len - 1]) reversed_chunks[-1] = chunk[i + total_escape_len - 1 :] # Otherwise, we have to preserve the long word intact. Only add diff --git a/test/test_internal.py b/test/test_internal.py index 17107c6..e7564d3 100644 --- a/test/test_internal.py +++ b/test/test_internal.py @@ -180,9 +180,7 @@ def test_wrap_text_wide_chars(): except ImportError: skip("test_wrap_text_wide_chars is skipped") - rows = [ - ["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"] - ] + rows = [["청자청자청자청자청자", "약간 감싸면 더 잘 보일 수있는 다소 긴 설명입니다"]] widths = [5, 20] expected = [ [ @@ -246,14 +244,7 @@ def test_wrap_text_to_colwidths_colors_wide_char(): except ImportError: skip("test_wrap_text_to_colwidths_colors_wide_char is skipped") - data = [ - [ - ( - "\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" - " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m" - ) - ] - ] + data = [[("\033[31m약간 감싸면 더 잘 보일 수있는 다소 긴" " 설명입니다 설명입니다 설명입니다 설명입니다 설명\033[0m")]] result = T._wrap_text_to_colwidths(data, [30]) expected = [ diff --git a/test/test_output.py b/test/test_output.py index bba7622..e3d369a 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2863,7 +2863,9 @@ def test_floatfmt(): def test_floatfmt_thousands(): "Output: floating point format" - result = tabulate([["1.23456789"], [1.0], ["1,234.56"]], floatfmt=".3f", tablefmt="plain") + result = tabulate( + [["1.23456789"], [1.0], ["1,234.56"]], floatfmt=".3f", tablefmt="plain" + ) expected = " 1.235\n 1.000\n1234.560" assert_equal(expected, result) From a81f6b0a032f84f9225bdfdf375d255ab8e847d3 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 8 Oct 2024 11:59:15 +0200 Subject: [PATCH 74/78] update the list of contributors --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a75f26..28ef166 100644 --- a/README.md +++ b/README.md @@ -1159,4 +1159,5 @@ Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH, Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan, Dimitri Papadopoulos, Élie Goudout, Racerroar888, Phill Zarfos, Keyacom, Andrew Coffey, Arpit Jain, Israel Roldan, ilya112358, -Dan Nicholson, Frederik Scheerer, cdar07 (cdar), Racerroar888. \ No newline at end of file +Dan Nicholson, Frederik Scheerer, cdar07 (cdar), Racerroar888, +Perry Kundert. From 3c43178313bc80dc45337e3931c4c0cfdfc032f4 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 8 Oct 2024 12:00:07 +0200 Subject: [PATCH 75/78] add macos build to github actions job matrix --- .github/workflows/tabulate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tabulate.yml b/.github/workflows/tabulate.yml index 9b113c9..c459484 100644 --- a/.github/workflows/tabulate.yml +++ b/.github/workflows/tabulate.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] - os: ["ubuntu-latest", "windows-latest"] + os: ["ubuntu-latest", "windows-latest", "macos-latest"] runs-on: ${{ matrix.os }} steps: From 1de098247fb374521448893765f0a4962cdf3147 Mon Sep 17 00:00:00 2001 From: Sergey Astanin Date: Tue, 8 Oct 2024 12:08:25 +0200 Subject: [PATCH 76/78] remove appveyor.yml and appveyor badge --- README.md | 2 +- appveyor.yml | 51 --------------------------------------------------- 2 files changed, 1 insertion(+), 52 deletions(-) delete mode 100644 appveyor.yml diff --git a/README.md b/README.md index 28ef166..b1b56d5 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ pip install tabulate Build status ------------ -[![python-tabulate](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.yml/badge.svg)](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.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) +[![python-tabulate](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.yml/badge.svg)](https://github.com/astanin/python-tabulate/actions/workflows/tabulate.yml) Library usage ------------- diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 85881d4..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,51 +0,0 @@ -image: Visual Studio 2022 -environment: - - matrix: - - # For Python versions available on Appveyor, see - # https://www.appveyor.com/docs/windows-images-software/#python - # 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:\\Python310" - #- PYTHON: "C:\\Python311" - #- PYTHON: "C:\\Python312" - - 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 - - "%PYTHON%\\python.exe -m pip install setuptools --upgrade" - # We need wheel installed to build wheels - - "%PYTHON%\\python.exe -m pip install wheel --upgrade" - - "%PYTHON%\\python.exe -m pip install build setuptools_scm" - - "%PYTHON%\\python.exe -m pip install pytest numpy pandas" - -build: off - -test_script: - # Put your test command here. - # 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. - #- "build.cmd %PYTHON%\\python.exe setup.py test" - - "%PYTHON%\\python.exe -m pytest -v --doctest-modules --ignore benchmark\\benchmark.py" - -after_test: - # This step builds your wheels. - # 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 ." - -artifacts: - # bdist_wheel puts your built wheel in the dist directory - - path: dist\* - -#on_success: -# You can use this step to upload your artifacts to a public website. -# See Appveyor's documentation for more details. Or you can simply -# access your wheels from the Appveyor "artifacts" tab for your build. From c37de756a0ccfa9f23dae5aa2281e813f42fef02 Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Tue, 8 Oct 2024 09:21:09 -0600 Subject: [PATCH 77/78] Correct README.md examples and outputs using doctest --- README.md | 128 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index b1b56d5..70b6839 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Earth 6371 5973.6 Moon 1737 73.5 Mars 3390 641.85 ----- ------ ------------- + ``` The following tabular data types are supported: @@ -101,6 +102,7 @@ Sun 696000 1.9891e+09 Earth 6371 5973.6 Moon 1737 73.5 Mars 3390 641.85 + ``` If `headers="firstrow"`, then the first row of data is used: @@ -112,6 +114,7 @@ Name Age ------ ----- Alice 24 Bob 19 + ``` If `headers="keys"`, then the keys of a dictionary/dataframe, or column @@ -125,6 +128,7 @@ Name Age ------ ----- Alice 24 Bob 19 + ``` When data is a list of dictionaries, a dictionary can be passed as `headers` @@ -137,6 +141,7 @@ Name Age ------ ----- Alice 24 Bob 19 + ``` ### Row Indices @@ -154,6 +159,7 @@ or `showindex=False`. To add a custom row index column, pass 0 F 24 1 M 19 - - -- + ``` ### Table format @@ -210,6 +216,7 @@ item qty spam 42 eggs 451 bacon 0 + ``` `simple` is the default format (the default may change in future @@ -223,6 +230,7 @@ item qty spam 42 eggs 451 bacon 0 + ``` `github` follows the conventions of GitHub flavored Markdown. It @@ -230,11 +238,12 @@ corresponds to the `pipe` format without alignment colons: ```pycon >>> print(tabulate(table, headers, tablefmt="github")) -| item | qty | +| item | qty | |--------|-------| -| spam | 42 | -| eggs | 451 | -| bacon | 0 | +| spam | 42 | +| eggs | 451 | +| bacon | 0 | + ``` `grid` is like tables formatted by Emacs' @@ -252,6 +261,7 @@ corresponds to the `pipe` format without alignment colons: +--------+-------+ | bacon | 0 | +--------+-------+ + ``` `simple_grid` draws a grid using single-line box-drawing characters: @@ -333,6 +343,7 @@ corresponds to the `pipe` format without alignment colons: ├────────┼───────┤ │ bacon │ 0 │ ╘════════╧═══════╛ + ``` `colon_grid` is similar to `grid` but uses colons only to define @@ -437,6 +448,7 @@ similar the alignment specification of Pandoc `grid_tables`: spam | 42 eggs | 451 bacon | 0 + ``` `pretty` attempts to be close to the format emitted by the PrettyTables @@ -451,6 +463,7 @@ library: | eggs | 451 | | bacon | 0 | +-------+-----+ + ``` `psql` is like tables formatted by Postgres' psql cli: @@ -464,6 +477,7 @@ library: | eggs | 451 | | bacon | 0 | +--------+-------+ + ``` `pipe` follows the conventions of [PHP Markdown @@ -478,6 +492,7 @@ indicate column alignment: | spam | 42 | | eggs | 451 | | bacon | 0 | + ``` `asciidoc` formats data like a simple table of the @@ -488,11 +503,12 @@ format: >>> print(tabulate(table, headers, tablefmt="asciidoc")) [cols="8<,7>",options="header"] |==== -| item | qty -| spam | 42 -| eggs | 451 -| bacon | 0 +| item | qty +| spam | 42 +| eggs | 451 +| bacon | 0 |==== + ``` `orgtbl` follows the conventions of Emacs @@ -506,6 +522,7 @@ in the minor orgtbl-mode. Hence its name: | spam | 42 | | eggs | 451 | | bacon | 0 | + ``` `jira` follows the conventions of Atlassian Jira markup language: @@ -516,6 +533,7 @@ in the minor orgtbl-mode. Hence its name: | spam | 42 | | eggs | 451 | | bacon | 0 | + ``` `rst` formats data like a simple table of the @@ -531,6 +549,7 @@ spam 42 eggs 451 bacon 0 ====== ===== + ``` `mediawiki` format produces a table markup used in @@ -550,6 +569,7 @@ MediaWiki-based sites: |- | bacon || style="text-align: right;"| 0 |} + ``` `moinmoin` format produces a table markup used in @@ -557,20 +577,22 @@ MediaWiki-based sites: ```pycon >>> print(tabulate(table, headers, tablefmt="moinmoin")) -|| ''' item ''' || ''' quantity ''' || -|| spam || 41.999 || -|| eggs || 451 || -|| bacon || || +|| ''' item ''' || ''' qty ''' || +|| spam || 42 || +|| eggs || 451 || +|| bacon || 0 || + ``` `youtrack` format produces a table markup used in Youtrack tickets: ```pycon >>> print(tabulate(table, headers, tablefmt="youtrack")) -|| item || quantity || -| spam | 41.999 | -| eggs | 451 | -| bacon | | +|| item || qty || +| spam | 42 | +| eggs | 451 | +| bacon | 0 | + ``` `textile` format produces a table markup used in @@ -582,6 +604,7 @@ MediaWiki-based sites: |<. spam |>. 42 | |<. eggs |>. 451 | |<. bacon |>. 0 | + ``` `html` produces standard HTML markup as an html.escape'd str @@ -592,13 +615,16 @@ and a .str property so that the raw HTML remains accessible. ```pycon >>> print(tabulate(table, headers, tablefmt="html")) - + + +
item qty
spam 42
eggs 451
bacon 0
+ ``` `latex` format creates a `tabular` environment for LaTeX markup, @@ -616,6 +642,7 @@ correspondents: bacon & 0 \\ \hline \end{tabular} + ``` `latex_raw` behaves like `latex` but does not escape LaTeX commands and @@ -650,6 +677,7 @@ at a glance: 12345 1234.5 ---------- + ``` Compare this with a more common right alignment: @@ -663,6 +691,7 @@ Compare this with a more common right alignment: 12345 1234.5 ------ + ``` For `tabulate`, anything which can be parsed as a number is a number. @@ -671,7 +700,7 @@ comes in handy when reading a mixed table of text and numbers from a file: ```pycon ->>> import csv ; from StringIO import StringIO +>>> import csv; from io import StringIO >>> table = list(csv.reader(StringIO("spam, 42\neggs, 451\n"))) >>> table [['spam', ' 42'], ['eggs', ' 451']] @@ -680,16 +709,18 @@ file: spam 42 eggs 451 ---- ---- + ``` To disable this feature use `disable_numparse=True`. ```pycon ->>> print(tabulate.tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True)) +>>> print(tabulate([["Ver1", "18.0"], ["Ver2","19.2"]], tablefmt="simple", disable_numparse=True)) ---- ---- Ver1 18.0 Ver2 19.2 ---- ---- + ``` ### Custom column alignment @@ -704,6 +735,7 @@ Furthermore, you can define `colalign` for column-specific alignment as a list o 1 2 3 4 111 222 333 444 --- --- --- --- + ``` ### Custom header alignment @@ -714,11 +746,11 @@ Headers' alignment can be defined separately from columns'. Like for columns, yo ```pycon >>> print(tabulate([[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'))) - h e a d e r --- --- --- --- --- --- 1 2 3 4 5 6 111 222 333 444 555 666 + ``` ### Number formatting @@ -732,6 +764,7 @@ columns of decimal numbers. Use `floatfmt` named argument: pi 3.1416 e 2.7183 -- ------ + ``` `floatfmt` argument can be a list or a tuple of format strings, one per @@ -742,6 +775,7 @@ column, in which case every column may have different number formatting: --- ----- ------- 0.1 0.123 0.12345 --- ----- ------- + ``` `intfmt` works similarly for integers @@ -752,6 +786,33 @@ column, in which case every column may have different number formatting: b 90,000 - ------ + +### Type Deduction and Missing Values + +When `tabulate` sees numerical data (with our without comma separators), it +attempts to align the column on the decimal point. However, if it observes +non-numerical data in the column, it aligns it to the left by default. If +data is missing in a column (either None or empty values), the remaining +data in the column is used to infer the type: + +```pycon +>>> from fractions import Fraction +>>> test_table = [ +... [None, "1.23423515351", Fraction(1, 3)], +... [Fraction(56789, 1000000), 12345.1, b"abc"], +... ["", b"", None], +... [Fraction(10000, 3), None, ""], +... ] +>>> print(tabulate(test_table, floatfmt=",.5g", missingval="?")) +------------ ----------- --- + ? 1.2342 1/3 + 0.056789 12,345 abc + ? +3,333.3 ? +------------ ----------- --- + +``` + ### Text formatting By default, `tabulate` removes leading and trailing whitespace from text @@ -802,6 +863,7 @@ a multiline cell, and headers with a multiline cell: ```pycon >>> table = [["eggs",451],["more\nspam",42]] >>> headers = ["item\nname", "qty"] + ``` `plain` tables: @@ -813,6 +875,7 @@ name eggs 451 more 42 spam + ``` `simple` tables: @@ -825,6 +888,7 @@ name eggs 451 more 42 spam + ``` `grid` tables: @@ -840,6 +904,7 @@ spam | more | 42 | | spam | | +--------+-------+ + ``` `fancy_grid` tables: @@ -855,6 +920,7 @@ spam │ more │ 42 │ │ spam │ │ ╘════════╧═══════╛ + ``` `pipe` tables: @@ -867,6 +933,7 @@ spam | eggs | 451 | | more | 42 | | spam | | + ``` `orgtbl` tables: @@ -879,18 +946,19 @@ spam | eggs | 451 | | more | 42 | | spam | | + ``` `jira` tables: ```pycon >>> print(tabulate(table, headers, tablefmt="jira")) -| item | qty | -| name | | -|:-------|------:| +|| item || qty || +|| name || || | eggs | 451 | | more | 42 | | spam | | + ``` `presto` tables: @@ -903,6 +971,7 @@ spam eggs | 451 more | 42 spam | + ``` `pretty` tables: @@ -917,6 +986,7 @@ spam | more | 42 | | spam | | +------+-----+ + ``` `psql` tables: @@ -931,6 +1001,7 @@ spam | more | 42 | | spam | | +--------+-------+ + ``` `rst` tables: @@ -945,6 +1016,7 @@ eggs 451 more 42 spam ====== ===== + ``` Multiline cells are not well-supported for the other table formats. @@ -974,6 +1046,7 @@ the lines being wrapped would probably be significantly longer than this. | John Smith | Middle | | | Manager | +------------+---------+ + ``` ### Adding Separating lines @@ -1016,7 +1089,7 @@ hyperlinks is that column width will be based on the length of the URL _text_ ra itself (terminals would show this text). For example: >>> len('\x1b]8;;https://example.com\x1b\\example\x1b]8;;\x1b\\') # display length is 7, showing 'example' - 45 + 40 Usage of the command line utility @@ -1136,6 +1209,13 @@ tox -e lint See `tox.ini` file to learn how to use to test individual Python versions. +To test the "doctest" examples and their outputs in `README.md`: + +```shell +python3 -m pip install pytest-doctestplus[md] +python3 -m doctest README.md +``` + Contributors ------------ From 232ed74a4be2806bda886739a20a703b9c1043ea Mon Sep 17 00:00:00 2001 From: Perry Kundert Date: Tue, 8 Oct 2024 12:49:37 -0600 Subject: [PATCH 78/78] Begin trying to simplify _isnumber --- README.md | 14 +++++++++++++ tabulate/__init__.py | 48 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 70b6839..f3d0fa9 100644 --- a/README.md +++ b/README.md @@ -813,6 +813,20 @@ data in the column is used to infer the type: ``` +The deduced type (eg. str, float) influences the rendering of any types +that have alternative representations. For example, since `Fraction` has +methods `__str__` and `__float__` defined (and hence is convertible to a +`float` and also has a `str` representation), the appropriate +representation is selected for the column's deduced type. In order to not +lose precision accidentally, types having both an `__int__` and +`__float__` represention will be considered a `float`. + +Therefore, if your table contains types convertible to int/float but you'd +*prefer* they be represented as strings, or your strings *might* all look +like numbers such as "1e23": either convert them to the desired +representation before you `tabulate`, or ensure that the column always +contains at least one other `str`. + ### Text formatting By default, `tabulate` removes leading and trailing whitespace from text diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 0d249ac..b15d7f0 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -876,25 +876,55 @@ def _isconvertible(conv, string): def _isnumber(string): - """ + """Detects if something *could* be considered a numeric value, vs. just a string. + + This promotes types convertible to both int and float to be considered + a float. Note that, iff *all* values appear to be some form of numeric + value such as eg. "1e2", they would be considered numbers! + + The exception is things that appear to be numbers but overflow to + +/-inf, eg. "1e23456"; we'll have to exclude them explicitly. + + >>> _isnumber(123) + True + >>> _isnumber(123.45) + True >>> _isnumber("123.45") True >>> _isnumber("123") True >>> _isnumber("spam") False - >>> _isnumber("123e45678") + >>> _isnumber("123e45") + True + >>> _isnumber("123e45678") # evaluates equal to 'inf', but ... isn't False >>> _isnumber("inf") True + >>> from fractions import Fraction + >>> _isnumber(Fraction(1,3)) + True + """ - if not _isconvertible(float, string): - return False - elif isinstance(string, (str, bytes)) and ( - math.isinf(float(string)) or math.isnan(float(string)) - ): - return string.lower() in ["inf", "-inf", "nan"] - return True + return ( + # fast path + type(string) in (float, int) + # covers 'NaN', +/- 'inf', and eg. '1e2', as well as any type + # convertible to int/float. + or ( + _isconvertible(float, string) + and ( + # some other type convertible to float + not isinstance(string, (str, bytes)) + # or, a numeric string eg. "1e1...", "NaN", ..., but isn't + # just an over/underflow + or ( + not (math.isinf(float(string)) or math.isnan(float(string))) + or string.lower() in ["inf", "-inf", "nan"] + ) + ) + ) + ) def _isint(string, inttype=int):