From e6470255e477a14bbd35614e105f4b7c781da95f Mon Sep 17 00:00:00 2001 From: Kevin Deldycke Date: Mon, 6 Apr 2026 12:06:08 +0200 Subject: [PATCH] Respect `padding` in pipe/github separator rows Follow up on #410; Refs #260 and #261. --- README.md | 6 ++--- tabulate/__init__.py | 57 ++++++++++++++++++++++++----------------- test/test_output.py | 17 ++++++------ test/test_regression.py | 10 ++++---- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 0283a0c..2e6e7c6 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ corresponds to the `pipe` format with the same alignment colons: ```pycon >>> print(tabulate(table, headers, tablefmt="github")) | item | qty | -|:-------|------:| +| :----- | ----: | | spam | 42 | | eggs | 451 | | bacon | 0 | @@ -487,7 +487,7 @@ indicate column alignment: ```pycon >>> print(tabulate(table, headers, tablefmt="pipe")) | item | qty | -|:-------|------:| +| :----- | ----: | | spam | 42 | | eggs | 451 | | bacon | 0 | @@ -931,7 +931,7 @@ spam >>> print(tabulate(table, headers, tablefmt="pipe")) | item | qty | | name | | -|:-------|------:| +| :----- | ----: | | eggs | 451 | | more | 42 | | spam | | diff --git a/tabulate/__init__.py b/tabulate/__init__.py index 12a2950..006b3a5 100644 --- a/tabulate/__init__.py +++ b/tabulate/__init__.py @@ -131,26 +131,37 @@ def _is_separating_line(row): ) -def _pipe_segment_with_colons(align, colwidth): +def _pipe_segment_with_colons(align, colwidth, padding=0): """Return a segment of a horizontal line with optional colons which - indicate column's alignment (as in `pipe` output format).""" + indicate column's alignment (as in `pipe` output format). + + When ``padding`` is non-zero, the segment respects the format's padding by + placing spaces at the padding positions instead of filling the entire width + with dashes. This makes separator rows consistent with data rows, which + already honour padding via ``_pad_row``. + """ w = colwidth + pad = " " * padding + # Width available for dashes and alignment colons. + dw = w - 2 * padding if align in ["right", "decimal"]: - return ("-" * (w - 1)) + ":" + return pad + ("-" * (dw - 1)) + ":" + pad elif align == "center": - return ":" + ("-" * (w - 2)) + ":" + return pad + ":" + ("-" * (dw - 2)) + ":" + pad elif align == "left": - return ":" + ("-" * (w - 1)) + return pad + ":" + ("-" * (dw - 1)) + pad else: - return "-" * w + return pad + ("-" * dw) + pad -def _pipe_line_with_colons(colwidths, colaligns): +def _pipe_line_with_colons(colwidths, colaligns, padding=0): """Return a horizontal line with optional colons to indicate column's alignment (as in `pipe` output format).""" if not colaligns: # e.g. printing an empty data frame (github issue #15) colaligns = [""] * len(colwidths) - segments = "|".join(_pipe_segment_with_colons(a, w) for a, w in zip(colaligns, colwidths)) + segments = "|".join( + _pipe_segment_with_colons(a, w, padding) for a, w in zip(colaligns, colwidths) + ) return f"|{segments}|" @@ -168,7 +179,7 @@ def _grid_segment_with_colons(colwidth, align): return "=" * width -def _grid_line_with_colons(colwidths, colaligns): +def _grid_line_with_colons(colwidths, colaligns, **kwargs): """Return a horizontal line with optional colons to indicate column's alignment in a grid table.""" if not colaligns: @@ -200,7 +211,7 @@ def _textile_row_with_attrs(cell_values, colwidths, colaligns): return f"|{values}|" -def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore): +def _html_begin_table_without_header(colwidths_ignore, colaligns_ignore, **kwargs): # this table header will be suppressed if there is a header row return "\n" @@ -242,7 +253,7 @@ def _moin_row_with_attrs(celltag, cell_values, colwidths, colaligns, header=""): return "".join(values_with_attrs) + "||" -def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=False): +def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=False, **kwargs): alignment = {"left": "l", "right": "r", "center": "c", "decimal": "r"} tabular_columns_fmt = "".join([alignment.get(a, "l") for a in colaligns]) return "\n".join( @@ -255,7 +266,7 @@ def _latex_line_begin_tabular(colwidths, colaligns, booktabs=False, longtable=Fa ) -def _asciidoc_row(is_header, *args): +def _asciidoc_row(is_header, *args, **kwargs): """handle header and data rows for asciidoc format""" def make_header_line(is_header, colwidths, colaligns): @@ -2063,7 +2074,7 @@ def tabulate( >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], ... ["strings", "numbers"], "pipe")) | strings | numbers | - |:----------|----------:| + | :-------- | --------: | | spam | 41.9999 | | eggs | 451 | @@ -2077,7 +2088,7 @@ def tabulate( eggs | 451 >>> print(tabulate([["spam", 41.9999], ["eggs", "451.0"]], tablefmt="pipe")) - |:-----|---------:| + | :--- | -------: | | spam | 41.9999 | | eggs | 451 | @@ -2578,12 +2589,12 @@ def _append_multiline_row( return lines -def _build_line(colwidths, colaligns, linefmt): +def _build_line(colwidths, colaligns, linefmt, padding=0): "Return a string which represents a horizontal line." if not linefmt: return None if callable(linefmt): - return linefmt(colwidths, colaligns) + return linefmt(colwidths, colaligns, padding=padding) else: begin, fill, sep, end = linefmt cells = [fill * w for w in colwidths] @@ -2591,8 +2602,8 @@ def _build_line(colwidths, colaligns, linefmt): return _build_simple_row(cells, rowfmt) -def _append_line(lines, colwidths, colaligns, linefmt): - lines.append(_build_line(colwidths, colaligns, linefmt)) +def _append_line(lines, colwidths, colaligns, linefmt, padding=0): + lines.append(_build_line(colwidths, colaligns, linefmt, padding=padding)) return lines @@ -2629,12 +2640,12 @@ def _format_table( padded_headers = pad_row(headers, pad) if fmt.lineabove and "lineabove" not in hidden: - _append_line(lines, padded_widths, colaligns, fmt.lineabove) + _append_line(lines, padded_widths, colaligns, fmt.lineabove, padding=pad) if padded_headers: append_row(lines, padded_headers, padded_widths, headersaligns, headerrow) if fmt.linebelowheader and "linebelowheader" not in hidden: - _append_line(lines, padded_widths, colaligns, fmt.linebelowheader) + _append_line(lines, padded_widths, colaligns, fmt.linebelowheader, padding=pad) if rows and fmt.linebetweenrows and "linebetweenrows" not in hidden: # initial rows with a line below @@ -2648,7 +2659,7 @@ def _format_table( fmt.datarow, rowalign=ralign, ) - _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows) + _append_line(lines, padded_widths, colaligns, fmt.linebetweenrows, padding=pad) # the last row without a line below append_row( lines, @@ -2670,12 +2681,12 @@ def _format_table( # test to see if either the 1st column or the 2nd column (account for showindex) has # the SEPARATING_LINE flag if _is_separating_line(row): - _append_line(lines, padded_widths, colaligns, separating_line) + _append_line(lines, padded_widths, colaligns, separating_line, padding=pad) else: append_row(lines, pad_row(row, pad), padded_widths, colaligns, fmt.datarow) if fmt.linebelow and "linebelow" not in hidden: - _append_line(lines, padded_widths, colaligns, fmt.linebelow) + _append_line(lines, padded_widths, colaligns, fmt.linebelow, padding=pad) if headers or rows: output = "\n".join(lines) diff --git a/test/test_output.py b/test/test_output.py index ea3da87..e3c9bad 100644 --- a/test/test_output.py +++ b/test/test_output.py @@ -369,9 +369,9 @@ def test_simple_headerless_with_sep_line_with_padding_in_tablefmt(): "Output: simple without headers with sep line with padding in tablefmt" expected = "\n".join( [ - "|:-----|---------:|", + "| :--- | -------: |", "| spam | 41.9999 |", - "|:-----|---------:|", + "| :--- | -------: |", "| eggs | 451 |", ] ) @@ -489,12 +489,13 @@ def test_github(): expected = "\n".join( [ "| strings | numbers |", - "|:----------|----------:|", + "| :-------- | --------: |", "| spam | 41.9999 |", "| eggs | 451 |", ] ) result = tabulate(_test_table, _test_table_headers, tablefmt="github") + assert_equal(expected, result) @@ -506,7 +507,7 @@ def test_github_multiline(): [ "| more | more spam |", "| spam eggs | & eggs |", - "|------------:|:------------|", + "| ----------: | :---------- |", "| 2 | foo |", "| | bar |", ] @@ -520,7 +521,7 @@ def test_github_with_colalign(): expected = "\n".join( [ "| Name | Age |", - "|:-------|------:|", + "| :----- | ----: |", "| Alice | 24 |", "| Bob | 19 |", ] @@ -539,7 +540,7 @@ def test_github_no_alignment(): expected = "\n".join( [ "| strings | numbers |", - "|-----------|-----------|", + "| --------- | --------- |", "| spam | 41.9999 |", "| eggs | 451 |", ] @@ -1995,7 +1996,7 @@ def test_pipe(): expected = "\n".join( [ "| strings | numbers |", - "|:----------|----------:|", + "| :-------- | --------: |", "| spam | 41.9999 |", "| eggs | 451 |", ] @@ -2006,7 +2007,7 @@ def test_pipe(): def test_pipe_headerless(): "Output: pipe without headers" - expected = "\n".join(["|:-----|---------:|", "| spam | 41.9999 |", "| eggs | 451 |"]) + expected = "\n".join(["| :--- | -------: |", "| spam | 41.9999 |", "| eggs | 451 |"]) result = tabulate(_test_table, tablefmt="pipe") assert_equal(expected, result) diff --git a/test/test_regression.py b/test/test_regression.py index 9555676..8ead6c1 100644 --- a/test/test_regression.py +++ b/test/test_regression.py @@ -13,7 +13,7 @@ def test_ansi_color_in_table_cells(): expected = "\n".join( [ "| test | test | test |", - "|:-------|:-------|:-------|", + "| :----- | :----- | :----- |", "| test | \x1b[31mtest\x1b[0m | \x1b[32mtest\x1b[0m |", ] ) @@ -195,7 +195,7 @@ def test_88_256_ANSI_color_codes(): expected = "\n".join( [ "| background | foreground |", - "|:-------------|:-------------|", + "| :----------- | :----------- |", "| \x1b[48;5;196mred\x1b[49m | \x1b[38;5;196mred\x1b[39m |", ] ) @@ -424,7 +424,7 @@ def test_empty_pipe_table_with_columns(): "Regression: allow empty pipe tables with columns, like empty dataframes (github issue #15)" table = [] headers = ["Col1", "Col2"] - expected = "\n".join(["| Col1 | Col2 |", "|--------|--------|"]) + expected = "\n".join(["| Col1 | Col2 |", "| ------ | ------ |"]) result = tabulate(table, headers, tablefmt="pipe") assert_equal(expected, result) @@ -542,7 +542,7 @@ def test_numpy_int64_as_integer(): expected = "\n".join( [ "| int | float |", - "|------:|--------:|", + "| ----: | ------: |", "| 1 | 3.14 |", ] ) @@ -596,5 +596,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)