Source code for terminaltables.base_table

"""Base table class. Define just the bare minimum to build tables."""

from terminaltables.build import build_border, build_row, flatten
from terminaltables.width_and_alignment import align_and_pad_cell, max_dimensions


class BaseTable(object):
    """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, title=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 = dict()  # {0: 'right', 1: 'left', 2: 'center'}
        self.padding_left = 1
        self.padding_right = 1

    def horizontal_border(self, style, outer_widths):
        """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, style, inner_widths, height):
        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 = list()

        # 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.
        for line in build_row(cells_in_row, left, center, right):
            yield line

    def gen_table(self, inner_widths, inner_heights, outer_widths):
        """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'
            for line in self.gen_row_lines(row, style, inner_widths, inner_heights[i]):
                yield line
            # 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):
        """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))