From 0dfe1c9e2780469e3a4696e8fb3e6f717a7ebeb7 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 16 Sep 2022 11:09:35 +0200 Subject: Adding upstream version 3.1.0. Signed-off-by: Daniel Baumann --- terminaltables/__init__.py | 17 +++ terminaltables/ascii_table.py | 55 +++++++++ terminaltables/base_table.py | 217 ++++++++++++++++++++++++++++++++++ terminaltables/build.py | 151 +++++++++++++++++++++++ terminaltables/github_table.py | 70 +++++++++++ terminaltables/other_tables.py | 177 +++++++++++++++++++++++++++ terminaltables/terminal_io.py | 98 +++++++++++++++ terminaltables/width_and_alignment.py | 160 +++++++++++++++++++++++++ 8 files changed, 945 insertions(+) create mode 100644 terminaltables/__init__.py create mode 100644 terminaltables/ascii_table.py create mode 100644 terminaltables/base_table.py create mode 100644 terminaltables/build.py create mode 100644 terminaltables/github_table.py create mode 100644 terminaltables/other_tables.py create mode 100644 terminaltables/terminal_io.py create mode 100644 terminaltables/width_and_alignment.py (limited to 'terminaltables') diff --git a/terminaltables/__init__.py b/terminaltables/__init__.py new file mode 100644 index 0000000..6cea813 --- /dev/null +++ b/terminaltables/__init__.py @@ -0,0 +1,17 @@ +"""Generate simple tables in terminals from a nested list of strings. + +Use SingleTable or DoubleTable instead of AsciiTable for box-drawing characters. + +https://github.com/Robpol86/terminaltables +https://pypi.python.org/pypi/terminaltables +""" + +from terminaltables.ascii_table import AsciiTable # noqa +from terminaltables.github_table import GithubFlavoredMarkdownTable # noqa +from terminaltables.other_tables import DoubleTable # noqa +from terminaltables.other_tables import SingleTable # noqa +from terminaltables.other_tables import PorcelainTable # noqa + +__author__ = '@Robpol86' +__license__ = 'MIT' +__version__ = '3.1.0' diff --git a/terminaltables/ascii_table.py b/terminaltables/ascii_table.py new file mode 100644 index 0000000..3623918 --- /dev/null +++ b/terminaltables/ascii_table.py @@ -0,0 +1,55 @@ +"""AsciiTable is the main table class. To be inherited by other tables. Define convenience methods here.""" + +from terminaltables.base_table import BaseTable +from terminaltables.terminal_io import terminal_size +from terminaltables.width_and_alignment import column_max_width, max_dimensions, table_width + + +class AsciiTable(BaseTable): + """Draw a table using regular ASCII characters, such as ``+``, ``|``, and ``-``. + + :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. + """ + + def column_max_width(self, column_number): + """Return the maximum width of a column based on the current terminal width. + + :param int column_number: The column number to query. + + :return: The max width of the column. + :rtype: int + """ + inner_widths = max_dimensions(self.table_data)[0] + outer_border = 2 if self.outer_border else 0 + inner_border = 1 if self.inner_column_border else 0 + padding = self.padding_left + self.padding_right + return column_max_width(inner_widths, column_number, outer_border, inner_border, padding) + + @property + def column_widths(self): + """Return a list of integers representing the widths of each table column without padding.""" + if not self.table_data: + return list() + return max_dimensions(self.table_data)[0] + + @property + def ok(self): # Too late to change API. # pylint: disable=invalid-name + """Return True if the table fits within the terminal width, False if the table breaks.""" + return self.table_width <= terminal_size()[0] + + @property + def table_width(self): + """Return the width of the table including padding and borders.""" + outer_widths = max_dimensions(self.table_data, self.padding_left, self.padding_right)[2] + outer_border = 2 if self.outer_border else 0 + inner_border = 1 if self.inner_column_border else 0 + return table_width(outer_widths, outer_border, inner_border) diff --git a/terminaltables/base_table.py b/terminaltables/base_table.py new file mode 100644 index 0000000..281d5a3 --- /dev/null +++ b/terminaltables/base_table.py @@ -0,0 +1,217 @@ +"""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)) diff --git a/terminaltables/build.py b/terminaltables/build.py new file mode 100644 index 0000000..6b23b2f --- /dev/null +++ b/terminaltables/build.py @@ -0,0 +1,151 @@ +"""Combine cells into rows.""" + +from terminaltables.width_and_alignment import visible_width + + +def combine(line, left, intersect, right): + """Zip borders between items in `line`. + + e.g. ('l', '1', 'c', '2', 'c', '3', 'r') + + :param iter line: List to iterate. + :param left: Left border. + :param intersect: Column separator. + :param right: Right border. + + :return: Yields combined objects. + """ + # Yield left border. + if left: + yield left + + # Yield items with intersect characters. + if intersect: + try: + for j, i in enumerate(line, start=-len(line) + 1): + yield i + if j: + yield intersect + except TypeError: # Generator. + try: + item = next(line) + except StopIteration: # Was empty all along. + pass + else: + while True: + yield item + try: + peek = next(line) + except StopIteration: + break + yield intersect + item = peek + else: + for i in line: + yield i + + # Yield right border. + if right: + yield right + + +def build_border(outer_widths, horizontal, left, intersect, right, title=None): + """Build the top/bottom/middle row. Optionally embed the table title within the border. + + Title is hidden if it doesn't fit between the left/right characters/edges. + + Example return value: + ('<', '-----', '+', '------', '+', '-------', '>') + ('<', 'My Table', '----', '+', '------->') + + :param iter outer_widths: List of widths (with padding) for each column. + :param str horizontal: Character to stretch across each column. + :param str left: Left border. + :param str intersect: Column separator. + :param str right: Right border. + :param title: Overlay the title on the border between the left and right characters. + + :return: Returns a generator of strings representing a border. + :rtype: iter + """ + length = 0 + + # Hide title if it doesn't fit. + if title is not None and outer_widths: + try: + length = visible_width(title) + except TypeError: + title = str(title) + length = visible_width(title) + if length > sum(outer_widths) + len(intersect) * (len(outer_widths) - 1): + title = None + + # Handle no title. + if title is None or not outer_widths or not horizontal: + return combine((horizontal * c for c in outer_widths), left, intersect, right) + + # Handle title fitting in the first column. + if length == outer_widths[0]: + return combine([title] + [horizontal * c for c in outer_widths[1:]], left, intersect, right) + if length < outer_widths[0]: + columns = [title + horizontal * (outer_widths[0] - length)] + [horizontal * c for c in outer_widths[1:]] + return combine(columns, left, intersect, right) + + # Handle wide titles/narrow columns. + columns_and_intersects = [title] + for width in combine(outer_widths, None, bool(intersect), None): + # If title is taken care of. + if length < 1: + columns_and_intersects.append(intersect if width is True else horizontal * width) + # If title's last character overrides an intersect character. + elif width is True and length == 1: + length = 0 + # If this is an intersect character that is overridden by the title. + elif width is True: + length -= 1 + # If title's last character is within a column. + elif width >= length: + columns_and_intersects[0] += horizontal * (width - length) # Append horizontal chars to title. + length = 0 + # If remainder of title won't fit in a column. + else: + length -= width + + return combine(columns_and_intersects, left, None, right) + + +def build_row(row, left, center, right): + """Combine single or multi-lined cells into a single row of list of lists including borders. + + Row must already be padded and extended so each cell has the same number of lines. + + Example return value: + [ + ['>', 'Left ', '|', 'Center', '|', 'Right', '<'], + ['>', 'Cell1', '|', 'Cell2 ', '|', 'Cell3', '<'], + ] + + :param iter row: List of cells for one row. + :param str left: Left border. + :param str center: Column separator. + :param str right: Right border. + + :return: Yields other generators that yield strings. + :rtype: iter + """ + if not row or not row[0]: + yield combine((), left, center, right) + return + for row_index in range(len(row[0])): + yield combine((c[row_index] for c in row), left, center, right) + + +def flatten(table): + """Flatten table data into a single string with newlines. + + :param iter table: Padded and bordered table data. + + :return: Joined rows/cells. + :rtype: str + """ + return '\n'.join(''.join(r) for r in table) diff --git a/terminaltables/github_table.py b/terminaltables/github_table.py new file mode 100644 index 0000000..7eb1be7 --- /dev/null +++ b/terminaltables/github_table.py @@ -0,0 +1,70 @@ +"""GithubFlavoredMarkdownTable class.""" + +from terminaltables.ascii_table import AsciiTable +from terminaltables.build import combine + + +class GithubFlavoredMarkdownTable(AsciiTable): + """Github flavored markdown table. + + https://help.github.com/articles/github-flavored-markdown/#tables + + :ivar iter table_data: List (empty or list of lists of strings) representing the table. + :ivar dict justify_columns: Horizontal justification. Keys are column indexes (int). Values are right/left/center. + """ + + def __init__(self, table_data): + """Constructor. + + :param iter table_data: List (empty or list of lists of strings) representing the table. + """ + # Github flavored markdown table won't support title. + super(GithubFlavoredMarkdownTable, self).__init__(table_data) + + def horizontal_border(self, _, outer_widths): + """Handle the GitHub heading border. + + E.g.: + |:---|:---:|---:|----| + + :param _: Unused. + :param iter outer_widths: List of widths (with padding) for each column. + + :return: Prepared border strings in a generator. + :rtype: iter + """ + horizontal = str(self.CHAR_INNER_HORIZONTAL) + left = self.CHAR_OUTER_LEFT_VERTICAL + intersect = self.CHAR_INNER_VERTICAL + right = self.CHAR_OUTER_RIGHT_VERTICAL + + columns = list() + for i, width in enumerate(outer_widths): + justify = self.justify_columns.get(i) + width = max(3, width) # Width should be at least 3 so justification can be applied. + if justify == 'left': + columns.append(':' + horizontal * (width - 1)) + elif justify == 'right': + columns.append(horizontal * (width - 1) + ':') + elif justify == 'center': + columns.append(':' + horizontal * (width - 2) + ':') + else: + columns.append(horizontal * width) + + return combine(columns, left, intersect, right) + + 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: + """ + for i, row in enumerate(self.table_data): + # Yield the row line by line (e.g. multi-line rows). + for line in self.gen_row_lines(row, 'row', inner_widths, inner_heights[i]): + yield line + # Yield heading separator. + if i == 0: + yield self.horizontal_border(None, outer_widths) diff --git a/terminaltables/other_tables.py b/terminaltables/other_tables.py new file mode 100644 index 0000000..50c0bcd --- /dev/null +++ b/terminaltables/other_tables.py @@ -0,0 +1,177 @@ +"""Additional simple tables defined here.""" + +from terminaltables.ascii_table import AsciiTable +from terminaltables.terminal_io import IS_WINDOWS + + +class UnixTable(AsciiTable): + """Draw a table using box-drawing characters on Unix platforms. Table borders won't have any gaps between lines. + + Similar to the tables shown on PC BIOS boot messages, but not double-lined. + """ + + CHAR_F_INNER_HORIZONTAL = '\033(0\x71\033(B' + CHAR_F_INNER_INTERSECT = '\033(0\x6e\033(B' + CHAR_F_INNER_VERTICAL = '\033(0\x78\033(B' + CHAR_F_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B' + CHAR_F_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B' + CHAR_F_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B' + CHAR_F_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B' + CHAR_H_INNER_HORIZONTAL = '\033(0\x71\033(B' + CHAR_H_INNER_INTERSECT = '\033(0\x6e\033(B' + CHAR_H_INNER_VERTICAL = '\033(0\x78\033(B' + CHAR_H_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B' + CHAR_H_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B' + CHAR_H_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B' + CHAR_H_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B' + CHAR_INNER_HORIZONTAL = '\033(0\x71\033(B' + CHAR_INNER_INTERSECT = '\033(0\x6e\033(B' + CHAR_INNER_VERTICAL = '\033(0\x78\033(B' + CHAR_OUTER_BOTTOM_HORIZONTAL = '\033(0\x71\033(B' + CHAR_OUTER_BOTTOM_INTERSECT = '\033(0\x76\033(B' + CHAR_OUTER_BOTTOM_LEFT = '\033(0\x6d\033(B' + CHAR_OUTER_BOTTOM_RIGHT = '\033(0\x6a\033(B' + CHAR_OUTER_LEFT_INTERSECT = '\033(0\x74\033(B' + CHAR_OUTER_LEFT_VERTICAL = '\033(0\x78\033(B' + CHAR_OUTER_RIGHT_INTERSECT = '\033(0\x75\033(B' + CHAR_OUTER_RIGHT_VERTICAL = '\033(0\x78\033(B' + CHAR_OUTER_TOP_HORIZONTAL = '\033(0\x71\033(B' + CHAR_OUTER_TOP_INTERSECT = '\033(0\x77\033(B' + CHAR_OUTER_TOP_LEFT = '\033(0\x6c\033(B' + CHAR_OUTER_TOP_RIGHT = '\033(0\x6b\033(B' + + @property + def table(self): + """Return a large string of the entire table ready to be printed to the terminal.""" + ascii_table = super(UnixTable, self).table + optimized = ascii_table.replace('\033(B\033(0', '') + return optimized + + +class WindowsTable(AsciiTable): + """Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Single-line borders. + + From: http://en.wikipedia.org/wiki/Code_page_437#Characters + """ + + CHAR_F_INNER_HORIZONTAL = b'\xc4'.decode('ibm437') + CHAR_F_INNER_INTERSECT = b'\xc5'.decode('ibm437') + CHAR_F_INNER_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_F_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437') + CHAR_F_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_F_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437') + CHAR_F_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_H_INNER_HORIZONTAL = b'\xc4'.decode('ibm437') + CHAR_H_INNER_INTERSECT = b'\xc5'.decode('ibm437') + CHAR_H_INNER_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_H_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437') + CHAR_H_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_H_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437') + CHAR_H_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_INNER_HORIZONTAL = b'\xc4'.decode('ibm437') + CHAR_INNER_INTERSECT = b'\xc5'.decode('ibm437') + CHAR_INNER_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_OUTER_BOTTOM_HORIZONTAL = b'\xc4'.decode('ibm437') + CHAR_OUTER_BOTTOM_INTERSECT = b'\xc1'.decode('ibm437') + CHAR_OUTER_BOTTOM_LEFT = b'\xc0'.decode('ibm437') + CHAR_OUTER_BOTTOM_RIGHT = b'\xd9'.decode('ibm437') + CHAR_OUTER_LEFT_INTERSECT = b'\xc3'.decode('ibm437') + CHAR_OUTER_LEFT_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_OUTER_RIGHT_INTERSECT = b'\xb4'.decode('ibm437') + CHAR_OUTER_RIGHT_VERTICAL = b'\xb3'.decode('ibm437') + CHAR_OUTER_TOP_HORIZONTAL = b'\xc4'.decode('ibm437') + CHAR_OUTER_TOP_INTERSECT = b'\xc2'.decode('ibm437') + CHAR_OUTER_TOP_LEFT = b'\xda'.decode('ibm437') + CHAR_OUTER_TOP_RIGHT = b'\xbf'.decode('ibm437') + + +class WindowsTableDouble(AsciiTable): + """Draw a table using box-drawing characters on Windows platforms. This uses Code Page 437. Double-line borders.""" + + CHAR_F_INNER_HORIZONTAL = b'\xcd'.decode('ibm437') + CHAR_F_INNER_INTERSECT = b'\xce'.decode('ibm437') + CHAR_F_INNER_VERTICAL = b'\xba'.decode('ibm437') + CHAR_F_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437') + CHAR_F_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437') + CHAR_F_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437') + CHAR_F_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437') + CHAR_H_INNER_HORIZONTAL = b'\xcd'.decode('ibm437') + CHAR_H_INNER_INTERSECT = b'\xce'.decode('ibm437') + CHAR_H_INNER_VERTICAL = b'\xba'.decode('ibm437') + CHAR_H_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437') + CHAR_H_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437') + CHAR_H_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437') + CHAR_H_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437') + CHAR_INNER_HORIZONTAL = b'\xcd'.decode('ibm437') + CHAR_INNER_INTERSECT = b'\xce'.decode('ibm437') + CHAR_INNER_VERTICAL = b'\xba'.decode('ibm437') + CHAR_OUTER_BOTTOM_HORIZONTAL = b'\xcd'.decode('ibm437') + CHAR_OUTER_BOTTOM_INTERSECT = b'\xca'.decode('ibm437') + CHAR_OUTER_BOTTOM_LEFT = b'\xc8'.decode('ibm437') + CHAR_OUTER_BOTTOM_RIGHT = b'\xbc'.decode('ibm437') + CHAR_OUTER_LEFT_INTERSECT = b'\xcc'.decode('ibm437') + CHAR_OUTER_LEFT_VERTICAL = b'\xba'.decode('ibm437') + CHAR_OUTER_RIGHT_INTERSECT = b'\xb9'.decode('ibm437') + CHAR_OUTER_RIGHT_VERTICAL = b'\xba'.decode('ibm437') + CHAR_OUTER_TOP_HORIZONTAL = b'\xcd'.decode('ibm437') + CHAR_OUTER_TOP_INTERSECT = b'\xcb'.decode('ibm437') + CHAR_OUTER_TOP_LEFT = b'\xc9'.decode('ibm437') + CHAR_OUTER_TOP_RIGHT = b'\xbb'.decode('ibm437') + + +class SingleTable(WindowsTable if IS_WINDOWS else UnixTable): + """Cross-platform table with single-line box-drawing characters. + + :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. + """ + + pass + + +class DoubleTable(WindowsTableDouble): + """Cross-platform table with box-drawing characters. On Windows it's double borders, on Linux/OSX it's unicode. + + :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. + """ + + pass + + +class PorcelainTable(AsciiTable): + """An AsciiTable stripped to a minimum. + + Meant to be machine passable and roughly follow format set by git --porcelain option (hence the name). + + :ivar iter table_data: List (empty or list of lists of strings) representing the table. + """ + + def __init__(self, table_data): + """Constructor. + + :param iter table_data: List (empty or list of lists of strings) representing the table. + """ + # Porcelain table won't support title since it has no outer birders. + super(PorcelainTable, self).__init__(table_data) + + # Removes outer border, and inner footing and header row borders. + self.inner_footing_row_border = False + self.inner_heading_row_border = False + self.outer_border = False diff --git a/terminaltables/terminal_io.py b/terminaltables/terminal_io.py new file mode 100644 index 0000000..8b8c10d --- /dev/null +++ b/terminaltables/terminal_io.py @@ -0,0 +1,98 @@ +"""Get info about the current terminal window/screen buffer.""" + +import ctypes +import struct +import sys + +DEFAULT_HEIGHT = 24 +DEFAULT_WIDTH = 79 +INVALID_HANDLE_VALUE = -1 +IS_WINDOWS = sys.platform == 'win32' +STD_ERROR_HANDLE = -12 +STD_OUTPUT_HANDLE = -11 + + +def get_console_info(kernel32, handle): + """Get information about this current console window (Windows only). + + https://github.com/Robpol86/colorclass/blob/ab42da59/colorclass/windows.py#L111 + + :raise OSError: When handle is invalid or GetConsoleScreenBufferInfo API call fails. + + :param ctypes.windll.kernel32 kernel32: Loaded kernel32 instance. + :param int handle: stderr or stdout handle. + + :return: Width (number of characters) and height (number of lines) of the terminal. + :rtype: tuple + """ + if handle == INVALID_HANDLE_VALUE: + raise OSError('Invalid handle.') + + # Query Win32 API. + lpcsbi = ctypes.create_string_buffer(22) # Populated by GetConsoleScreenBufferInfo. + if not kernel32.GetConsoleScreenBufferInfo(handle, lpcsbi): + raise ctypes.WinError() # Subclass of OSError. + + # Parse data. + left, top, right, bottom = struct.unpack('hhhhHhhhhhh', lpcsbi.raw)[5:-2] + width, height = right - left, bottom - top + return width, height + + +def terminal_size(kernel32=None): + """Get the width and height of the terminal. + + http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/ + http://stackoverflow.com/questions/17993814/why-the-irrelevant-code-made-a-difference + + :param kernel32: Optional mock kernel32 object. For testing. + + :return: Width (number of characters) and height (number of lines) of the terminal. + :rtype: tuple + """ + if IS_WINDOWS: + kernel32 = kernel32 or ctypes.windll.kernel32 + try: + return get_console_info(kernel32, kernel32.GetStdHandle(STD_ERROR_HANDLE)) + except OSError: + try: + return get_console_info(kernel32, kernel32.GetStdHandle(STD_OUTPUT_HANDLE)) + except OSError: + return DEFAULT_WIDTH, DEFAULT_HEIGHT + + try: + device = __import__('fcntl').ioctl(0, __import__('termios').TIOCGWINSZ, '\0\0\0\0\0\0\0\0') + except IOError: + return DEFAULT_WIDTH, DEFAULT_HEIGHT + height, width = struct.unpack('hhhh', device)[:2] + return width, height + + +def set_terminal_title(title, kernel32=None): + """Set the terminal title. + + :param title: The title to set (string, unicode, bytes accepted). + :param kernel32: Optional mock kernel32 object. For testing. + + :return: If title changed successfully (Windows only, always True on Linux/OSX). + :rtype: bool + """ + try: + title_bytes = title.encode('utf-8') + except AttributeError: + title_bytes = title + + if IS_WINDOWS: + kernel32 = kernel32 or ctypes.windll.kernel32 + try: + is_ascii = all(ord(c) < 128 for c in title) # str/unicode. + except TypeError: + is_ascii = all(c < 128 for c in title) # bytes. + if is_ascii: + return kernel32.SetConsoleTitleA(title_bytes) != 0 + else: + return kernel32.SetConsoleTitleW(title) != 0 + + # Linux/OSX. + sys.stdout.write(b'\033]0;' + title_bytes + b'\007') + return True diff --git a/terminaltables/width_and_alignment.py b/terminaltables/width_and_alignment.py new file mode 100644 index 0000000..057e800 --- /dev/null +++ b/terminaltables/width_and_alignment.py @@ -0,0 +1,160 @@ +"""Functions that handle alignment, padding, widths, etc.""" + +import re +import unicodedata + +from terminaltables.terminal_io import terminal_size + +RE_COLOR_ANSI = re.compile(r'(\033\[[\d;]+m)') + + +def visible_width(string): + """Get the visible width of a unicode string. + + Some CJK unicode characters are more than one byte unlike ASCII and latin unicode characters. + + From: https://github.com/Robpol86/terminaltables/pull/9 + + :param str string: String to measure. + + :return: String's width. + :rtype: int + """ + if '\033' in string: + string = RE_COLOR_ANSI.sub('', string) + + # Convert to unicode. + try: + string = string.decode('u8') + except (AttributeError, UnicodeEncodeError): + pass + + width = 0 + for char in string: + if unicodedata.east_asian_width(char) in ('F', 'W'): + width += 2 + else: + width += 1 + + return width + + +def align_and_pad_cell(string, align, inner_dimensions, padding, space=' '): + """Align a string horizontally and vertically. Also add additional padding in both dimensions. + + :param str string: Input string to operate on. + :param tuple align: Tuple that contains one of left/center/right and/or top/middle/bottom. + :param tuple inner_dimensions: Width and height ints to expand string to without padding. + :param iter padding: Number of space chars for left, right, top, and bottom (4 ints). + :param str space: Character to use as white space for resizing/padding (use single visible chars only). + + :return: Padded cell split into lines. + :rtype: list + """ + if not hasattr(string, 'splitlines'): + string = str(string) + + # Handle trailing newlines or empty strings, str.splitlines() does not satisfy. + lines = string.splitlines() or [''] + if string.endswith('\n'): + lines.append('') + + # Vertically align and pad. + if 'bottom' in align: + lines = ([''] * (inner_dimensions[1] - len(lines) + padding[2])) + lines + ([''] * padding[3]) + elif 'middle' in align: + delta = inner_dimensions[1] - len(lines) + lines = ([''] * (delta // 2 + delta % 2 + padding[2])) + lines + ([''] * (delta // 2 + padding[3])) + else: + lines = ([''] * padding[2]) + lines + ([''] * (inner_dimensions[1] - len(lines) + padding[3])) + + # Horizontally align and pad. + for i, line in enumerate(lines): + new_width = inner_dimensions[0] + len(line) - visible_width(line) + if 'right' in align: + lines[i] = line.rjust(padding[0] + new_width, space) + (space * padding[1]) + elif 'center' in align: + lines[i] = (space * padding[0]) + line.center(new_width, space) + (space * padding[1]) + else: + lines[i] = (space * padding[0]) + line.ljust(new_width + padding[1], space) + + return lines + + +def max_dimensions(table_data, padding_left=0, padding_right=0, padding_top=0, padding_bottom=0): + """Get maximum widths of each column and maximum height of each row. + + :param iter table_data: List of list of strings (unmodified table data). + :param int padding_left: Number of space chars on left side of cell. + :param int padding_right: Number of space chars on right side of cell. + :param int padding_top: Number of empty lines on top side of cell. + :param int padding_bottom: Number of empty lines on bottom side of cell. + + :return: 4-item tuple of n-item lists. Inner column widths and row heights, outer column widths and row heights. + :rtype: tuple + """ + inner_widths = [0] * (max(len(r) for r in table_data) if table_data else 0) + inner_heights = [0] * len(table_data) + + # Find max width and heights. + for j, row in enumerate(table_data): + for i, cell in enumerate(row): + if not hasattr(cell, 'count') or not hasattr(cell, 'splitlines'): + cell = str(cell) + if not cell: + continue + inner_heights[j] = max(inner_heights[j], cell.count('\n') + 1) + inner_widths[i] = max(inner_widths[i], *[visible_width(l) for l in cell.splitlines()]) + + # Calculate with padding. + outer_widths = [padding_left + i + padding_right for i in inner_widths] + outer_heights = [padding_top + i + padding_bottom for i in inner_heights] + + return inner_widths, inner_heights, outer_widths, outer_heights + + +def column_max_width(inner_widths, column_number, outer_border, inner_border, padding): + """Determine the maximum width of a column based on the current terminal width. + + :param iter inner_widths: List of widths (no padding) for each column. + :param int column_number: The column number to query. + :param int outer_border: Sum of left and right outer border visible widths. + :param int inner_border: Visible width of the inner border character. + :param int padding: Total padding per cell (left + right padding). + + :return: The maximum width the column can be without causing line wrapping. + """ + column_count = len(inner_widths) + terminal_width = terminal_size()[0] + + # Count how much space padding, outer, and inner borders take up. + non_data_space = outer_border + non_data_space += inner_border * (column_count - 1) + non_data_space += column_count * padding + + # Exclude selected column's width. + data_space = sum(inner_widths) - inner_widths[column_number] + + return terminal_width - data_space - non_data_space + + +def table_width(outer_widths, outer_border, inner_border): + """Determine the width of the entire table including borders and padding. + + :param iter outer_widths: List of widths (with padding) for each column. + :param int outer_border: Sum of left and right outer border visible widths. + :param int inner_border: Visible width of the inner border character. + + :return: The width of the table. + :rtype: int + """ + column_count = len(outer_widths) + + # Count how much space outer and inner borders take up. + non_data_space = outer_border + if column_count: + non_data_space += inner_border * (column_count - 1) + + # Space of all columns and their padding. + data_space = sum(outer_widths) + return data_space + non_data_space -- cgit v1.2.3