"""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))