1
0
Fork 0
terminaltables/terminaltables3/base_table.py
Daniel Baumann 07735c967b
Merging upstream version 4.0.0 (Closes: #1095814).
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-02-12 15:00:49 +01:00

236 lines
9.9 KiB
Python

"""Base table class. Define just the bare minimum to build tables."""
from typing import Generator, Optional, Sequence, Tuple
from terminaltables3.build import build_border, build_row, flatten
from terminaltables3.width_and_alignment import align_and_pad_cell, max_dimensions
class BaseTable:
"""Base table class.
:ivar iter table_data: List (empty or list of lists of strings) representing the table.
:ivar str title: Optional title to show within the top border of the table.
:ivar bool inner_column_border: Separates columns.
:ivar bool inner_footing_row_border: Show a border before the last row.
:ivar bool inner_heading_row_border: Show a border after the first row.
:ivar bool inner_row_border: Show a border in between every row.
:ivar bool outer_border: Show the top, left, right, and bottom border.
:ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center.
:ivar int padding_left: Number of spaces to pad on the left side of every cell.
:ivar int padding_right: Number of spaces to pad on the right side of every cell.
"""
CHAR_F_INNER_HORIZONTAL = "-"
CHAR_F_INNER_INTERSECT = "+"
CHAR_F_INNER_VERTICAL = "|"
CHAR_F_OUTER_LEFT_INTERSECT = "+"
CHAR_F_OUTER_LEFT_VERTICAL = "|"
CHAR_F_OUTER_RIGHT_INTERSECT = "+"
CHAR_F_OUTER_RIGHT_VERTICAL = "|"
CHAR_H_INNER_HORIZONTAL = "-"
CHAR_H_INNER_INTERSECT = "+"
CHAR_H_INNER_VERTICAL = "|"
CHAR_H_OUTER_LEFT_INTERSECT = "+"
CHAR_H_OUTER_LEFT_VERTICAL = "|"
CHAR_H_OUTER_RIGHT_INTERSECT = "+"
CHAR_H_OUTER_RIGHT_VERTICAL = "|"
CHAR_INNER_HORIZONTAL = "-"
CHAR_INNER_INTERSECT = "+"
CHAR_INNER_VERTICAL = "|"
CHAR_OUTER_BOTTOM_HORIZONTAL = "-"
CHAR_OUTER_BOTTOM_INTERSECT = "+"
CHAR_OUTER_BOTTOM_LEFT = "+"
CHAR_OUTER_BOTTOM_RIGHT = "+"
CHAR_OUTER_LEFT_INTERSECT = "+"
CHAR_OUTER_LEFT_VERTICAL = "|"
CHAR_OUTER_RIGHT_INTERSECT = "+"
CHAR_OUTER_RIGHT_VERTICAL = "|"
CHAR_OUTER_TOP_HORIZONTAL = "-"
CHAR_OUTER_TOP_INTERSECT = "+"
CHAR_OUTER_TOP_LEFT = "+"
CHAR_OUTER_TOP_RIGHT = "+"
def __init__(
self, table_data: Sequence[Sequence[str]], title: Optional[str] = None
):
"""Constructor.
:param iter table_data: List (empty or list of lists of strings) representing the table.
:param title: Optional title to show within the top border of the table.
"""
self.table_data = table_data
self.title = title
self.inner_column_border = True
self.inner_footing_row_border = False
self.inner_heading_row_border = True
self.inner_row_border = False
self.outer_border = True
self.justify_columns = {} # {0: 'right', 1: 'left', 2: 'center'}
self.padding_left = 1
self.padding_right = 1
def horizontal_border(
self, style: str, outer_widths: Sequence[int]
) -> Tuple[str, ...]:
"""Build any kind of horizontal border for the table.
:param str style: Type of border to return.
:param iter outer_widths: List of widths (with padding) for each column.
:return: Prepared border as a tuple of strings.
:rtype: tuple
"""
if style == "top":
horizontal = self.CHAR_OUTER_TOP_HORIZONTAL
left = self.CHAR_OUTER_TOP_LEFT
intersect = (
self.CHAR_OUTER_TOP_INTERSECT if self.inner_column_border else ""
)
right = self.CHAR_OUTER_TOP_RIGHT
title = self.title
elif style == "bottom":
horizontal = self.CHAR_OUTER_BOTTOM_HORIZONTAL
left = self.CHAR_OUTER_BOTTOM_LEFT
intersect = (
self.CHAR_OUTER_BOTTOM_INTERSECT if self.inner_column_border else ""
)
right = self.CHAR_OUTER_BOTTOM_RIGHT
title = None
elif style == "heading":
horizontal = self.CHAR_H_INNER_HORIZONTAL
left = self.CHAR_H_OUTER_LEFT_INTERSECT if self.outer_border else ""
intersect = self.CHAR_H_INNER_INTERSECT if self.inner_column_border else ""
right = self.CHAR_H_OUTER_RIGHT_INTERSECT if self.outer_border else ""
title = None
elif style == "footing":
horizontal = self.CHAR_F_INNER_HORIZONTAL
left = self.CHAR_F_OUTER_LEFT_INTERSECT if self.outer_border else ""
intersect = self.CHAR_F_INNER_INTERSECT if self.inner_column_border else ""
right = self.CHAR_F_OUTER_RIGHT_INTERSECT if self.outer_border else ""
title = None
else:
horizontal = self.CHAR_INNER_HORIZONTAL
left = self.CHAR_OUTER_LEFT_INTERSECT if self.outer_border else ""
intersect = self.CHAR_INNER_INTERSECT if self.inner_column_border else ""
right = self.CHAR_OUTER_RIGHT_INTERSECT if self.outer_border else ""
title = None
return build_border(outer_widths, horizontal, left, intersect, right, title)
def gen_row_lines(
self, row: Sequence[str], style: str, inner_widths: Sequence[int], height: int
) -> Generator[Tuple[str, ...], None, None]:
r"""Combine cells in row and group them into lines with vertical borders.
Caller is expected to pass yielded lines to ''.join() to combine them into a printable line. Caller must append
newline character to the end of joined line.
In:
['Row One Column One', 'Two', 'Three']
Out:
[
('|', ' Row One Column One ', '|', ' Two ', '|', ' Three ', '|'),
]
In:
['Row One\nColumn One', 'Two', 'Three'],
Out:
[
('|', ' Row One ', '|', ' Two ', '|', ' Three ', '|'),
('|', ' Column One ', '|', ' ', '|', ' ', '|'),
]
:param iter row: One row in the table. List of cells.
:param str style: Type of border characters to use.
:param iter inner_widths: List of widths (no padding) for each column.
:param int height: Inner height (no padding) (number of lines) to expand row to.
:return: Yields lines split into components in a list. Caller must ''.join() line.
"""
cells_in_row = []
# Resize row if it doesn't have enough cells.
if len(row) != len(inner_widths):
row = row + [""] * (len(inner_widths) - len(row))
# Pad and align each cell. Split each cell into lines to support multi-line cells.
for i, cell in enumerate(row):
align = (self.justify_columns.get(i),)
inner_dimensions = (inner_widths[i], height)
padding = (self.padding_left, self.padding_right, 0, 0)
cells_in_row.append(
align_and_pad_cell(cell, align, inner_dimensions, padding)
)
# Determine border characters.
if style == "heading":
left = self.CHAR_H_OUTER_LEFT_VERTICAL if self.outer_border else ""
center = self.CHAR_H_INNER_VERTICAL if self.inner_column_border else ""
right = self.CHAR_H_OUTER_RIGHT_VERTICAL if self.outer_border else ""
elif style == "footing":
left = self.CHAR_F_OUTER_LEFT_VERTICAL if self.outer_border else ""
center = self.CHAR_F_INNER_VERTICAL if self.inner_column_border else ""
right = self.CHAR_F_OUTER_RIGHT_VERTICAL if self.outer_border else ""
else:
left = self.CHAR_OUTER_LEFT_VERTICAL if self.outer_border else ""
center = self.CHAR_INNER_VERTICAL if self.inner_column_border else ""
right = self.CHAR_OUTER_RIGHT_VERTICAL if self.outer_border else ""
# Yield each line.
yield from build_row(cells_in_row, left, center, right)
def gen_table(
self,
inner_widths: Sequence[int],
inner_heights: Sequence[int],
outer_widths: Sequence[int],
) -> Generator[Tuple[str, ...], None, None]:
"""Combine everything and yield every line of the entire table with borders.
:param iter inner_widths: List of widths (no padding) for each column.
:param iter inner_heights: List of heights (no padding) for each row.
:param iter outer_widths: List of widths (with padding) for each column.
:return:
"""
# Yield top border.
if self.outer_border:
yield self.horizontal_border("top", outer_widths)
# Yield table body.
row_count = len(self.table_data)
last_row_index, before_last_row_index = row_count - 1, row_count - 2
for i, row in enumerate(self.table_data):
# Yield the row line by line (e.g. multi-line rows).
if self.inner_heading_row_border and i == 0:
style = "heading"
elif self.inner_footing_row_border and i == last_row_index:
style = "footing"
else:
style = "row"
yield from self.gen_row_lines(row, style, inner_widths, inner_heights[i])
# If this is the last row then break. No separator needed.
if i == last_row_index:
break
# Yield heading separator.
if self.inner_heading_row_border and i == 0:
yield self.horizontal_border("heading", outer_widths)
# Yield footing separator.
elif self.inner_footing_row_border and i == before_last_row_index:
yield self.horizontal_border("footing", outer_widths)
# Yield row separator.
elif self.inner_row_border:
yield self.horizontal_border("row", outer_widths)
# Yield bottom border.
if self.outer_border:
yield self.horizontal_border("bottom", outer_widths)
@property
def table(self) -> str:
"""Return a large string of the entire table ready to be printed to the terminal."""
dimensions = max_dimensions(
self.table_data, self.padding_left, self.padding_right
)[:3]
return flatten(self.gen_table(*dimensions))