Skip to content
Closed
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
15 changes: 15 additions & 0 deletions tabulate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,21 @@ def tabulate(
elif align != "global":
aligns[idx] = align
minwidths = [width_fn(h) + min_padding for h in headers] if headers else [0] * len(cols)
# When maxcolwidths is active and the header text is *at least as wide* as
# the requested column limit, min_padding would push the minimum width
# beyond the user's requested maximum, adding spurious trailing spaces.
# In that case the header width alone is sufficient — strip the extra
# padding so the column stays at the requested width.
# We only apply this correction when header_width >= maxcolwidth, because
# when the header is narrower than the limit the data cells (wrapped to
# maxcolwidth chars) determine the column width and min_padding is still
# needed for borderless table formats.
if maxcolwidths is not None and headers:
for i, (h, mw) in enumerate(zip(headers, maxcolwidths)):
if mw is not None and i < len(minwidths):
hw = width_fn(h)
if hw >= mw:
minwidths[i] = hw # drop the +min_padding
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.
Expand Down
46 changes: 46 additions & 0 deletions test/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -3349,3 +3349,49 @@ def test_break_on_hyphens():
expected = "h1 h2 h3\n---- ---- ----\nfoo- bar- foo-\nbar bar foo"
result = tabulate(test_table, table_headers, maxcolwidths=5, break_on_hyphens=True)
assert_equal(expected, result)


def test_maxcolwidth_no_extra_padding_when_header_equals_limit():
"""Regression test for issue #354.

When the header text width exactly equals the maxcolwidths limit,
the column must not be widened by min_padding beyond the limit.

Previously, min_padding (2 spaces) was unconditionally added to the
minimum column width derived from the header, causing the column to be
2 chars wider than requested — enough to cause terminal overflow when
the user had calculated maxcolwidths from the terminal width.
"""
headers = ["Header#1", "Header#2", "Header#3"]
data = [
[
"Alpha beta gama zeta omega",
"The weather was exceptionally good that day again",
"The files were concatenated and archived for posterity.",
],
]

result = tabulate(
data,
headers=headers,
tablefmt="fancy_grid",
maxcolwidths=[None, None, 8],
)

# Each cell in Header#3's column must be at most 10 chars wide
# (1 border-space + 8 content + 1 border-space) when using fancy_grid.
# Before the fix they were 12 chars (+ 2 extra spurious spaces).
lines = result.split("\n")
for line in lines:
if "│" not in line:
continue
col3_end = line.rfind("│")
col3_start = line.rfind("│", 0, col3_end)
if col3_start < 0:
continue
cell = line[col3_start + 1 : col3_end]
assert len(cell) <= 10, (
f"Column 'Header#3' cell is {len(cell)} chars wide (expected ≤10); "
f"got {cell!r}. maxcolwidths=8 should not add 2 extra spaces when "
f"header width equals the limit."
)
51 changes: 31 additions & 20 deletions test/test_textwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,44 +259,55 @@ def test_wrap_datetime():


def test_wrap_none_value():
"""TextWrapper: Show that None can be wrapped without crashing"""
"""TextWrapper: Show that None can be wrapped without crashing.

When header width equals maxcolwidths, the column must not be inflated
by min_padding beyond the user-requested maximum (issue #354).
"""
data = [["First Entry", None], ["Second Entry", None]]
headers = ["Title", "Value"]
result = tabulate(data, headers=headers, tablefmt="grid", maxcolwidths=[7, 5])

# "Value" (5 chars) == maxcolwidth (5): column stays at 5, not 5+min_padding.
expected = [
"+---------+---------+",
"| Title | Value |",
"+=========+=========+",
"| First | |",
"| Entry | |",
"+---------+---------+",
"| Second | |",
"| Entry | |",
"+---------+---------+",
"+---------+-------+",
"| Title | Value |",
"+=========+=======+",
"| First | |",
"| Entry | |",
"+---------+-------+",
"| Second | |",
"| Entry | |",
"+---------+-------+",
]
expected = "\n".join(expected)
assert_equal(expected, result)


def test_wrap_none_value_with_missingval():
"""TextWrapper: Show that None can be wrapped without crashing and with a missing value"""
"""TextWrapper: Show that None can be wrapped without crashing and with a missing value.

When header width equals maxcolwidths, the column must not be inflated
by min_padding beyond the user-requested maximum (issue #354).
"""
data = [["First Entry", None], ["Second Entry", None]]
headers = ["Title", "Value"]
result = tabulate(
data, headers=headers, tablefmt="grid", maxcolwidths=[7, 5], missingval="???"
)

# "Value" (5 chars) == maxcolwidth (5): column stays at 5.
# "???" (3 chars) < maxcolwidth (5): fits without wrapping.
expected = [
"+---------+---------+",
"| Title | Value |",
"+=========+=========+",
"| First | ??? |",
"| Entry | |",
"+---------+---------+",
"| Second | ??? |",
"| Entry | |",
"+---------+---------+",
"+---------+-------+",
"| Title | Value |",
"+=========+=======+",
"| First | ??? |",
"| Entry | |",
"+---------+-------+",
"| Second | ??? |",
"| Entry | |",
"+---------+-------+",
]
expected = "\n".join(expected)
assert_equal(expected, result)
Expand Down