Skip to content

Commit b206ba9

Browse files
authored
Merge branch 'master' into fix-astanin#214-maxcolwidths-doesn't-accept-tuple
2 parents f078669 + 95ae5eb commit b206ba9

File tree

8 files changed

+182
-31
lines changed

8 files changed

+182
-31
lines changed

README.md

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -666,18 +666,31 @@ Ver2 19.2
666666

667667
### Custom column alignment
668668

669-
`tabulate` allows a custom column alignment to override the above. The
670-
`colalign` argument can be a list or a tuple of `stralign` named
671-
arguments. Possible column alignments are: `right`, `center`, `left`,
672-
`decimal` (only for numbers), and `None` (to disable alignment).
673-
Omitting an alignment uses the default. For example:
669+
`tabulate` allows a custom column alignment to override the smart alignment described above.
670+
Use `colglobalalign` to define a global setting. Possible alignments are: `right`, `center`, `left`, `decimal` (only for numbers).
671+
Furthermore, you can define `colalign` for column-specific alignment as a list or a tuple. Possible values are `global` (keeps global setting), `right`, `center`, `left`, `decimal` (only for numbers), `None` (to disable alignment). Missing alignments are treated as `global`.
674672

675673
```pycon
676-
>>> print(tabulate([["one", "two"], ["three", "four"]], colalign=("right",))
677-
----- ----
678-
one two
679-
three four
680-
----- ----
674+
>>> print(tabulate([[1,2,3,4],[111,222,333,444]], colglobalalign='center', colalign = ('global','left','right')))
675+
--- --- --- ---
676+
1 2 3 4
677+
111 222 333 444
678+
--- --- --- ---
679+
```
680+
681+
### Custom header alignment
682+
683+
Headers' alignment can be defined separately from columns'. Like for columns, you can use:
684+
- `headersglobalalign` to define a header-specific global alignment setting. Possible values are `right`, `center`, `left`, `None` (to follow column alignment),
685+
- `headersalign` list or tuple to further specify header-wise alignment. Possible values are `global` (keeps global setting), `same` (follow column alignment), `right`, `center`, `left`, `None` (to disable alignment). Missing alignments are treated as `global`.
686+
687+
```pycon
688+
>>> 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')))
689+
690+
h e a d e r
691+
--- --- --- --- --- ---
692+
1 2 3 4 5 6
693+
111 222 333 444 555 666
681694
```
682695

683696
### Number formatting
@@ -1123,5 +1136,4 @@ Bart Broere, Vilhelm Prytz, Alexander Gažo, Hugo van Kemenade,
11231136
jamescooke, Matt Warner, Jérôme Provensal, Kevin Deldycke,
11241137
Kian-Meng Ang, Kevin Patterson, Shodhan Save, cleoold, KOLANICH,
11251138
Vijaya Krishna Kasula, Furcy Pin, Christian Fibich, Shaun Duncan,
1126-
Dimitri Papadopoulos, Élie Goudout, Racerroar, Phill Zarfos.
1127-
1139+
Dimitri Papadopoulos, Élie Goudout, Racerroar888, Phill Zarfos.

appveyor.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ environment:
1515
- PYTHON: "C:\\Python38-x64"
1616
- PYTHON: "C:\\Python39-x64"
1717
- PYTHON: "C:\\Python310-x64"
18+
- PYTHON: "C:\\Python311-x64"
1819

1920
install:
2021
# Newer setuptools is needed for proper support of pyproject.toml

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ classifiers = [
1717
"Programming Language :: Python :: 3.8",
1818
"Programming Language :: Python :: 3.9",
1919
"Programming Language :: Python :: 3.10",
20+
"Programming Language :: Python :: 3.11",
2021
"Topic :: Software Development :: Libraries",
2122
]
2223
requires-python = ">=3.7"

tabulate/__init__.py

