Skip to content

Commit 2055dfc

Browse files
authored
Merge pull request #1049 from python-cmd2/header_tabs
Fixed width calculation when table column headers had tabs
2 parents b329218 + f8b2407 commit 2055dfc

File tree

4 files changed

+63
-30
lines changed

4 files changed

+63
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Bug Fixes
33
* Fixed bug where setting `always_show_hint=True` did not show a hint when completing `Settables`
44
* Fixed bug in editor detection logic on Linux systems that do not have `which`
5+
* Fixed bug in table creator where column headers with tabs would result in an incorrect width calculation
6+
* Fixed `FileNotFoundError` which occurred when running `history --clear` and no history file existed.
57
* Enhancements
68
* Added `silent_startup_script` option to `cmd2.Cmd.__init__()`. If `True`, then the startup script's
79
output will be suppressed. Anything written to stderr will still display.

cmd2/cmd2.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3989,7 +3989,10 @@ def do_history(self, args: argparse.Namespace) -> Optional[bool]:
39893989
self.history.clear()
39903990

39913991
if self.persistent_history_file:
3992-
os.remove(self.persistent_history_file)
3992+
try:
3993+
os.remove(self.persistent_history_file)
3994+
except FileNotFoundError:
3995+
pass
39933996

39943997
if rl_type != RlType.NONE:
39953998
readline.clear_history()

