Skip to content

Commit

Permalink
Merge pull request astanin#118 from magelisk/94-row-vertical-alignment
Browse files Browse the repository at this point in the history
Rows can specify top, bottom, or center alignment for their cell text
  • Loading branch information
astanin authored Jun 22, 2022
2 parents cca9fc4 + 8cee80b commit 246bcdb
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 9 deletions.
52 changes: 43 additions & 9 deletions tabulate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1341,6 +1341,7 @@ def tabulate(
disable_numparse=False,
colalign=None,
maxcolwidths=None,
rowalign=None,
maxheadercolwidths=None,
):
"""Format a fixed width table for pretty printing.
Expand Down Expand Up @@ -1912,7 +1913,12 @@ def tabulate(
if not isinstance(tablefmt, TableFormat):
tablefmt = _table_formats.get(tablefmt, _table_formats["simple"])

return _format_table(tablefmt, headers, rows, minwidths, aligns, is_multiline)
ra_default = rowalign if isinstance(rowalign, str) else None
rowaligns = _expand_iterable(rowalign, len(rows), ra_default)

return _format_table(
tablefmt, headers, rows, minwidths, aligns, is_multiline, rowaligns=rowaligns
)


def _expand_numparse(disable_numparse, column_count):
Expand Down Expand Up @@ -1940,7 +1946,7 @@ def _expand_iterable(original, num_desired, default):
If `original` is not a list to begin with (i.e. scalar value) a list of
length `num_desired` completely populated with `default will be returned
"""
if isinstance(original, Iterable):
if isinstance(original, Iterable) and not isinstance(original, str):
return original + [default] * (num_desired - len(original))
else:
return [default] * num_desired
Expand Down Expand Up @@ -1971,20 +1977,39 @@ def _build_row(padded_cells, colwidths, colaligns, rowfmt):
return _build_simple_row(padded_cells, rowfmt)


def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt):
def _append_basic_row(lines, padded_cells, colwidths, colaligns, rowfmt, rowalign=None):
# NOTE: rowalign is ignored and exists for api compatibility with _append_multiline_row
lines.append(_build_row(padded_cells, colwidths, colaligns, rowfmt))
return lines


def _align_cell_veritically(text_lines, num_lines, column_width, row_alignment):
delta_lines = num_lines - len(text_lines)
blank = [" " * column_width]
if row_alignment == "bottom":
return blank * delta_lines + text_lines
elif row_alignment == "center":
top_delta = delta_lines // 2
bottom_delta = delta_lines - top_delta
return top_delta * blank + text_lines + bottom_delta * blank
else:
return text_lines + blank * delta_lines


def _append_multiline_row(
lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad
lines, padded_multiline_cells, padded_widths, colaligns, rowfmt, pad, rowalign=None
):
colwidths = [w - 2 * pad for w in padded_widths]
cells_lines = [c.splitlines() for c in padded_multiline_cells]
nlines = max(map(len, cells_lines)) # number of lines in the row
# vertically pad cells where some lines are missing
# cells_lines = [
# (cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths)
# ]

cells_lines = [
(cl + [" " * w] * (nlines - len(cl))) for cl, w in zip(cells_lines, colwidths)
_align_cell_veritically(cl, nlines, w, rowalign)
for cl, w in zip(cells_lines, colwidths)
]
lines_cells = [[cl[i] for cl in cells_lines] for i in range(nlines)]
for ln in lines_cells:
Expand Down Expand Up @@ -2023,7 +2048,7 @@ def str(self):
return self


def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline):
def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline, rowaligns):
"""Produce a plain-text representation of the table."""
lines = []
hidden = fmt.with_header_hide if (headers and fmt.with_header_hide) else []
Expand Down Expand Up @@ -2051,11 +2076,20 @@ def _format_table(fmt, headers, rows, colwidths, colaligns, is_multiline):

