diff options
author | Todd Leonhardt <todd.leonhardt@gmail.com> | 2020-06-02 15:05:27 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-02 15:05:27 -0400 |
commit | f626f8674c3bdf40c3c96f7009f01cf2d5315a7a (patch) | |
tree | 2b3285371fa2edebb10045f32bdf0c1e9b1b8202 | |
parent | 8d9405a1fcc2169aa039172a8e2891b839a59e6c (diff) | |
parent | 52e70d09ac2c365b77ac00a8689913251f713f67 (diff) | |
download | cmd2-git-f626f8674c3bdf40c3c96f7009f01cf2d5315a7a.tar.gz |
Merge pull request #941 from python-cmd2/hint_bug
Hint bug
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 41 | ||||
-rw-r--r-- | cmd2/cmd2.py | 4 | ||||
-rw-r--r-- | cmd2/table_creator.py | 128 | ||||
-rwxr-xr-x | examples/table_creation.py | 2 | ||||
-rw-r--r-- | tests/test_table_creator.py | 180 |
6 files changed, 266 insertions, 91 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 31daa079..44331511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## 1.1.0 (TBD, 2020) * Bug Fixes * Fixed issue where subcommand usage text could contain a subcommand alias instead of the actual name + * Fixed bug in `ArgparseCompleter` where `fill_width` could become negative if `token_width` was large + relative to the terminal width. * Enhancements * Made `ipy` consistent with `py` in the following ways * `ipy` returns whether any of the commands run in it returned True to stop command loop diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 61f173cc..6acb5abc 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -23,6 +23,7 @@ from .argparse_custom import ( CompletionItem, generate_range_error, ) +from .table_creator import Column, SimpleTable from .utils import CompletionError, basic_complete # If no descriptive header is supplied, then this will be used instead @@ -467,29 +468,35 @@ class ArgparseCompleter: # If a metavar was defined, use that instead of the dest field destination = action.metavar if action.metavar else action.dest + + desc_header = getattr(action, ATTR_DESCRIPTIVE_COMPLETION_HEADER, None) + if desc_header is None: + desc_header = DEFAULT_DESCRIPTIVE_HEADER + + # Calculate needed widths for the token and description columns of the table token_width = ansi.style_aware_wcswidth(destination) - completions_with_desc = [] + desc_width = ansi.style_aware_wcswidth(desc_header) for item in completions: - item_width = ansi.style_aware_wcswidth(item) - if item_width > token_width: - token_width = item_width + token_width = max(ansi.style_aware_wcswidth(item), token_width) + desc_width = max(ansi.style_aware_wcswidth(item.description), desc_width) - term_size = shutil.get_terminal_size() - fill_width = int(term_size.columns * .6) - (token_width + 2) - for item in completions: - entry = '{: <{token_width}}{: <{fill_width}}'.format(item, item.description, - token_width=token_width + 2, - fill_width=fill_width) - completions_with_desc.append(entry) + # Create a table that's over half the width of the terminal. + # This will force readline to place each entry on its own line. + min_width = int(shutil.get_terminal_size().columns * 0.6) + base_width = SimpleTable.base_width(2) + initial_width = base_width + token_width + desc_width - desc_header = getattr(action, ATTR_DESCRIPTIVE_COMPLETION_HEADER, None) - if desc_header is None: - desc_header = DEFAULT_DESCRIPTIVE_HEADER - header = '\n{: <{token_width}}{}'.format(destination.upper(), desc_header, token_width=token_width + 2) + if initial_width < min_width: + desc_width += (min_width - initial_width) + + cols = list() + cols.append(Column(destination.upper(), width=token_width)) + cols.append(Column(desc_header, width=desc_width)) - self._cmd2_app.completion_header = header - self._cmd2_app.display_matches = completions_with_desc + hint_table = SimpleTable(cols, divider_char=None) + self._cmd2_app.completion_header = hint_table.generate_header() + self._cmd2_app.display_matches = [hint_table.generate_data_row([item, item.description]) for item in completions] return completions diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 73f7aa79..65047cc8 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1147,7 +1147,7 @@ class Cmd(cmd.Cmd): # Print the header if one exists if self.completion_header: - sys.stdout.write('\n' + self.completion_header) + sys.stdout.write('\n\n' + self.completion_header) # Call readline's display function # rl_display_match_list(strings_array, number of completion matches, longest match length) @@ -1176,7 +1176,7 @@ class Cmd(cmd.Cmd): # Print the header if one exists if self.completion_header: # noinspection PyUnresolvedReferences - readline.rl.mode.console.write('\n' + self.completion_header) + readline.rl.mode.console.write('\n\n' + self.completion_header) # Display matches using actual display function. This also redraws the prompt and line. orig_pyreadline_display(matches_to_display) diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 70b7c1eb..b35cade6 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -5,6 +5,7 @@ This API is built upon two core classes: Column and TableCreator The general use case is to inherit from TableCreator to create a table class with custom formatting options. There are already implemented and ready-to-use examples of this below TableCreator's code. """ +import copy import functools import io from collections import deque @@ -103,7 +104,7 @@ class TableCreator: :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab, then it will be converted to one space. """ - self.cols = cols + self.cols = copy.copy(cols) self.tab_width = tab_width @staticmethod @@ -465,8 +466,9 @@ class TableCreator: if cell_index == len(self.cols) - 1: row_buf.write(post_line) - # Add a newline if this is not the last row - row_buf.write('\n') + # Add a newline if this is not the last line + if line_index < total_lines - 1: + row_buf.write('\n') return row_buf.getvalue() @@ -480,6 +482,9 @@ class SimpleTable(TableCreator): Implementation of TableCreator which generates a borderless table with an optional divider row after the header. This class can be used to create the whole table at once or one row at a time. """ + # Spaces between cells + INTER_CELL = 2 * SPACE + def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, divider_char: Optional[str] = '-') -> None: """ SimpleTable initializer @@ -487,32 +492,68 @@ class SimpleTable(TableCreator): :param cols: column definitions for this table :param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab, then it will be converted to one space. - :param divider_char: optional character used to build the header divider row. If provided, its value must meet the - same requirements as fill_char in TableCreator.generate_row() or exceptions will be raised. - Set this to None if you don't want a divider row. (Defaults to dash) + :param divider_char: optional character used to build the header divider row. Set this to None if you don't + want a divider row. Defaults to dash. (Cannot be a line breaking character) + :raises: TypeError if fill_char is more than one character (not including ANSI style sequences) + :raises: ValueError if text or fill_char contains an unprintable character """ + if divider_char is not None: + if len(ansi.strip_style(divider_char)) != 1: + raise TypeError("Divider character must be exactly one character long") + + divider_char_width = ansi.style_aware_wcswidth(divider_char) + if divider_char_width == -1: + raise (ValueError("Divider character is an unprintable character")) + super().__init__(cols, tab_width=tab_width) self.divider_char = divider_char - self.empty_data = [EMPTY for _ in self.cols] - def generate_header(self) -> str: + @classmethod + def base_width(cls, num_cols: int) -> int: """ - Generate header with an optional divider row + Utility method to calculate the display width required for a table before data is added to it. + This is useful when determining how wide to make your columns to have a table be a specific width. + + :param num_cols: how many columns the table will have + :return: base width + :raises: ValueError if num_cols is less than 1 """ + if num_cols < 1: + raise ValueError("Column count cannot be less than 1") + + data_str = SPACE + data_width = ansi.style_aware_wcswidth(data_str) * num_cols + + tbl = cls([Column(data_str)] * num_cols) + data_row = tbl.generate_data_row([data_str] * num_cols) + + return ansi.style_aware_wcswidth(data_row) - data_width + + def total_width(self) -> int: + """Calculate the total display width of this table""" + base_width = self.base_width(len(self.cols)) + data_width = sum(col.width for col in self.cols) + return base_width + data_width + + def generate_header(self) -> str: + """Generate table header with an optional divider row""" header_buf = io.StringIO() # Create the header labels - if self.divider_char is None: - inter_cell = 2 * SPACE - else: - inter_cell = SPACE * ansi.style_aware_wcswidth(2 * self.divider_char) - header = self.generate_row(inter_cell=inter_cell) + header = self.generate_row(inter_cell=self.INTER_CELL) header_buf.write(header) - # Create the divider. Use empty strings for the row_data. + # Create the divider if necessary if self.divider_char is not None: - divider = self.generate_row(row_data=self.empty_data, fill_char=self.divider_char, - inter_cell=(2 * self.divider_char)) + total_width = self.total_width() + divider_char_width = ansi.style_aware_wcswidth(self.divider_char) + + # Make divider as wide as table and use padding if width of + # divider_char does not divide evenly into table width. + divider = self.divider_char * (total_width // divider_char_width) + divider += SPACE * (total_width % divider_char_width) + + header_buf.write('\n') header_buf.write(divider) return header_buf.getvalue() @@ -523,11 +564,7 @@ class SimpleTable(TableCreator): :param row_data: data with an entry for each column in the row :return: data row string """ - if self.divider_char is None: - inter_cell = 2 * SPACE - else: - inter_cell = SPACE * ansi.style_aware_wcswidth(2 * self.divider_char) - return self.generate_row(row_data=row_data, inter_cell=inter_cell) + return self.generate_row(row_data=row_data, inter_cell=self.INTER_CELL) def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True, row_spacing: int = 1) -> str: @@ -548,6 +585,8 @@ class SimpleTable(TableCreator): if include_header: header = self.generate_header() table_buf.write(header) + if len(table_data) > 0: + table_buf.write('\n') for index, row_data in enumerate(table_data): if index > 0 and row_spacing > 0: @@ -555,6 +594,8 @@ class SimpleTable(TableCreator): row = self.generate_data_row(row_data) table_buf.write(row) + if index < len(table_data) - 1: + table_buf.write('\n') return table_buf.getvalue() @@ -586,6 +627,35 @@ class BorderedTable(TableCreator): raise ValueError("Padding cannot be less than 0") self.padding = padding + @classmethod + def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int = 1) -> int: + """ + Utility method to calculate the display width required for a table before data is added to it. + This is useful when determining how wide to make your columns to have a table be a specific width. + + :param num_cols: how many columns the table will have + :param column_borders: if True, borders between columns will be included in the calculation (Defaults to True) + :param padding: number of spaces between text and left/right borders of cell + :return: base width + :raises: ValueError if num_cols is less than 1 + """ + if num_cols < 1: + raise ValueError("Column count cannot be less than 1") + + data_str = SPACE + data_width = ansi.style_aware_wcswidth(data_str) * num_cols + + tbl = cls([Column(data_str)] * num_cols, column_borders=column_borders, padding=padding) + data_row = tbl.generate_data_row([data_str] * num_cols) + + return ansi.style_aware_wcswidth(data_row) - data_width + + def total_width(self) -> int: + """Calculate the total display width of this table""" + base_width = self.base_width(len(self.cols), column_borders=self.column_borders, padding=self.padding) + data_width = sum(col.width for col in self.cols) + return base_width + data_width + def generate_table_top_border(self): """Generate a border which appears at the top of the header and data section""" pre_line = '╔' + self.padding * '═' @@ -643,10 +713,7 @@ class BorderedTable(TableCreator): inter_cell=inter_cell, post_line=post_line) def generate_header(self) -> str: - """ - Generate header - :return: header string - """ + """Generate table header""" pre_line = '║' + self.padding * SPACE inter_cell = self.padding * SPACE @@ -659,7 +726,9 @@ class BorderedTable(TableCreator): # Create the bordered header header_buf = io.StringIO() header_buf.write(self.generate_table_top_border()) + header_buf.write('\n') header_buf.write(self.generate_row(pre_line=pre_line, inter_cell=inter_cell, post_line=post_line)) + header_buf.write('\n') header_buf.write(self.generate_header_bottom_border()) return header_buf.getvalue() @@ -699,13 +768,17 @@ class BorderedTable(TableCreator): top_border = self.generate_table_top_border() table_buf.write(top_border) + table_buf.write('\n') + for index, row_data in enumerate(table_data): if index > 0: row_bottom_border = self.generate_row_bottom_border() table_buf.write(row_bottom_border) + table_buf.write('\n') row = self.generate_data_row(row_data) table_buf.write(row) + table_buf.write('\n') table_buf.write(self.generate_table_bottom_border()) return table_buf.getvalue() @@ -797,9 +870,12 @@ class AlternatingTable(BorderedTable): top_border = self.generate_table_top_border() table_buf.write(top_border) + table_buf.write('\n') + for row_data in table_data: row = self.generate_data_row(row_data) table_buf.write(row) + table_buf.write('\n') table_buf.write(self.generate_table_bottom_border()) return table_buf.getvalue() diff --git a/examples/table_creation.py b/examples/table_creation.py index 85bfc3f0..6325b200 100755 --- a/examples/table_creation.py +++ b/examples/table_creation.py @@ -53,7 +53,7 @@ data_list.append(["John Jones", def ansi_print(text): """Wraps style_aware_write so style can be stripped if needed""" - ansi.style_aware_write(sys.stdout, text + '\n') + ansi.style_aware_write(sys.stdout, text + '\n\n') def main(): diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py index ed53efa6..0d2edfb2 100644 --- a/tests/test_table_creator.py +++ b/tests/test_table_creator.py @@ -70,14 +70,14 @@ def test_column_alignment(): header = tc.generate_row() assert header == ('Col 1 Three \n' ' Col 2 line \n' - ' Col 3 header \n') + ' Col 3 header ') # Create a data row row_data = ["Val 1", "Val 2", "Val 3", "Three\nline\ndata"] row = tc.generate_row(row_data=row_data) assert row == ('Val 1 Three \n' ' Val 2 line \n' - ' Val 3 data \n') + ' Val 3 data ') def test_wrap_text(): @@ -93,13 +93,13 @@ def test_wrap_text(): 'that will \n' 'wrap \n' 'Not wrap \n' - ' 1 2 3 \n') + ' 1 2 3 ') # Test preserving a multiple space sequence across a line break row_data = ['First last one'] row = tc.generate_row(row_data=row_data) assert row == ('First \n' - ' last one \n') + ' last one ') def test_wrap_text_max_lines(): @@ -110,31 +110,31 @@ def test_wrap_text_max_lines(): row_data = ['First line last line'] row = tc.generate_row(row_data=row_data) assert row == ('First line\n' - 'last line \n') + 'last line ') # Test having to truncate the last word because it's too long for the final line row_data = ['First line last lineextratext'] row = tc.generate_row(row_data=row_data) assert row == ('First line\n' - 'last line…\n') + 'last line…') # Test having to truncate the last word because it fits the final line but there is more text not being included row_data = ['First line thistxtfit extra'] row = tc.generate_row(row_data=row_data) assert row == ('First line\n' - 'thistxtfi…\n') + 'thistxtfi…') # Test having to truncate the last word because it fits the final line but there are more lines not being included row_data = ['First line thistxtfit\nextra'] row = tc.generate_row(row_data=row_data) assert row == ('First line\n' - 'thistxtfi…\n') + 'thistxtfi…') # Test having space left on the final line and adding an ellipsis because there are more lines not being included row_data = ['First line last line\nextra line'] row = tc.generate_row(row_data=row_data) assert row == ('First line\n' - 'last line…\n') + 'last line…') def test_wrap_long_word(): @@ -148,7 +148,7 @@ def test_wrap_long_word(): # Test header row header = tc.generate_row() assert header == ('LongColumn \n' - 'Name Col 2 \n') + 'Name Col 2 ') # Test data row row_data = list() @@ -162,7 +162,7 @@ def test_wrap_long_word(): row = tc.generate_row(row_data=row_data) expected = (ansi.RESET_ALL + ansi.fg.green + "LongerThan" + ansi.RESET_ALL + " Word \n" + ansi.RESET_ALL + ansi.fg.green + "10" + ansi.fg.reset + ansi.RESET_ALL + ' ' + ansi.RESET_ALL + ' LongerThan\n' - ' 10 \n') + ' 10 ') assert row == expected @@ -192,7 +192,7 @@ def test_wrap_long_word_max_data_lines(): row = tc.generate_row(row_data=row_data) assert row == ('LongerThan LongerThan LongerThan A LongerT…\n' - '10FitsLast 10FitsLas… 10RunsOve… \n') + '10FitsLast 10FitsLas… 10RunsOve… ') def test_wrap_long_char_wider_than_max_width(): @@ -203,7 +203,7 @@ def test_wrap_long_char_wider_than_max_width(): column_1 = Column("Col 1", width=1) tc = TableCreator([column_1]) row = tc.generate_row(row_data=['深']) - assert row == '…\n' + assert row == '…' def test_generate_row_exceptions(): @@ -237,12 +237,12 @@ def test_tabs(): row = tc.generate_row(fill_char='\t', pre_line='\t', inter_cell='\t', post_line='\t') - assert row == ' Col 1 Col 2 \n' + assert row == ' Col 1 Col 2 ' -def test_simple_table(): - column_1 = Column("Col 1", width=15) - column_2 = Column("Col 2", width=15) +def test_simple_table_creation(): + column_1 = Column("Col 1", width=16) + column_2 = Column("Col 2", width=16) row_data = list() row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) @@ -252,46 +252,76 @@ def test_simple_table(): st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '--------------------------------\n' - 'Col 1 Row 1 Col 2 Row 1 \n' + assert table == ('Col 1 Col 2 \n' + '----------------------------------\n' + 'Col 1 Row 1 Col 2 Row 1 \n' '\n' - 'Col 1 Row 2 Col 2 Row 2 \n') + 'Col 1 Row 2 Col 2 Row 2 ') # Custom divider st = SimpleTable([column_1, column_2], divider_char='─') table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - '────────────────────────────────\n' - 'Col 1 Row 1 Col 2 Row 1 \n' + assert table == ('Col 1 Col 2 \n' + '──────────────────────────────────\n' + 'Col 1 Row 1 Col 2 Row 1 \n' '\n' - 'Col 1 Row 2 Col 2 Row 2 \n') + 'Col 1 Row 2 Col 2 Row 2 ') # No divider st = SimpleTable([column_1, column_2], divider_char=None) table = st.generate_table(row_data) - assert table == ('Col 1 Col 2 \n' - 'Col 1 Row 1 Col 2 Row 1 \n' + assert table == ('Col 1 Col 2 \n' + 'Col 1 Row 1 Col 2 Row 1 \n' '\n' - 'Col 1 Row 2 Col 2 Row 2 \n') + 'Col 1 Row 2 Col 2 Row 2 ') # No row spacing st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, row_spacing=0) - assert table == ('Col 1 Col 2 \n' - '--------------------------------\n' - 'Col 1 Row 1 Col 2 Row 1 \n' - 'Col 1 Row 2 Col 2 Row 2 \n') + assert table == ('Col 1 Col 2 \n' + '----------------------------------\n' + 'Col 1 Row 1 Col 2 Row 1 \n' + 'Col 1 Row 2 Col 2 Row 2 ') # No header st = SimpleTable([column_1, column_2]) table = st.generate_table(row_data, include_header=False) - assert table == ('Col 1 Row 1 Col 2 Row 1 \n' + assert table == ('Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ') + + # Wide custom divider (divider needs no padding) + st = SimpleTable([column_1, column_2], divider_char='深') + table = st.generate_table(row_data) + + assert table == ('Col 1 Col 2 \n' + '深深深深深深深深深深深深深深深深深\n' + 'Col 1 Row 1 Col 2 Row 1 \n' '\n' - 'Col 1 Row 2 Col 2 Row 2 \n') + 'Col 1 Row 2 Col 2 Row 2 ') + + # Wide custom divider (divider needs padding) + column_2 = Column("Col 2", width=17) + st = SimpleTable([column_1, column_2], divider_char='深') + table = st.generate_table(row_data) + + assert table == ('Col 1 Col 2 \n' + '深深深深深深深深深深深深深深深深深 \n' + 'Col 1 Row 1 Col 2 Row 1 \n' + '\n' + 'Col 1 Row 2 Col 2 Row 2 ') + + # Invalid divider character + with pytest.raises(TypeError) as excinfo: + SimpleTable([column_1, column_2], divider_char='too long') + assert "Divider character must be exactly one character long" in str(excinfo.value) + + with pytest.raises(ValueError) as excinfo: + SimpleTable([column_1, column_2], divider_char='\n') + assert "Divider character is an unprintable character" in str(excinfo.value) # Invalid row spacing st = SimpleTable([column_1, column_2]) @@ -300,7 +330,29 @@ def test_simple_table(): assert "Row spacing cannot be less than 0" in str(excinfo.value) -def test_bordered_table(): +def test_simple_table_width(): + # Base width + for num_cols in range(1, 10): + assert SimpleTable.base_width(num_cols) == (num_cols - 1) * 2 + + # Invalid num_cols value + with pytest.raises(ValueError) as excinfo: + SimpleTable.base_width(0) + assert "Column count cannot be less than 1" in str(excinfo.value) + + # Total width + column_1 = Column("Col 1", width=16) + column_2 = Column("Col 2", width=16) + + row_data = list() + row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) + row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) + + st = SimpleTable([column_1, column_2]) + assert st.total_width() == 34 + + +def test_bordered_table_creation(): column_1 = Column("Col 1", width=15) column_2 = Column("Col 2", width=15) @@ -317,7 +369,7 @@ def test_bordered_table(): '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' '╟─────────────────┼─────────────────╢\n' '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' - '╚═════════════════╧═════════════════╝\n') + '╚═════════════════╧═════════════════╝') # No column borders bt = BorderedTable([column_1, column_2], column_borders=False) @@ -328,7 +380,7 @@ def test_bordered_table(): '║ Col 1 Row 1 Col 2 Row 1 ║\n' '╟──────────────────────────────────╢\n' '║ Col 1 Row 2 Col 2 Row 2 ║\n' - '╚══════════════════════════════════╝\n') + '╚══════════════════════════════════╝') # No header bt = BorderedTable([column_1, column_2]) @@ -337,7 +389,7 @@ def test_bordered_table(): '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' '╟─────────────────┼─────────────────╢\n' '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' - '╚═════════════════╧═════════════════╝\n') + '╚═════════════════╧═════════════════╝') # Non-default padding bt = BorderedTable([column_1, column_2], padding=2) @@ -348,7 +400,7 @@ def test_bordered_table(): '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' '╟───────────────────┼───────────────────╢\n' '║ Col 1 Row 2 │ Col 2 Row 2 ║\n' - '╚═══════════════════╧═══════════════════╝\n') + '╚═══════════════════╧═══════════════════╝') # Invalid padding with pytest.raises(ValueError) as excinfo: @@ -356,7 +408,45 @@ def test_bordered_table(): assert "Padding cannot be less than 0" in str(excinfo.value) -def test_alternating_table(): +def test_bordered_table_width(): + # Default behavior (column_borders=True, padding=1) + assert BorderedTable.base_width(1) == 4 + assert BorderedTable.base_width(2) == 7 + assert BorderedTable.base_width(3) == 10 + + # No column borders + assert BorderedTable.base_width(1, column_borders=False) == 4 + assert BorderedTable.base_width(2, column_borders=False) == 6 + assert BorderedTable.base_width(3, column_borders=False) == 8 + + # No padding + assert BorderedTable.base_width(1, padding=0) == 2 + assert BorderedTable.base_width(2, padding=0) == 3 + assert BorderedTable.base_width(3, padding=0) == 4 + + # Extra padding + assert BorderedTable.base_width(1, padding=3) == 8 + assert BorderedTable.base_width(2, padding=3) == 15 + assert BorderedTable.base_width(3, padding=3) == 22 + + # Invalid num_cols value + with pytest.raises(ValueError) as excinfo: + BorderedTable.base_width(0) + assert "Column count cannot be less than 1" in str(excinfo.value) + + # Total width + column_1 = Column("Col 1", width=15) + column_2 = Column("Col 2", width=15) + + row_data = list() + row_data.append(["Col 1 Row 1", "Col 2 Row 1"]) + row_data.append(["Col 1 Row 2", "Col 2 Row 2"]) + + bt = BorderedTable([column_1, column_2]) + assert bt.total_width() == 37 + + +def test_alternating_table_creation(): column_1 = Column("Col 1", width=15) column_2 = Column("Col 2", width=15) @@ -372,7 +462,7 @@ def test_alternating_table(): '╠═════════════════╪═════════════════╣\n' '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚═════════════════╧═════════════════╝\n') + '╚═════════════════╧═════════════════╝') # Other bg colors at = AlternatingTable([column_1, column_2], bg_odd=ansi.bg.bright_blue, bg_even=ansi.bg.green) @@ -382,7 +472,7 @@ def test_alternating_table(): '╠═════════════════╪═════════════════╣\n' '\x1b[104m║ \x1b[49m\x1b[0m\x1b[104mCol 1 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m │ \x1b[49m\x1b[0m\x1b[104mCol 2 Row 1\x1b[49m\x1b[0m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[104m \x1b[49m\x1b[0m\x1b[104m ║\x1b[49m\n' '\x1b[42m║ \x1b[49m\x1b[0m\x1b[42mCol 1 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m │ \x1b[49m\x1b[0m\x1b[42mCol 2 Row 2\x1b[49m\x1b[0m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[42m \x1b[49m\x1b[0m\x1b[42m ║\x1b[49m\n' - '╚═════════════════╧═════════════════╝\n') + '╚═════════════════╧═════════════════╝') # No column borders at = AlternatingTable([column_1, column_2], column_borders=False) @@ -392,7 +482,7 @@ def test_alternating_table(): '╠══════════════════════════════════╣\n' '║ Col 1 Row 1 Col 2 Row 1 ║\n' '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚══════════════════════════════════╝\n') + '╚══════════════════════════════════╝') # No header at = AlternatingTable([column_1, column_2]) @@ -400,7 +490,7 @@ def test_alternating_table(): assert table == ('╔═════════════════╤═════════════════╗\n' '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚═════════════════╧═════════════════╝\n') + '╚═════════════════╧═════════════════╝') # Non-default padding at = AlternatingTable([column_1, column_2], padding=2) @@ -410,7 +500,7 @@ def test_alternating_table(): '╠═══════════════════╪═══════════════════╣\n' '║ Col 1 Row 1 │ Col 2 Row 1 ║\n' '\x1b[100m║ \x1b[49m\x1b[0m\x1b[100mCol 1 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m │ \x1b[49m\x1b[0m\x1b[100mCol 2 Row 2\x1b[49m\x1b[0m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[100m \x1b[49m\x1b[0m\x1b[100m ║\x1b[49m\n' - '╚═══════════════════╧═══════════════════╝\n') + '╚═══════════════════╧═══════════════════╝') # Invalid padding with pytest.raises(ValueError) as excinfo: |