Skip to content

Commit cd5b98a

Browse files
committed
Changed how SimpleTable creates divider when divider_char is wide. It no longer stretches the width of the table.
1 parent d154392 commit cd5b98a

File tree

3 files changed

+102
-60
lines changed

3 files changed

+102
-60
lines changed

cmd2/argparse_completer.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -484,9 +484,8 @@ def _format_completions(self, action, completions: List[Union[str, CompletionIte
484484

485485
# Create a table that's over half the width of the terminal.
486486
# This will force readline to place each entry on its own line.
487-
divider_char = None
488487
min_width = int(shutil.get_terminal_size().columns * 0.6)
489-
base_width = SimpleTable.base_width(2, divider_char=divider_char)
488+
base_width = SimpleTable.base_width(2)
490489
initial_width = base_width + token_width + desc_width
491490

492491
if initial_width < min_width:
@@ -496,7 +495,7 @@ def _format_completions(self, action, completions: List[Union[str, CompletionIte
496495
cols.append(Column(destination.upper(), width=token_width))
497496
cols.append(Column(desc_header, width=desc_width))
498497

499-
hint_table = SimpleTable(cols, divider_char=divider_char)
498+
hint_table = SimpleTable(cols)
500499
self._cmd2_app.completion_header = hint_table.generate_header()
501500
self._cmd2_app.display_matches = [hint_table.generate_data_row([item, item.description]) for item in completions]
502501

cmd2/table_creator.py

Lines changed: 43 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
The general use case is to inherit from TableCreator to create a table class with custom formatting options.
66
There are already implemented and ready-to-use examples of this below TableCreator's code.
77
"""
8+
import copy
89
import functools
910
import io
1011
from collections import deque
@@ -103,7 +104,7 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None:
103104
:param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab,
104105
then it will be converted to one space.
105106
"""
106-
self.cols = cols
107+
self.cols = copy.copy(cols)
107108
self.tab_width = tab_width
108109

109110
@staticmethod
@@ -481,8 +482,8 @@ class SimpleTable(TableCreator):
481482
Implementation of TableCreator which generates a borderless table with an optional divider row after the header.
482483
This class can be used to create the whole table at once or one row at a time.
483484
"""
484-
# Num chars between cells
485-
INTER_CELL_CHARS = 2
485+
# Spaces between cells
486+
INTER_CELL = 2 * SPACE
486487

487488
def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, divider_char: Optional[str] = '-') -> None:
488489
"""
@@ -491,24 +492,29 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, divider_char:
491492
:param cols: column definitions for this table
492493
:param tab_width: all tabs will be replaced with this many spaces. If a row's fill_char is a tab,
493494
then it will be converted to one space.
494-
:param divider_char: optional character used to build the header divider row. If provided, its value must meet the
495-
same requirements as fill_char in TableCreator.generate_row() or exceptions will be raised.
496-
Set this to None if you don't want a divider row. (Defaults to dash)
495+
:param divider_char: optional character used to build the header divider row. Set this to None if you don't
496+
want a divider row. Defaults to dash. (Cannot be a line breaking character)
497+
:raises: TypeError if fill_char is more than one character (not including ANSI style sequences)
498+
:raises: ValueError if text or fill_char contains an unprintable character
497499
"""
500+
if divider_char is not None:
501+
if len(ansi.strip_style(divider_char)) != 1:
502+
raise TypeError("Divider character must be exactly one character long")
503+
504+
divider_char_width = ansi.style_aware_wcswidth(divider_char)
505+
if divider_char_width == -1:
506+
raise (ValueError("Divider character is an unprintable character"))
507+
498508
super().__init__(cols, tab_width=tab_width)
499509
self.divider_char = divider_char
500-
self.empty_data = [EMPTY for _ in self.cols]
501510

502511
@classmethod
503-
def base_width(cls, num_cols: int, *, divider_char: Optional[str] = '-') -> int:
512+
def base_width(cls, num_cols: int) -> int:
504513
"""
505-
Utility method to calculate the width required for a table before data is added to it.
506-
This is useful to know how much room is left for data with creating a table of a specific width.
514+
Utility method to calculate the display width required for a table before data is added to it.
515+
This is useful when determining how wide to make your columns to have a table be a specific width.
507516
508517
:param num_cols: how many columns the table will have
509-
:param divider_char: optional character used to build the header divider row. If provided, its value must meet the
510-
same requirements as fill_char in TableCreator.generate_row() or exceptions will be raised.
511-
Set this to None if you don't want a divider row. (Defaults to dash)
512518
:return: base width
513519
:raises: ValueError if num_cols is less than 1
514520
"""
@@ -518,29 +524,35 @@ def base_width(cls, num_cols: int, *, divider_char: Optional[str] = '-') -> int:
518524
data_str = SPACE
519525
data_width = ansi.style_aware_wcswidth(data_str) * num_cols
520526

521-
tbl = cls([Column(data_str)] * num_cols, divider_char=divider_char)
527+
tbl = cls([Column(data_str)] * num_cols)
522528
data_row = tbl.generate_data_row([data_str] * num_cols)
523529

524530
return ansi.style_aware_wcswidth(data_row) - data_width
525531

532+
def total_width(self) -> int:
533+
"""Calculate the total display width of this table"""
534+
base_width = self.base_width(len(self.cols))
535+
data_width = sum(col.width for col in self.cols)
536+
return base_width + data_width
537+
526538
def generate_header(self) -> str:
527539
"""Generate table header with an optional divider row"""
528540
header_buf = io.StringIO()
529541

530542
# Create the header labels
531-
if self.divider_char is None:
532-
inter_cell = SimpleTable.INTER_CELL_CHARS * SPACE
533-
else:
534-
inter_cell = SPACE * ansi.style_aware_wcswidth(SimpleTable.INTER_CELL_CHARS * self.divider_char)
535-
header = self.generate_row(inter_cell=inter_cell)
543+
header = self.generate_row(inter_cell=self.INTER_CELL)
536544
header_buf.write(header)
537545

538-
# Create the divider. Use empty strings for the row_data.
546+
# Create the divider if necessary
539547
if self.divider_char is not None:
540-
divider = self.generate_row(row_data=self.empty_data, fill_char=self.divider_char,
541-
inter_cell=(SimpleTable.INTER_CELL_CHARS * self.divider_char))
548+
total_width = self.total_width()
549+
divider_char_width = ansi.style_aware_wcswidth(self.divider_char)
550+
551+
# Add padding if divider char does not divide evenly into table width
552+
divider = (self.divider_char * (total_width // divider_char_width)) + (SPACE * (total_width % divider_char_width))
542553
header_buf.write('\n')
543554
header_buf.write(divider)
555+
544556
return header_buf.getvalue()
545557

546558
def generate_data_row(self, row_data: Sequence[Any]) -> str:
@@ -550,11 +562,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str:
550562
:param row_data: data with an entry for each column in the row
551563
:return: data row string
552564
"""
553-
if self.divider_char is None:
554-
inter_cell = 2 * SPACE
555-
else:
556-
inter_cell = SPACE * ansi.style_aware_wcswidth(2 * self.divider_char)
557-
return self.generate_row(row_data=row_data, inter_cell=inter_cell)
565+
return self.generate_row(row_data=row_data, inter_cell=self.INTER_CELL)
558566

559567
def generate_table(self, table_data: Sequence[Sequence[Any]], *,
560568
include_header: bool = True, row_spacing: int = 1) -> str:
@@ -620,8 +628,8 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4,
620628
@classmethod
621629
def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int = 1) -> int:
622630
"""
623-
Utility method to calculate the width required for a table before data is added to it.
624-
This is useful to know how much room is left for data with creating a table of a specific width.
631+
Utility method to calculate the display width required for a table before data is added to it.
632+
This is useful when determining how wide to make your columns to have a table be a specific width.
625633
626634
:param num_cols: how many columns the table will have
627635
:param column_borders: if True, borders between columns will be included in the calculation (Defaults to True)
@@ -640,6 +648,12 @@ def base_width(cls, num_cols: int, *, column_borders: bool = True, padding: int
640648

641649
return ansi.style_aware_wcswidth(data_row) - data_width
642650

651+
def total_width(self) -> int:
652+
"""Calculate the total display width of this table"""
653+
base_width = self.base_width(len(self.cols), column_borders=self.column_borders, padding=self.padding)
654+
data_width = sum(col.width for col in self.cols)
655+
return base_width + data_width
656+
643657
def generate_table_top_border(self):
644658
"""Generate a border which appears at the top of the header and data section"""
645659
pre_line = '╔' + self.padding * '═'

tests/test_table_creator.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -268,16 +268,6 @@ def test_simple_table_creation():
268268
'\n'
269269
'Col 1 Row 2 Col 2 Row 2 ')
270270

271-
# Wide custom divider
272-
st = SimpleTable([column_1, column_2], divider_char='深')
273-
table = st.generate_table(row_data)
274-
275-
assert table == ('Col 1 Col 2 \n'
276-
'深深深深深深深深深深深深深深深深深深\n'
277-
'Col 1 Row 1 Col 2 Row 1 \n'
278-
'\n'
279-
'Col 1 Row 2 Col 2 Row 2 ')
280-
281271
# No divider
282272
st = SimpleTable([column_1, column_2], divider_char=None)
283273
table = st.generate_table(row_data)
@@ -303,36 +293,64 @@ def test_simple_table_creation():
303293
'\n'
304294
'Col 1 Row 2 Col 2 Row 2 ')
305295

296+
# Wide custom divider (divider needs no padding)
297+
st = SimpleTable([column_1, column_2], divider_char='深')
298+
table = st.generate_table(row_data)
299+
300+
assert table == ('Col 1 Col 2 \n'
301+
'深深深深深深深深深深深深深深深深深\n'
302+
'Col 1 Row 1 Col 2 Row 1 \n'
303+
'\n'
304+
'Col 1 Row 2 Col 2 Row 2 ')
305+
306+
# Wide custom divider (divider needs padding)
307+
column_2 = Column("Col 2", width=17)
308+
st = SimpleTable([column_1, column_2], divider_char='深')
309+
table = st.generate_table(row_data)
310+
311+
assert table == ('Col 1 Col 2 \n'
312+
'深深深深深深深深深深深深深深深深深 \n'
313+
'Col 1 Row 1 Col 2 Row 1 \n'
314+
'\n'
315+
'Col 1 Row 2 Col 2 Row 2 ')
316+
317+
# Invalid divider character
318+
with pytest.raises(TypeError) as excinfo:
319+
SimpleTable([column_1, column_2], divider_char='too long')
320+
assert "Divider character must be exactly one character long" in str(excinfo.value)
321+
322+
with pytest.raises(ValueError) as excinfo:
323+
SimpleTable([column_1, column_2], divider_char='\n')
324+
assert "Divider character is an unprintable character" in str(excinfo.value)
325+
306326
# Invalid row spacing
307327
st = SimpleTable([column_1, column_2])
308328
with pytest.raises(ValueError) as excinfo:
309329
st.generate_table(row_data, row_spacing=-1)
310330
assert "Row spacing cannot be less than 0" in str(excinfo.value)
311331

312332

313-
def test_simple_table_base_width():
314-
# Default divider char
315-
assert SimpleTable.base_width(1) == 0
316-
assert SimpleTable.base_width(2) == 2
317-
assert SimpleTable.base_width(3) == 4
318-
319-
# Standard divider char
320-
divider_char = '*'
321-
assert SimpleTable.base_width(1, divider_char=divider_char) == 0
322-
assert SimpleTable.base_width(2, divider_char=divider_char) == 2
323-
assert SimpleTable.base_width(3, divider_char=divider_char) == 4
324-
325-
# Wide divider char
326-
divider_char = '深'
327-
assert SimpleTable.base_width(1, divider_char=divider_char) == 0
328-
assert SimpleTable.base_width(2, divider_char=divider_char) == 4
329-
assert SimpleTable.base_width(3, divider_char=divider_char) == 8
333+
def test_simple_table_width():
334+
# Base width
335+
for num_cols in range(1, 10):
336+
assert SimpleTable.base_width(num_cols) == (num_cols - 1) * 2
330337

331338
# Invalid num_cols value
332339
with pytest.raises(ValueError) as excinfo:
333340
SimpleTable.base_width(0)
334341
assert "Column count cannot be less than 1" in str(excinfo.value)
335342

343+
# Total width
344+
column_1 = Column("Col 1", width=16)
345+
column_2 = Column("Col 2", width=16)
346+
347+
row_data = list()
348+
row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
349+
row_data.append(["Col 1 Row 2", "Col 2 Row 2"])
350+
351+
st = SimpleTable([column_1, column_2])
352+
assert st.total_width() == 34
353+
336354

337355
def test_bordered_table_creation():
338356
column_1 = Column("Col 1", width=15)
@@ -390,7 +408,7 @@ def test_bordered_table_creation():
390408
assert "Padding cannot be less than 0" in str(excinfo.value)
391409

392410

393-
def test_bordered_table_base_width():
411+
def test_bordered_table_width():
394412
# Default behavior (column_borders=True, padding=1)
395413
assert BorderedTable.base_width(1) == 4
396414
assert BorderedTable.base_width(2) == 7
@@ -416,6 +434,17 @@ def test_bordered_table_base_width():
416434
BorderedTable.base_width(0)
417435
assert "Column count cannot be less than 1" in str(excinfo.value)
418436

437+
# Total width
438+
column_1 = Column("Col 1", width=15)
439+
column_2 = Column("Col 2", width=15)
440+
441+
row_data = list()
442+
row_data.append(["Col 1 Row 1", "Col 2 Row 1"])
443+
row_data.append(["Col 1 Row 2", "Col 2 Row 2"])
444+
445+
bt = BorderedTable([column_1, column_2])
446+
assert bt.total_width() == 37
447+
419448

420449
def test_alternating_table_creation():
421450
column_1 = Column("Col 1", width=15)

0 commit comments

Comments
 (0)