if padded_rows and fmt.linebetweenrows and "linebetweenrows" not in hidden:
# initial rows with a line below
for row in padded_rows[:-1]:
append_row(lines, row, padded_widths, colaligns, fmt.datarow)
for row, ralign in zip(padded_rows[:-1], rowaligns):
append_row(
lines, row, padded_widths, colaligns, fmt.datarow, rowalign=ralign
)
_append_line(lines, padded_widths, colaligns, fmt.linebetweenrows)
# the last row without a line below
append_row(lines, padded_rows[-1], padded_widths, colaligns, fmt.datarow)
append_row(
lines,
padded_rows[-1],
padded_widths,
colaligns,
fmt.datarow,
rowalign=rowaligns[-1],
)
else:
for row in padded_rows:
append_row(lines, row, padded_widths, colaligns, fmt.datarow)
Expand Down
1 change: 1 addition & 0 deletions test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ def test_tabulate_signature():
("disable_numparse", False),
("colalign", None),
("maxcolwidths", None),
("rowalign", None),
("maxheadercolwidths", None),
]
_check_signature(tabulate, expected_sig)
Expand Down
73 changes: 73 additions & 0 deletions test/test_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,79 @@ def test_align_column_multiline():
assert_equal(output, expected)


def test_align_cell_veritically_one_line_only():
"Internal: Aligning a single height cell is same regardless of alignment value"
lines = ["one line"]
column_width = 8

top = T._align_cell_veritically(lines, 1, column_width, "top")
center = T._align_cell_veritically(lines, 1, column_width, "center")
bottom = T._align_cell_veritically(lines, 1, column_width, "bottom")
none = T._align_cell_veritically(lines, 1, column_width, None)

expected = ["one line"]
assert top == center == bottom == none == expected


def test_align_cell_veritically_top_single_text_multiple_pad():
"Internal: Align single cell text to top"
result = T._align_cell_veritically(["one line"], 3, 8, "top")

expected = ["one line", " ", " "]

assert_equal(expected, result)


def test_align_cell_veritically_center_single_text_multiple_pad():
"Internal: Align single cell text to center"
result = T._align_cell_veritically(["one line"], 3, 8, "center")

expected = [" ", "one line", " "]

assert_equal(expected, result)


def test_align_cell_veritically_bottom_single_text_multiple_pad():
"Internal: Align single cell text to bottom"
result = T._align_cell_veritically(["one line"], 3, 8, "bottom")

expected = [" ", " ", "one line"]

assert_equal(expected, result)


def test_align_cell_veritically_top_multi_text_multiple_pad():
"Internal: Align multiline celltext text to top"
text = ["just", "one ", "cell"]
result = T._align_cell_veritically(text, 6, 4, "top")

expected = ["just", "one ", "cell", " ", " ", " "]

assert_equal(expected, result)


def test_align_cell_veritically_center_multi_text_multiple_pad():
"Internal: Align multiline celltext text to center"
text = ["just", "one ", "cell"]
result = T._align_cell_veritically(text, 6, 4, "center")

# Even number of rows, can't perfectly center, but we pad less
# at top when required to do make a judgement
expected = [" ", "just", "one ", "cell", " ", " "]

assert_equal(expected, result)


def test_align_cell_veritically_bottom_multi_text_multiple_pad():
"Internal: Align multiline celltext text to bottom"
text = ["just", "one ", "cell"]
result = T._align_cell_veritically(text, 6, 4, "bottom")

expected = [" ", " ", " ", "just", "one ", "cell"]

assert_equal(expected, result)


def test_wrap_text_to_colwidths():
"Internal: Test _wrap_text_to_colwidths to show it will wrap text based on colwidths"
rows = [
Expand Down
31 changes: 31 additions & 0 deletions test/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,37 @@ def test_fancy_grid_multiline_with_empty_cells_headerless():
assert_equal(expected, result)


def test_fancy_grid_multiline_row_align():
"Output: fancy_grid with multiline cells aligning some text not to top of cell"
table = [
["0", "some\ndefault\ntext", "up\ntop"],
["1", "very\nlong\ndata\ncell", "mid\ntest"],
["2", "also\nvery\nlong\ndata\ncell", "fold\nthis"],
]
expected = "\n".join(
[
"╒═══╤═════════╤══════╕",
"│ 0 │ some │ up │",
"│ │ default │ top │",
"│ │ text │ │",
"├───┼─────────┼──────┤",
"│ │ very │ │",
"│ 1 │ long │ mid │",
"│ │ data │ test │",
"│ │ cell │ │",
"├───┼─────────┼──────┤",
"│ │ also │ │",
"│ │ very │ │",
"│ │ long │ │",
"│ │ data │ fold │",
"│ 2 │ cell │ this │",
"╘═══╧═════════╧══════╛",
]
)
result = tabulate(table, tablefmt="fancy_grid", rowalign=[None, "center", "bottom"])
assert_equal(expected, result)


def test_outline():
"Output: outline with headers"
expected = "\n".join(
Expand Down

0 comments on commit 246bcdb

Please sign in to comment.