summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2020-06-02 15:05:27 -0400
committerGitHub <noreply@github.com>2020-06-02 15:05:27 -0400
commitf626f8674c3bdf40c3c96f7009f01cf2d5315a7a (patch)
tree2b3285371fa2edebb10045f32bdf0c1e9b1b8202
parent8d9405a1fcc2169aa039172a8e2891b839a59e6c (diff)
parent52e70d09ac2c365b77ac00a8689913251f713f67 (diff)
downloadcmd2-git-f626f8674c3bdf40c3c96f7009f01cf2d5315a7a.tar.gz
Merge pull request #941 from python-cmd2/hint_bug
Hint bug
-rw-r--r--CHANGELOG.md2
-rw-r--r--cmd2/argparse_completer.py41
-rw-r--r--cmd2/cmd2.py4
-rw-r--r--cmd2/table_creator.py128
-rwxr-xr-xexamples/table_creation.py2
-rw-r--r--tests/test_table_creator.py180
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: