diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..a743000 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -2504,7 +2504,7 @@ def _pad_row(cells, padding): return cells -def _build_simple_row(padded_cells: list[list], rowfmt: DataRow) -> str: +def _build_simple_row(padded_cells: list[list], rowfmt: DataRow, preserve_trailing_empty: bool = False) -> str: "Format row according to DataRow format without padding." begin = rowfmt.begin sep = rowfmt.sep @@ -2520,7 +2520,15 @@ def escape_char(c): else: escaped_cells = padded_cells - return (begin + sep.join(escaped_cells) + end).rstrip() + row = begin + sep.join(escaped_cells) + end + # When there is no closing delimiter and the last cell is purely whitespace + # (i.e. a missing/None value formatted as ""), rstrip() silently drops the + # trailing column separator and makes that column invisible. Skip rstrip() + # only for primary data rows (preserve_trailing_empty=True) so that + # multiline continuation lines are still stripped as before. + if preserve_trailing_empty and not end and escaped_cells and not escaped_cells[-1].strip(): + return row + return row.rstrip() def _build_row( @@ -2528,6 +2536,7 @@ def _build_row( colwidths: list[int], colaligns: list[str], rowfmt: DataRow | Callable, + preserve_trailing_empty: bool = False, ) -> str: "Return a string which represents a row of data cells." if not rowfmt: @@ -2535,12 +2544,12 @@ def _build_row( if callable(rowfmt): return rowfmt(padded_cells, colwidths, colaligns) else: - return _build_simple_row(padded_cells, rowfmt) + return _build_simple_row(padded_cells, rowfmt, preserve_trailing_empty) -def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt, rowalign=None): +def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt, rowalign=None, preserve_trailing_empty=False): # NOTE: rowalign is ignored and exists for api compatibility with _append_multiline_row - lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt)) + lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt, preserve_trailing_empty)) return lines @@ -2624,7 +2633,9 @@ def _format_table( append_row = partial(_append_multiline_row, pad=pad) else: pad_row = _pad_row - append_row = _append_basic_row + # preserve_trailing_empty=True so that a trailing None/empty cell (which + # formats to "") is not silently rstripped away and the column disappears. + append_row = partial(_append_basic_row, preserve_trailing_empty=True) padded_headers = pad_row(headers, pad) diff --git a/test/test_input.py b/test/test_input.py index 3cc3237..e55e8fe 100644 --- a/test/test_input.py +++ b/test/test_input.py @@ -57,7 +57,7 @@ def test_list_of_lists(): " string number", "-- -------- --------", "a one 1", - "b two", + "b two ", ] ) result = tabulate(ll, headers=["string", "number"]) @@ -72,7 +72,7 @@ def test_list_of_lists_firstrow(): " string number", "-- -------- --------", "a one 1", - "b two", + "b two ", ] ) result = tabulate(ll, headers="firstrow") @@ -82,7 +82,7 @@ def test_list_of_lists_firstrow(): def test_list_of_lists_keys(): "Input: a list of lists with column indices as headers." ll = [["a", "one", 1], ["b", "two", None]] - expected = "\n".join(["0 1 2", "--- --- ---", "a one 1", "b two"]) + expected = "\n".join(["0 1 2", "--- --- ---", "a one 1", "b two "]) result = tabulate(ll, headers="keys") assert_equal(expected, result) @@ -405,8 +405,8 @@ def test_list_of_dicts_with_missing_keys(): [ " foo bar baz", "----- ----- -----", - " 1", - " 2", + " 1 ", + " 2 ", " 4 3", ] ) diff --git a/test/test_output.py b/test/test_output.py index ea3da87..8f6f333 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -3028,7 +3028,7 @@ def test_missingval_multi(): missingval=("n/a", "?"), tablefmt="plain", ) - expected = "Alice Bob Charlie\nn/a ?" + expected = "Alice Bob Charlie\nn/a ? " assert_equal(expected, result) @@ -3053,7 +3053,7 @@ def test_column_emptymissing_deduction(): ? 1.2342 1/3 0.056789 12,345 abc ? -3,333.3 ? +3,333.3 ? ------------ ----------- ---""" assert_equal(expected, result) diff --git a/test/test_regression.py b/test/test_regression.py index 9555676..1ed2ef6 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -205,7 +205,7 @@ def test_88_256_ANSI_color_codes(): def test_column_with_mixed_value_types(): "Regression: mixed value types in the same column (issue #31)" - expected = "\n".join(["-----", "", "a", "я", "0", "False", "-----"]) + expected = "\n".join(["-----", " ", "a", "я", "0", "False", "-----"]) data = [[None], ["a"], ["\u044f"], [0], [False]] table = tabulate(data) assert_equal(table, expected) @@ -415,7 +415,7 @@ def test_escape_empty_cell_in_first_column_in_rst(): def test_ragged_rows(): "Regression: allow rows with different number of columns (issue #85)" table = [[1, 2, 3], [1, 2], [1, 2, 3, 4]] - expected = "\n".join(["- - - -", "1 2 3", "1 2", "1 2 3 4", "- - - -"]) + expected = "\n".join(["- - - -", "1 2 3 ", "1 2 ", "1 2 3 4", "- - - -"]) result = tabulate(table) assert_equal(expected, result)