diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..e1abad2 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -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( diff --git a/test/test_output.py b/test/test_output.py index ea3da87..1277041 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -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}", ] diff --git a/test/test_regression.py b/test/test_regression.py index 9555676..ca5e64f 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -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", @@ -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)