cmd2/table_creator.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def __init__(self, header: str, *, width: Optional[int] = None,
8383
:param header: label for column header
8484
:param width: display width of column. This does not account for any borders or padding which
8585
may be added (e.g pre_line, inter_cell, and post_line). Header and data text wrap within
86-
this width using word-based wrapping (defaults to width of header or 1 if header is blank)
86+
this width using word-based wrapping (defaults to actual width of header or 1 if header is blank)
8787
:param header_horiz_align: horizontal alignment of header cells (defaults to left)
8888
:param header_vert_align: vertical alignment of header cells (defaults to bottom)
8989
:param data_horiz_align: horizontal alignment of data cells (defaults to left)
@@ -95,12 +95,7 @@ def __init__(self, header: str, *, width: Optional[int] = None,
9595
"""
9696
self.header = header
9797

98-
if width is None:
99-
# Use the width of the widest line in the header or 1 if the header has no width
100-
line_widths = [ansi.style_aware_wcswidth(line) for line in self.header.splitlines()]
101-
line_widths.append(1)
102-
self.width = max(line_widths)
103-
elif width < 1:
98+
if width is not None and width < 1:
10499
raise ValueError("Column width cannot be less than 1")
105100
else:
106101
self.width = width
@@ -141,8 +136,20 @@ def __init__(self, cols: Sequence[Column], *, tab_width: int = 4) -> None:
141136
self.cols = copy.copy(cols)
142137
self.tab_width = tab_width
143138

139+
for col in self.cols:
140+
# Replace tabs before calculating width of header strings
141+
col.header = col.header.replace('\t', SPACE * self.tab_width)
142+
143+
# For headers with the width not yet set, use the width of the
144+
# widest line in the header or 1 if the header has no width
145+
if col.width is None:
146+
line_widths = [ansi.style_aware_wcswidth(line) for line in col.header.splitlines()]
147+
line_widths.append(1)
148+
col.width = max(line_widths)
149+
144150
@staticmethod
145-
def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float], is_last_word: bool) -> Tuple[str, int, int]:
151+
def _wrap_long_word(word: str, max_width: int, max_lines: Union[int, float],
152+
is_last_word: bool) -> Tuple[str, int, int]:
146153
"""
147154
Used by _wrap_text() to wrap a long word over multiple lines
148155
@@ -351,14 +358,16 @@ def add_word(word_to_add: str, is_last_word: bool):
351358

352359
# Stop line loop if we've written to max_lines
353360
if total_lines == max_lines:
354-
# If this isn't the last data line and there is space left on the final wrapped line, then add an ellipsis
361+
# If this isn't the last data line and there is space
362+
# left on the final wrapped line, then add an ellipsis
355363
if data_line_index < len(data_str_lines) - 1 and cur_line_width < max_width:
356364
wrapped_buf.write(constants.HORIZONTAL_ELLIPSIS)
357365
break
358366

359367
return wrapped_buf.getvalue()
360368

361-
def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fill_char: str) -> Tuple[Deque[str], int]:
369+
def _generate_cell_lines(self, cell_data: Any, is_header: bool,
370+
col: Column, fill_char: str) -> Tuple[Deque[str], int]:
362371
"""
363372
Generate the lines of a table cell
364373
@@ -398,14 +407,14 @@ def generate_row(self, *, row_data: Optional[Sequence[Any]] = None, fill_char: s
398407
399408
:param row_data: If this is None then a header row is generated. Otherwise data should have an entry for each
400409
column in the row. (Defaults to None)
401-
:param fill_char: character that fills remaining space in a cell. Defaults to space. If this is a tab, then it will
402-
be converted to one space. (Cannot be a line breaking character)
410+
:param fill_char: character that fills remaining space in a cell. Defaults to space. If this is a tab,
411+
then it will be converted to one space. (Cannot be a line breaking character)
403412
:param pre_line: string to print before each line of a row. This can be used for a left row border and
404413
padding before the first cell's text. (Defaults to blank)
405414
:param inter_cell: string to print where two cells meet. This can be used for a border between cells and padding
406415
between it and the 2 cells' text. (Defaults to 2 spaces)
407-
:param post_line: string to print after each line of a row. This can be used for padding after the last cell's text
408-
and a right row border. (Defaults to blank)
416+
:param post_line: string to print after each line of a row. This can be used for padding after
417+
the last cell's text and a right row border. (Defaults to blank)
409418
:return: row string
410419
:raises: ValueError if data isn't the same length as self.cols
411420
:raises: TypeError if fill_char is more than one character (not including ANSI style sequences)
@@ -608,7 +617,8 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *,
608617
:param table_data: Data with an entry for each data row of the table. Each entry should have data for
609618
each column in the row.
610619
:param include_header: If True, then a header will be included at top of table. (Defaults to True)
611-
:param row_spacing: A number 0 or greater specifying how many blank lines to place between each row (Defaults to 1)
620+
:param row_spacing: A number 0 or greater specifying how many blank lines to place between
621+
each row (Defaults to 1)
612622
:raises: ValueError if row_spacing is less than 0
613623
"""
614624
if row_spacing < 0:
@@ -820,8 +830,8 @@ def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header:
820830

821831
class AlternatingTable(BorderedTable):
822832
"""
823-
Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border lines.
824-
This class can be used to create the whole table at once or one row at a time.
833+
Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border
834+
lines. This class can be used to create the whole table at once or one row at a time.
825835
"""
826836
def __init__(self, cols: Sequence[Column], *, tab_width: int = 4, column_borders: bool = True, padding: int = 1,
827837
bg_odd: Optional[ansi.bg] = None, bg_even: Optional[ansi.bg] = ansi.bg.bright_black) -> None:

tests/test_table_creator.py

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,6 @@
2020

2121

2222
def test_column_creation():
23-
# No width specified, blank label
24-
c = Column("")
25-
assert c.width == 1
26-
27-
# No width specified, label isn't blank but has no width
28-
c = Column(ansi.style('', fg=ansi.fg.green))
29-
assert c.width == 1
30-
31-
# No width specified, label has width
32-
c = Column("short\nreally long")
33-
assert c.width == ansi.style_aware_wcswidth("really long")
34-
3523
# Width less than 1
3624
with pytest.raises(ValueError) as excinfo:
3725
Column("Column 1", width=0)
@@ -46,6 +34,36 @@ def test_column_creation():
4634
Column("Column 1", max_data_lines=0)
4735
assert "Max data lines cannot be less than 1" in str(excinfo.value)
4836

37+
# No width specified, blank label
38+
c = Column("")
39+
assert c.width is None
40+
tc = TableCreator([c])
41+
assert tc.cols[0].width == 1
42+
43+
# No width specified, label isn't blank but has no width
44+
c = Column(ansi.style('', fg=ansi.fg.green))
45+
assert c.width is None
46+
tc = TableCreator([c])
47+
assert tc.cols[0].width == 1
48+
49+
# No width specified, label has width
50+
c = Column("a line")
51+
assert c.width is None
52+
tc = TableCreator([c])
53+
assert tc.cols[0].width == ansi.style_aware_wcswidth("a line")
54+
55+
# No width specified, label has width and multiple lines
56+
c = Column("short\nreally long")
57+
assert c.width is None
58+
tc = TableCreator([c])
59+
assert tc.cols[0].width == ansi.style_aware_wcswidth("really long")
60+
61+
# No width specified, label has tabs
62+
c = Column("line\twith\ttabs")
63+
assert c.width is None
64+
tc = TableCreator([c])
65+
assert tc.cols[0].width == ansi.style_aware_wcswidth("line with tabs")
66+
4967

5068
def test_column_alignment():
5169
column_1 = Column("Col 1", width=10,

0 commit comments

Comments
 (0)