Lines changed: 64 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Pretty-print tabular data."""
22

3+
import warnings
34
from collections import namedtuple
45
from collections.abc import Iterable, Sized
56
from html import escape as htmlescape
@@ -1318,7 +1319,7 @@ def _bool(val):
13181319

13191320

13201321
def _normalize_tabular_data(tabular_data, headers, showindex="default"):
1321-
"""Transform a supported data type to a list of lists, and a list of headers.
1322+
"""Transform a supported data type to a list of lists, and a list of headers, with headers padding.
13221323
13231324
Supported tabular data types:
13241325
@@ -1498,13 +1499,12 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
14981499
pass
14991500

15001501
# pad with empty headers for initial columns if necessary
1502+
headers_pad = 0
15011503
if headers and len(rows) > 0:
1502-
nhs = len(headers)
1503-
ncols = len(rows[0])
1504-
if nhs < ncols:
1505-
headers = [""] * (ncols - nhs) + headers
1504+
headers_pad = max(0, len(rows[0]) - len(headers))
1505+
headers = [""] * headers_pad + headers
15061506

1507-
return rows, headers
1507+
return rows, headers, headers_pad
15081508

15091509

15101510
def _wrap_text_to_colwidths(list_of_lists, colwidths, numparses=True):
@@ -1580,8 +1580,11 @@ def tabulate(
15801580
missingval=_DEFAULT_MISSINGVAL,
15811581
showindex="default",
15821582
disable_numparse=False,
1583+
colglobalalign=None,
15831584
colalign=None,
15841585
maxcolwidths=None,
1586+
headersglobalalign=None,
1587+
headersalign=None,
15851588
rowalign=None,
15861589
maxheadercolwidths=None,
15871590
):
@@ -1636,8 +1639,8 @@ def tabulate(
16361639
- - --
16371640
16381641
1639-
Column alignment
1640-
----------------
1642+
Column and Headers alignment
1643+
----------------------------
16411644
16421645
`tabulate` tries to detect column types automatically, and aligns
16431646
the values properly. By default it aligns decimal points of the
@@ -1646,6 +1649,23 @@ def tabulate(
16461649
(`numalign`, `stralign`) are: "right", "center", "left", "decimal"
16471650
(only for `numalign`), and None (to disable alignment).
16481651
1652+
`colglobalalign` allows for global alignment of columns, before any
1653+
specific override from `colalign`. Possible values are: None
1654+
(defaults according to coltype), "right", "center", "decimal",
1655+
"left".
1656+
`colalign` allows for column-wise override starting from left-most
1657+
column. Possible values are: "global" (no override), "right",
1658+
"center", "decimal", "left".
1659+
`headersglobalalign` allows for global headers alignment, before any
1660+
specific override from `headersalign`. Possible values are: None
1661+
(follow columns alignment), "right", "center", "left".
1662+
`headersalign` allows for header-wise override starting from left-most
1663+
given header. Possible values are: "global" (no override), "same"
1664+
(follow column alignment), "right", "center", "left".
1665+
1666+
Note on intended behaviour: If there is no `tabular_data`, any column
1667+
alignment argument is ignored. Hence, in this case, header
1668+
alignment cannot be inferred from column alignment.
16491669
16501670
Table formats
16511671
-------------
@@ -2065,7 +2085,7 @@ def tabulate(
20652085
if tabular_data is None:
20662086
tabular_data = []
20672087

2068-
list_of_lists, headers = _normalize_tabular_data(
2088+
list_of_lists, headers, headers_pad = _normalize_tabular_data(
20692089
tabular_data, headers, showindex=showindex
20702090
)
20712091
list_of_lists, separating_lines = _remove_separating_lines(list_of_lists)
@@ -2183,11 +2203,21 @@ def tabulate(
21832203
]
21842204

21852205
# align columns
2186-
aligns = [numalign if ct in [int, float] else stralign for ct in coltypes]
2206+
# first set global alignment
2207+
if colglobalalign is not None: # if global alignment provided
2208+
aligns = [colglobalalign] * len(cols)
2209+
else: # default
2210+
aligns = [numalign if ct in [int, float] else stralign for ct in coltypes]
2211+
# then specific alignements
21872212
if colalign is not None:
21882213
assert isinstance(colalign, Iterable)
2214+
if isinstance(colalign, str):
2215+
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)
21892216
for idx, align in enumerate(colalign):
2190-
aligns[idx] = align
2217+
if not idx < len(aligns):
2218+
break
2219+
elif align != "global":
2220+
aligns[idx] = align
21912221
minwidths = (
21922222
[width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols)
21932223
)
@@ -2196,17 +2226,35 @@ def tabulate(
21962226
for c, a, minw in zip(cols, aligns, minwidths)
21972227
]
21982228

2229+
aligns_headers = None
21992230
if headers:
22002231
# align headers and add headers
22012232
t_cols = cols or [[""]] * len(headers)
2202-
t_aligns = aligns or [stralign] * len(headers)
2233+
# first set global alignment
2234+
if headersglobalalign is not None: # if global alignment provided
2235+
aligns_headers = [headersglobalalign] * len(t_cols)
2236+
else: # default
2237+
aligns_headers = aligns or [stralign] * len(headers)
2238+
# then specific header alignements
2239+
if headersalign is not None:
2240+
assert isinstance(headersalign, Iterable)
2241+
if isinstance(headersalign, str):
2242+
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)
2243+
for idx, align in enumerate(headersalign):
2244+
hidx = headers_pad + idx
2245+
if not hidx < len(aligns_headers):
2246+
break
2247+
elif align == "same" and hidx < len(aligns): # same as column align
2248+
aligns_headers[hidx] = aligns[hidx]
2249+
elif align != "global":
2250+
aligns_headers[hidx] = align
22032251
minwidths = [
22042252
max(minw, max(width_fn(cl) for cl in c))
22052253
for minw, c in zip(minwidths, t_cols)
22062254
]
22072255
headers = [
22082256
_align_header(h, a, minw, width_fn(h), is_multiline, width_fn)
2209-
for h, a, minw in zip(headers, t_aligns, minwidths)
2257+
for h, a, minw in zip(headers, aligns_headers, minwidths)
22102258
]
22112259
rows = list(zip(*cols))
22122260
else:
@@ -2221,7 +2269,7 @@ def tabulate(
22212269
_reinsert_separating_lines(rows, separating_lines)
22222270

22232271
return _format_table(
2224-
tablefmt, headers, rows, minwidths, aligns, is_multiline, rowaligns=rowaligns
2272+
tablefmt, headers, aligns_headers, rows, minwidths, aligns, is_multiline, rowaligns=rowaligns
22252273
)
22262274

22272275

@@ -2352,7 +2400,7 @@ def str(self):
23522400
return self
23532401

23542402

2355-
def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline, rowaligns):
2403+
def _format_table(fmt, headers, headersaligns, rows, colwidths, colaligns, is_multiline, rowaligns):
23562404
"""Produce a plain-text representation of the table."""
23572405
lines = []
23582406
hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
@@ -2374,7 +2422,7 @@ def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline, rowali
23742422
_append_line(lines, padded_widths, colaligns, fmt.lineabove)
23752423

23762424
if padded_headers:
2377-
append_row(lines, padded_headers, padded_widths, colaligns, headerrow)
2425+
append_row(lines, padded_headers, padded_widths, headersaligns, headerrow)
23782426
if fmt.linebelowheader and "linebelowheader" not in hidden:
23792427
_append_line(lines, padded_widths, colaligns, fmt.linebelowheader)
23802428

test/common.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest # noqa
22
from pytest import skip, raises # noqa
3-
3+
import warnings
44

55
def assert_equal(expected, result):
66
print("Expected:\n%s\n" % expected)
@@ -27,3 +27,18 @@ def rows_to_pipe_table_str(rows):
2727
lines.append(line)
2828

2929
return "\n".join(lines)
30+
31+
def check_warnings(func_args_kwargs, *, num=None, category=None, contain=None):
32+
func, args, kwargs = func_args_kwargs
33+
with warnings.catch_warnings(record=True) as W:
34+
# Causes all warnings to always be triggered inside here.
35+
warnings.simplefilter("always")
36+
func(*args, **kwargs)
37+
# Checks
38+
if num is not None:
39+
assert len(W) == num
40+
if category is not None:
41+
assert all([issubclass(w.category, category) for w in W])
42+
if contain is not None:
43+
assert all([contain in str(w.message) for w in W])
44+

test/test_api.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,11 @@ def test_tabulate_signature():
4848
("missingval", ""),
4949
("showindex", "default"),
5050
("disable_numparse", False),
51+
("colglobalalign", None),
5152
("colalign", None),
5253
("maxcolwidths", None),
54+
("headersglobalalign", None),
55+
("headersalign", None),
5356
("rowalign", None),
5457
("maxheadercolwidths", None),
5558
]

test/test_output.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Test output of the various forms of tabular data."""
22

