Skip to content

Commit d8f36b7

Browse files
authored
Merge branch 'master' into pre-commit
2 parents 4cd6475 + a384471 commit d8f36b7

11 files changed

Lines changed: 174 additions & 51 deletions

File tree

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
python-version: ['3.12']
12+
python-version: ['3.13']
1313

1414
steps:
1515
- uses: actions/checkout@v4

.github/workflows/tabulate.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
build:
99
strategy:
1010
matrix:
11-
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
11+
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14']
1212
os: ["ubuntu-latest", "windows-latest", "macos-latest"]
1313
runs-on: ${{ matrix.os }}
1414

HOWTOPUBLISH

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# update contributors and CHANGELOG in README
22
python -m pre_commit run -a # and then commit changes
3-
tox -e py39-extra,py310-extra,py311-extra,py312-extra,py313-extra
3+
tox -e py310-extra,py311-extra,py312-extra,py313-extra,py314-extra
44
# tag version release
55
python -m build -s # this will update tabulate/version.py
66
python -m pip install . # install tabulate in the current venv

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,7 @@ format:
501501

502502
```pycon
503503
>>> print(tabulate(table, headers, tablefmt="asciidoc"))
504-
[cols="8<,7>",options="header"]
504+
[cols="<8,>7",options="header"]
505505
|====
506506
| item | qty
507507
| spam | 42

pyproject.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ classifiers = [
1313
"Development Status :: 4 - Beta",
1414
"Operating System :: OS Independent",
1515
"Programming Language :: Python :: 3",
16-
"Programming Language :: Python :: 3.9",
1716
"Programming Language :: Python :: 3.10",
1817
"Programming Language :: Python :: 3.11",
1918
"Programming Language :: Python :: 3.12",
2019
"Programming Language :: Python :: 3.13",
20+
"Programming Language :: Python :: 3.14",
2121
"Topic :: Software Development :: Libraries",
2222
]
23-
requires-python = ">=3.9"
23+
requires-python = ">=3.10"
2424
dynamic = ["version"]
2525

2626
[project.urls]
@@ -32,8 +32,7 @@ widechars = ["wcwidth"]
3232
[project.scripts]
3333
tabulate = "tabulate:_main"
3434

35-
[tool.setuptools]
36-
packages = ["tabulate"]
35+
# [tool.setuptools]
36+
# packages = ["tabulate"]
3737

3838
[tool.setuptools_scm]
39-
write_to = "tabulate/version.py"

tabulate/__init__.py

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,10 @@ def _pipe_line_with_colons(colwidths, colaligns):
142142
alignment (as in `pipe` output format)."""
143143
if not colaligns: # e.g. printing an empty data frame (github issue #15)
144144
colaligns = [""] * len(colwidths)
145-
segments = [_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)]
146-
return "|" + "|".join(segments) + "|"
145+
segments = "|".join(
146+
_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)
147+
)
148+
return f"|{segments}|"
147149

148150

149151
def _grid_segment_with_colons(colwidth, align):
@@ -165,8 +167,10 @@ def _grid_line_with_colons(colwidths, colaligns):
165167
in a grid table."""
166168
if not colaligns:
167169
colaligns = [""] * len(colwidths)
168-
segments = [_grid_segment_with_colons(w, a) for a, w in zip(colaligns, colwidths)]
169-
return "+" + "+".join(segments) + "+"
170+
segments = "+".join(
171+
_grid_segment_with_colons(w, a) for a, w in zip(colaligns, colwidths)
172+
)
173+
return f"+{segments}+"
170174

171175

172176
def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
@@ -188,8 +192,8 @@ def _mediawiki_row_with_attrs(separator, cell_values, colwidths, colaligns):
188192
def _textile_row_with_attrs(cell_values, colwidths, colaligns):
189193
cell_values[0] += " "
190194
alignment = {"left": "<.", "right": ">.", "center": "=.", "decimal": ">."}
191-
values = (alignment.get(a, "") + v for a, v in zip(colaligns, cell_values))
192-
return "|" + "|".join(values) + "|"
195+
values = "|".join(alignment.get(a, "") + v for a, v in zip(colaligns, cell_values))
196+
return f"|{values}|"
193197

194198

195199
def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore):
@@ -259,7 +263,7 @@ def make_header_line(is_header, colwidths, colaligns):
259263
colwidths, [alignment[colalign] for colalign in colaligns]
260264
)
261265
asciidoc_column_specifiers = [
262-
f"{width:d}{align}" for width, align in asciidoc_alignments
266+
f"{align}{width:d}" for width, align in asciidoc_alignments
263267
]
264268
header_list = ['cols="' + (",".join(asciidoc_column_specifiers)) + '"']
265269

@@ -270,7 +274,8 @@ def make_header_line(is_header, colwidths, colaligns):
270274
options_list.append("header")
271275

272276
if options_list:
273-
header_list += ['options="' + ",".join(options_list) + '"']
277+
options_list = ",".join(options_list)
278+
header_list.append(f'options="{options_list}"')
274279

275280
# generate the list of entries in the table header field
276281

@@ -1352,12 +1357,18 @@ def _format(val, valtype, floatfmt, intfmt, missingval="", has_invisible=True):
13521357
is_a_colored_number = has_invisible and isinstance(val, (str, bytes))
13531358
if is_a_colored_number:
13541359
raw_val = _strip_ansi(val)
1355-
formatted_val = format(float(raw_val), floatfmt)
1360+
try:
1361+
formatted_val = format(float(raw_val), floatfmt)
1362+
except (ValueError, TypeError):
1363+
return f"{val}"
13561364
return val.replace(raw_val, formatted_val)
13571365
else:
13581366
if isinstance(val, str) and "," in val:
13591367
val = val.replace(",", "") # handle thousands-separators
1360-
return format(float(val), floatfmt)
1368+
try:
1369+
return format(float(val), floatfmt)
1370+
except (ValueError, TypeError):
1371+
return f"{val}"
13611372
else:
13621373
return f"{val}"
13631374

@@ -1480,7 +1491,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"):
14801491
index = None
14811492
if hasattr(tabular_data, "keys") and hasattr(tabular_data, "values"):
14821493
# dict-like and pandas.DataFrame?
1483-
if hasattr(tabular_data.values, "__call__"):
1494+
if callable(tabular_data.values):
14841495
# likely a conventional dict
14851496
keys = tabular_data.keys()
14861497
try:
@@ -1644,6 +1655,10 @@ def _wrap_text_to_colwidths(
16441655
list_of_lists,
16451656
colwidths,
16461657
numparses=True,
1658+
<<<<<<< pre-commit
1659+
=======
1660+
missingval=_DEFAULT_MISSINGVAL,
1661+
>>>>>>> master
16471662
break_long_words=_BREAK_LONG_WORDS,
16481663
break_on_hyphens=_BREAK_ON_HYPHENS,
16491664
):
@@ -1668,7 +1683,21 @@ def _wrap_text_to_colwidths(
16681683
break_long_words=break_long_words,
16691684
break_on_hyphens=break_on_hyphens,
16701685
)
1686+
<<<<<<< pre-commit
16711687
casted_cell = str(cell)
1688+
=======
1689+
# Cast based on our internal type handling. Any future custom
1690+
# formatting of types (such as datetimes) may need to be more
1691+
# explicit than just `str` of the object. Also doesn't work for
1692+
# custom floatfmt/intfmt, nor with any missing/blank cells.
1693+
casted_cell = (
1694+
missingval
1695+
if cell is None
1696+
else str(cell)
1697+
if cell == "" or _isnumber(cell)
1698+
else str(_type(cell, numparse)(cell))
1699+
)
1700+
>>>>>>> master
16721701
wrapped = [
16731702
"\n".join(wrapper.wrap(line))
16741703
for line in casted_cell.splitlines()
@@ -2226,8 +2255,8 @@ def tabulate(
22262255
Tabulate will, by default, set the width of each column to the length of the
22272256
longest element in that column. However, in situations where fields are expected
22282257
to reasonably be too long to look good as a single line, tabulate can help automate
2229-
word wrapping long fields for you. Use the parameter `maxcolwidth` to provide a
2230-
list of maximal column widths
2258+
word wrapping long fields for you. Use the parameter `maxcolwidths` to provide a
2259+
list of maximal column widths:
22312260
22322261
>>> print(tabulate( \
22332262
[('1', 'John Smith', \
@@ -2244,7 +2273,7 @@ def tabulate(
22442273
| | | better if it is wrapped a bit |
22452274
+------------+------------+-------------------------------+
22462275
2247-
Header column width can be specified in a similar way using `maxheadercolwidth`
2276+
Header column width can be specified in a similar way using `maxheadercolwidths`.
22482277
22492278
"""
22502279

@@ -2273,12 +2302,16 @@ def tabulate(
22732302
list_of_lists,
22742303
maxcolwidths,
22752304
numparses=numparses,
2305+
<<<<<<< pre-commit
2306+
=======
2307+
missingval=missingval,
2308+
>>>>>>> master
22762309
break_long_words=break_long_words,
22772310
break_on_hyphens=break_on_hyphens,
22782311
)
22792312

22802313
if maxheadercolwidths is not None:
2281-
num_cols = len(list_of_lists[0])
2314+
num_cols = len(list_of_lists[0]) if list_of_lists else len(headers)
22822315
if isinstance(maxheadercolwidths, int): # Expand scalar for all columns
22832316
maxheadercolwidths = _expand_iterable(
22842317
maxheadercolwidths, num_cols, maxheadercolwidths
@@ -2291,6 +2324,10 @@ def tabulate(
22912324
[headers],
22922325
maxheadercolwidths,
22932326
numparses=numparses,
2327+
<<<<<<< pre-commit
2328+
=======
2329+
missingval=missingval,
2330+
>>>>>>> master
22942331
break_long_words=break_long_words,
22952332
break_on_hyphens=break_on_hyphens,
22962333
)[0]
@@ -2535,7 +2572,7 @@ def _build_row(padded_cells, colwidths, colaligns, rowfmt):
25352572
"Return a string which represents a row of data cells."
25362573
if not rowfmt:
25372574
return None
2538-
if hasattr(rowfmt, "__call__"):
2575+
if callable(rowfmt):
25392576
return rowfmt(padded_cells, colwidths, colaligns)
25402577
else:
25412578
return _build_simple_row(padded_cells, rowfmt)
@@ -2586,7 +2623,7 @@ def _build_line(colwidths, colaligns, linefmt):
25862623
"Return a string which represents a horizontal line."
25872624
if not linefmt:
25882625
return None
2589-
if hasattr(linefmt, "__call__"):
2626+
if callable(linefmt):
25902627
return linefmt(colwidths, colaligns)
25912628
else:
25922629
begin, fill, sep, end = linefmt
@@ -2758,13 +2795,16 @@ def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
27582795

27592796
# If we're allowed to break long words, then do so: put as much
27602797
# of the next chunk onto the current line as will fit.
2761-
if self.break_long_words:
2798+
if self.break_long_words and space_left > 0:
27622799
# Tabulate Custom: Build the string up piece-by-piece in order to
27632800
# take each charcter's width into account
27642801
chunk = reversed_chunks[-1]
27652802
i = 1
27662803
# Only count printable characters, so strip_ansi first, index later.
2767-
while len(_strip_ansi(chunk)[:i]) <= space_left:
2804+
stripped_chunk = _strip_ansi(chunk)
2805+
while (
2806+
i <= len(stripped_chunk) and self._len(stripped_chunk[:i]) <= space_left
2807+
):
27682808
i = i + 1
27692809
# Consider escape codes when breaking words up
27702810
total_escape_len = 0

test/test_input.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def test_numpy_record_array():
173173
[("Alice", 23, 169.5), ("Bob", 27, 175.0)],
174174
dtype={
175175
"names": ["name", "age", "height"],
176-
"formats": ["a32", "uint8", "float32"],
176+
"formats": ["S32", "uint8", "float32"],
177177
},
178178
)
179179
expected = "\n".join(
@@ -199,7 +199,7 @@ def test_numpy_record_array_keys():
199199
[("Alice", 23, 169.5), ("Bob", 27, 175.0)],
200200
dtype={
201201
"names": ["name", "age", "height"],
202-
"formats": ["a32", "uint8", "float32"],
202+
"formats": ["S32", "uint8", "float32"],
203203
},
204204
)
205205
expected = "\n".join(
@@ -225,7 +225,7 @@ def test_numpy_record_array_headers():
225225
[("Alice", 23, 169.5), ("Bob", 27, 175.0)],
226226
dtype={
227227
"names": ["name", "age", "height"],
228-
"formats": ["a32", "uint8", "float32"],
228+
"formats": ["S32", "uint8", "float32"],
229229
},
230230
)
231231
expected = "\n".join(
@@ -313,6 +313,7 @@ def test_sqlite3():
313313
cursor.execute("INSERT INTO people VALUES (?, ?, ?)", values)
314314
cursor.execute("SELECT name, age, height FROM people ORDER BY name")
315315
result = tabulate(cursor, headers=["whom", "how old", "how tall"])
316+
conn.close()
316317
expected = """\
317318
whom how old how tall
318319
------ --------- ----------
@@ -337,6 +338,7 @@ def test_sqlite3_keys():
337338
'SELECT name "whom", age "how old", height "how tall" FROM people ORDER BY name'
338339
)
339340
result = tabulate(cursor, headers="keys")
341+
conn.close()
340342
expected = """\
341343
whom how old how tall
342344
------ --------- ----------

test/test_output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2101,7 +2101,7 @@ def test_asciidoc():
21012101
"Output: asciidoc with headers"
21022102
expected = "\n".join(
21032103
[
2104-
'[cols="11<,11>",options="header"]',
2104+
'[cols="<11,>11",options="header"]',
21052105
"|====",
21062106
"| strings | numbers ",
21072107
"| spam | 41.9999 ",
@@ -2117,7 +2117,7 @@ def test_asciidoc_headerless():
21172117
"Output: asciidoc without headers"
21182118
expected = "\n".join(
21192119
[
2120-
'[cols="6<,10>"]',
2120+
'[cols="<6,>10"]',
21212121
"|====",
21222122
"| spam | 41.9999 ",
21232123
"| eggs | 451 ",

test/test_regression.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,15 +258,18 @@ def test_alignment_of_decimal_numbers_with_ansi_color():
258258

259259
def test_alignment_of_decimal_numbers_with_commas():
260260
"Regression: alignment for decimal numbers with comma separators"
261-
skip("test is temporarily disable until the feature is reimplemented")
262-
# table = [["c1r1", "14502.05"], ["c1r2", 105]]
263-
# result = tabulate(table, tablefmt="grid", floatfmt=',.2f')
264-
# expected = "\n".join(
265-
# ['+------+-----------+', '| c1r1 | 14,502.05 |',
266-
# '+------+-----------+', '| c1r2 | 105.00 |',
267-
# '+------+-----------+']
268-
# )
269-
# assert_equal(expected, result)
261+
table = [["c1r1", "14502.05"], ["c1r2", 105]]
262+
result = tabulate(table, tablefmt="grid", floatfmt=",.2f")
263+
expected = "\n".join(
264+
[
265+
"+------+-----------+",
266+
"| c1r1 | 14,502.05 |",
267+
"+------+-----------+",
268+
"| c1r2 | 105.00 |",
269+
"+------+-----------+",
270+
]
271+
)
272+
assert_equal(expected, result)
270273

271274

272275
def test_long_integers():
@@ -545,3 +548,22 @@ def test_empty_table_with_colalign():
545548
]
546549
)
547550
assert_equal(expected, table)
551+
552+
553+
def test_empty_table_with_maxheadercolwidths():
554+
"Regression: empty table with maxheadercolwidths kwarg (issue #365)"
555+
result = tabulate([], headers=["one", "two", "three"], maxheadercolwidths=5)
556+
expected = "\n".join(
557+
[
558+
"one two three",
559+
"----- ----- -------",
560+
]
561+
)
562+
assert_equal(expected, result)
563+
564+
565+
def test_mixed_bool_strings_and_numeric_strings():
566+
"Regression: column with bool-like strings and numeric strings should not crash (issue #209)"
567+
result = tabulate([["False"], ["1."]])
568+
expected = "\n".join(["-----", "False", " 1", "-----"])
569+
assert_equal(expected, result)

0 commit comments

Comments
 (0)