Skip to content
Open
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
29 changes: 28 additions & 1 deletion tabulate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2385,11 +2385,38 @@ def tabulate(
break
elif align != "global":
aligns[idx] = align
# Formats that escape special characters (latex, github) used to escape at
# render time, i.e. after padding, so escaped cells were wider than the
# measured column widths and rows misaligned (issue #244). Escape before
# widths are measured, then drop the escape rules from the row formatters
# to avoid escaping twice.
_is_colon_grid = tablefmt == "colon_grid"
if not isinstance(tablefmt, TableFormat):
tablefmt = _table_formats.get(tablefmt, _table_formats["simple"])
escape_map = None
for _rowfmt in (tablefmt.headerrow, tablefmt.datarow):
if isinstance(_rowfmt, DataRow) and _rowfmt.escape_map:
escape_map = _rowfmt.escape_map
break
if escape_map:

def _escape_text(s):
return "".join(escape_map.get(c, c) for c in s) if isinstance(s, str) else s

cols = [[_escape_text(v) for v in c] for c in cols]
headers = [_escape_text(h) for h in headers]
_repl = {}
for _name in ("headerrow", "datarow"):
_rowfmt = getattr(tablefmt, _name)
if isinstance(_rowfmt, DataRow) and _rowfmt.escape_map:
_repl[_name] = DataRow(_rowfmt.begin, _rowfmt.sep, _rowfmt.end)
if _repl:
tablefmt = tablefmt._replace(**_repl)
minwidths = [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":
if _is_colon_grid:
aligns_copy = ["left"] * len(cols)
cols = [
_align_column(
Expand Down
4 changes: 2 additions & 2 deletions test/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -2684,8 +2684,8 @@ def test_latex():
r"\hline",
r" strings & numbers (\$N\_0\$) \\",
r"\hline",
r" spam & 41.9999 \\",
r" eggs & 451 \\",
r" spam & 41.9999 \\",
r" eggs & 451 \\",
r"\hline",
r"\end{tabular}",
]
Expand Down
27 changes: 25 additions & 2 deletions test/test_regression.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,36 @@ def test_column_with_mixed_value_types():
assert_equal(table, expected)


def test_latex_escaped_cell_width():
"Regression: escaped chars widen cells; column widths must account for it (issue #244)"
data = {"col1": ["first", "a_long_second_row", "third_row"], "col2": [1, 2, 3]}
result = tabulate(data, tablefmt="latex", headers="keys")
expected = "\n".join(
[
r"\begin{tabular}{lr}",
r"\hline",
r" col1 & col2 \\",
r"\hline",
r" first & 1 \\",
r" a\_long\_second\_row & 2 \\",
r" third\_row & 3 \\",
r"\hline",
r"\end{tabular}",
]
)
assert_equal(expected, result)
# the & separator must sit at the same column on every row
amp_cols = {line.index("&") for line in result.splitlines() if "&" in line}
assert len(amp_cols) == 1


def test_latex_escape_special_chars():
"Regression: escape special characters in LaTeX output (issue #32)"
expected = "\n".join(
[
r"\begin{tabular}{l}",
r"\hline",
r" foo\^{}bar \\",
r" foo\^{}bar \\",
r"\hline",
r" \&\%\^{}\_\$\#\{\}\ensuremath{<}\ensuremath{>}\textasciitilde{} \\",
r"\hline",
Expand Down Expand Up @@ -596,5 +619,5 @@ def test_asciidoc_without_trailing_whitespace():
def test_github_escape_pipe_character():
"Regression: github format must escape pipe character with a backslash (issue #241)"
result = tabulate([["foo|bar"]], headers=("spam|eggs",), tablefmt="github")
expected = "| spam\\|eggs |\n|:------------|\n| foo\\|bar |"
expected = "| spam\\|eggs |\n|:-------------|\n| foo\\|bar |"
assert_equal(expected, result)