33
import tabulate as tabulate_module
4-
from common import assert_equal, raises, skip
4+
from common import assert_equal, raises, skip, check_warnings
55
from tabulate import tabulate, simple_separated_format, SEPARATING_LINE
66

77
# _test_table shows
@@ -2680,6 +2680,60 @@ def test_colalign_multi_with_sep_line():
26802680
expected = " one two\n\nthree four"
26812681
assert_equal(expected, result)
26822682

2683+
def test_column_global_and_specific_alignment():
2684+
""" Test `colglobalalign` and `"global"` parameter for `colalign`. """
2685+
table = [[1,2,3,4],[111,222,333,444]]
2686+
colglobalalign = 'center'
2687+
colalign = ('global','left', 'right')
2688+
result = tabulate(table, colglobalalign=colglobalalign, colalign=colalign)
2689+
expected = '\n'.join([
2690+
"--- --- --- ---",
2691+
" 1 2 3 4",
2692+
"111 222 333 444",
2693+
"--- --- --- ---"])
2694+
assert_equal(expected, result)
2695+
2696+
def test_headers_global_and_specific_alignment():
2697+
""" Test `headersglobalalign` and `headersalign`. """
2698+
table = [[1,2,3,4,5,6],[111,222,333,444,555,666]]
2699+
colglobalalign = 'center'
2700+
colalign = ('left',)
2701+
headers = ['h', 'e', 'a', 'd', 'e', 'r']
2702+
headersglobalalign = 'right'
2703+
headersalign = ('same', 'same', 'left', 'global', 'center')
2704+
result = tabulate(table, headers=headers, colglobalalign=colglobalalign, colalign=colalign, headersglobalalign=headersglobalalign, headersalign=headersalign)
2705+
expected = '\n'.join([
2706+
"h e a d e r",
2707+
"--- --- --- --- --- ---",
2708+
"1 2 3 4 5 6",
2709+
"111 222 333 444 555 666"])
2710+
assert_equal(expected, result)
2711+
2712+
def test_colalign_or_headersalign_too_long():
2713+
""" Test `colalign` and `headersalign` too long. """
2714+
table = [[1,2],[111,222]]
2715+
colalign = ('global', 'left', 'center')
2716+
headers = ['h']
2717+
headersalign = ('center', 'right', 'same')
2718+
result = tabulate(table, headers=headers, colalign=colalign, headersalign=headersalign)
2719+
expected = '\n'.join([
2720+
" h",
2721+
"--- ---",
2722+
" 1 2",
2723+
"111 222"])
2724+
assert_equal(expected, result)
2725+
2726+
def test_warning_when_colalign_or_headersalign_is_string():
2727+
""" Test user warnings when `colalign` or `headersalign` is a string. """
2728+
table = [[1,"bar"]]
2729+
opt = {
2730+
'colalign': "center",
2731+
'headers': ['foo', '2'],
2732+
'headersalign': "center"}
2733+
check_warnings((tabulate, [table], opt),
2734+
num = 2,
2735+
category = UserWarning,
2736+
contain = "As a string")
26832737

26842738
def test_float_conversions():
26852739
"Output: float format parsed"

tox.ini

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# for testing and it is disabled by default.
99

1010
[tox]
11-
envlist = lint, py{37, 38, 39, 310}
11+
envlist = lint, py{37, 38, 39, 310, 311}
1212
isolated_build = True
1313

1414
[testenv]
@@ -89,6 +89,23 @@ deps =
8989
wcwidth
9090

9191

92+
[testenv:py311]
93+
basepython = python3.11
94+
commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
95+
deps =
96+
pytest
97+
98+
[testenv:py311-extra]
99+
basepython = python3.11
100+
setenv = PYTHONDEVMODE = 1
101+
commands = pytest -v --doctest-modules --ignore benchmark.py {posargs}
102+
deps =
103+
pytest
104+
numpy
105+
pandas
106+
wcwidth
107+
108+
92109
[flake8]
93110
max-complexity = 22
94111
max-line-length = 99

0 commit comments

Comments
 (0)