Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Python 3.12 and drop EOL 3.7 #304

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion HOWTOPUBLISH
Original file line number Diff line number Diff line change
@@ -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/*
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1074,33 +1074,33 @@ 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

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.

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
Expand Down
10 changes: 2 additions & 8 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,13 @@ 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"
- PYTHON: "C:\\Python311-x64"
- PYTHON: "C:\\Python312-x64"

install:
# Newer setuptools is needed for proper support of pyproject.toml
Expand All @@ -29,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.
Expand All @@ -40,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 ."

Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
51 changes: 35 additions & 16 deletions tabulate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)) + '"']

Expand Down Expand Up @@ -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):
Expand All @@ -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 = []
Expand All @@ -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:

Expand All @@ -1331,7 +1332,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

Expand Down Expand Up @@ -1457,7 +1458,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
Expand Down Expand Up @@ -1600,7 +1601,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.


Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
)


Expand Down Expand Up @@ -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 []
Expand Down
3 changes: 2 additions & 1 deletion test/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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])

2 changes: 1 addition & 1 deletion test/test_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'", "你好"]
)
Expand Down
Loading