diff --git a/README.md b/README.md index 0283a0c..e8f2102 100644 --- a/README.md +++ b/README.md @@ -1102,6 +1102,8 @@ linebetweenrows, linebelowheader, linebelow, lineabove or just a simple empty li Moon 1737 ----- ---- +When using a custom `showindex`, supply one index per data row. Separating lines do not consume index values. + ### ANSI support ANSI escape codes are non-printable byte sequences usually used for terminal operations like setting color output or modifying cursor positions. Because multi-byte ANSI sequences are inherently non-printable, diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..6d4cba9 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -1401,12 +1401,12 @@ def _prepend_row_index(rows, index): """Add a left-most index column.""" if index is None or index is False: return rows - if isinstance(index, Sized) and len(index) != len(rows): + sans_rows, separating_lines = _remove_separating_lines(rows) + if isinstance(index, Sized) and len(index) != len(sans_rows): raise ValueError( "index must be as long as the number of data rows: " - f"len(index)={len(index)} len(rows)={len(rows)}" + f"len(index)={len(index)} len(rows)={len(sans_rows)}" ) - sans_rows, separating_lines = _remove_separating_lines(rows) new_rows = [] index_iter = iter(index) for row in sans_rows: @@ -1589,6 +1589,10 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): # rows = list(map(list, rows)) rows = [r if _is_separating_line(r) else list(r) for r in rows] + # Keep DataFrame index labels aligned with data rows after removing separators. + if index is not None: + index = [value for row, value in zip(rows, index) if not _is_separating_line(row)] + # add or remove an index column showindex_is_a_str = type(showindex) in [str, bytes] if showindex_is_a_str and showindex == "default" and index is not None: @@ -1599,7 +1603,7 @@ def _normalize_tabular_data(tabular_data, headers, showindex="default"): rows = _prepend_row_index(rows, showindex) elif showindex == "always" or (_bool(showindex) and not showindex_is_a_str): if index is None: - index = list(range(len(rows))) + index = list(range(len(_remove_separating_lines(rows)[0]))) rows = _prepend_row_index(rows, index) elif showindex == "never" or (not _bool(showindex) and not showindex_is_a_str): pass diff --git a/test/test_output.py b/test/test_output.py index ea3da87..3dea31e 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -2,7 +2,7 @@ from decimal import Decimal -from pytest import mark +from pytest import importorskip, mark from tabulate import SEPARATING_LINE, simple_separated_format, tabulate @@ -3248,6 +3248,62 @@ def test_list_of_lists_with_supplied_index(): tabulate(dd, headers=["a", "b"], showindex=[1, 2]) +@mark.parametrize("index_factory", [list, tuple, iter]) +@mark.parametrize( + ("rows", "expected_lines"), + [ + ( + [["a"], SEPARATING_LINE, ["b"]], + ["+----+---+", "| 10 | a |", "|----+---|", "| 20 | b |", "+----+---+"], + ), + ( + [["a"], [SEPARATING_LINE], SEPARATING_LINE, ["b"]], + [ + "+----+---+", + "| 10 | a |", + "|----+---|", + "|----+---|", + "| 20 | b |", + "+----+---+", + ], + ), + ], +) +def test_supplied_index_with_separating_lines(index_factory, rows, expected_lines): + "A separator does not consume a custom row index." + result = tabulate(rows, showindex=index_factory([10, 20]), tablefmt="psql") + assert_equal("\n".join(expected_lines), result) + + +@mark.parametrize("index", [[10], [10, 20, 30]]) +def test_supplied_index_length_with_separating_lines(index): + "Index length validation counts data rows rather than separators." + rows = [["a"], SEPARATING_LINE, ["b"]] + with raises(ValueError): + tabulate(rows, showindex=index) + + +@mark.parametrize("showindex", ["default", "always", True]) +def test_dataframe_index_with_separating_line(showindex): + "A separator keeps the original labels of the remaining DataFrame rows." + pd = importorskip("pandas") + frame = pd.DataFrame( + [["a", 1], [SEPARATING_LINE, None], ["b", 2]], + index=["row-a", "separator", "row-b"], + ) + expected = "\n".join( + [ + "+-------+---+---+", + "| row-a | a | 1 |", + "+-------+---+---+", + "+-------+---+---+", + "| row-b | b | 2 |", + "+-------+---+---+", + ] + ) + assert_equal(expected, tabulate(frame, showindex=showindex, tablefmt="grid")) + + def test_list_of_lists_with_index_firstrow(): "Output: a table with a running index and header='firstrow'" dd = zip(*[["a"] + list(range(3)), ["b"] + list(range(101, 104))])