Skip to content

Commit f2c4fd1

Browse files
committed
ArgparseCompleter now sorts CompletionItems created with numerical values as numbers.
Completion hint tables now right-align the left column if the hints have a numerical type. Fixed issue introduced in 2.3.0 with AlternatingTable, BorderedTable, and SimpleTable that caused header alignment settings to be overridden by data alignment settings.
1 parent 75f8008 commit f2c4fd1

6 files changed

+122
-77
lines changed

CHANGELOG.md

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
## 2.3.1 (TBD, 2021)
2+
* Bug Fixes
3+
* Fixed issue introduced in 2.3.0 with `AlternatingTable`, `BorderedTable`, and `SimpleTable` that caused
4+
header alignment settings to be overridden by data alignment settings.
25
* Enhancements
3-
* Added ability to use `CompletionItems` as argparse choices
6+
* `CompletionItems` now saves the original object from which it creates a string.
7+
* Using `CompletionItems` as argparse choices is fully supported. `cmd2` patched `argparse` to compare input to
8+
the original value instead of the `CompletionItems` instance.
9+
* `ArgparseCompleter` now does the following if a list of `CompletionItems` was created with numerical types
10+
* Sorts completion hints numerically
11+
* Right-aligns the left-most column in completion hint table
412

513
## 2.3.0 (November 11, 2021)
614
* Bug Fixes
@@ -13,7 +21,7 @@
1321
and/or data text. This allows for things like nesting an AlternatingTable in another AlternatingTable.
1422
* AlternatingTable no longer automatically applies background color to borders. This was done to improve
1523
appearance since the background color extended beyond the borders of the table.
16-
* Added ability to colorize all aspects of `AlternatingTables`, `BorderedTables`, and `SimpleTables`.
24+
* Added ability to colorize all aspects of `AlternatingTable`, `BorderedTable`, and `SimpleTable`.
1725
* Added support for 8-bit/256-colors with the `cmd2.EightBitFg` and `cmd2.EightBitBg` classes.
1826
* Added support for 24-bit/RGB colors with the `cmd2.RgbFg` and `cmd2.RgbBg` classes.
1927
* Removed dependency on colorama.

cmd2/argparse_completer.py

+35-8
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
)
5050
from .table_creator import (
5151
Column,
52+
HorizontalAlignment,
5253
SimpleTable,
5354
)
5455

@@ -547,15 +548,32 @@ def _complete_flags(self, text: str, line: str, begidx: int, endidx: int, matche
547548
return matches
548549

549550
def _format_completions(self, arg_state: _ArgumentState, completions: Union[List[str], List[CompletionItem]]) -> List[str]:
550-
# Check if the results are CompletionItems and that there aren't too many to display
551-
if 1 < len(completions) <= self._cmd2_app.max_completion_items and isinstance(completions[0], CompletionItem):
552-
completion_items = cast(List[CompletionItem], completions)
553-
four_spaces = 4 * ' '
551+
"""Format CompletionItems into hint table"""
552+
553+
# Nothing to do if we don't have at least 2 completions which are all CompletionItems
554+
if len(completions) < 2 or not all(isinstance(c, CompletionItem) for c in completions):
555+
return cast(List[str], completions)
556+
557+
completion_items = cast(List[CompletionItem], completions)
554558

555-
# If the user has not already sorted the CompletionItems, then sort them before appending the descriptions
556-
if not self._cmd2_app.matches_sorted:
559+
# Check if the data being completed have a numerical type
560+
all_nums = all(isinstance(c.orig_value, numbers.Number) for c in completion_items)
561+
562+
# Sort CompletionItems before building the hint table
563+
if not self._cmd2_app.matches_sorted:
564+
# If all orig_value types are numbers, then sort by that value
565+
if all_nums:
566+
completion_items.sort(key=lambda c: c.orig_value) # type: ignore[no-any-return]
567+
568+
# Otherwise sort as strings
569+
else:
557570
completion_items.sort(key=self._cmd2_app.default_sort_key)
558-
self._cmd2_app.matches_sorted = True
571+
572+
self._cmd2_app.matches_sorted = True
573+
574+
# Check if there are too many CompletionItems to display as a table
575+
if len(completions) <= self._cmd2_app.max_completion_items:
576+
four_spaces = 4 * ' '
559577

560578
# If a metavar was defined, use that instead of the dest field
561579
destination = arg_state.action.metavar if arg_state.action.metavar else arg_state.action.dest
@@ -588,13 +606,22 @@ def _format_completions(self, arg_state: _ArgumentState, completions: Union[List
588606
desc_width = max(widest_line(item.description), desc_width)
589607

590608
cols = list()
591-
cols.append(Column(destination.upper(), width=token_width))
609+
dest_alignment = HorizontalAlignment.RIGHT if all_nums else HorizontalAlignment.LEFT
610+
cols.append(
611+
Column(
612+
destination.upper(),
613+
width=token_width,
614+
header_horiz_align=dest_alignment,
615+
data_horiz_align=dest_alignment,
616+
)
617+
)
592618
cols.append(Column(desc_header, width=desc_width))
593619

594620
hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler)
595621
table_data = [[item, item.description] for item in completion_items]
596622
self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0)
597623

624+
# Return sorted list of completions
598625
return cast(List[str], completions)
599626

600627
def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: int, tokens: List[str]) -> List[str]:

cmd2/argparse_custom.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,12 @@ def __init__(self, value: object, description: str = '', *args: Any) -> None:
332332

333333
# Save the original value to support CompletionItems as argparse choices.
334334
# cmd2 has patched argparse so input is compared to this value instead of the CompletionItem instance.
335-
self.__orig_value = value
335+
self._orig_value = value
336336

337337
@property
338338
def orig_value(self) -> Any:
339-
"""Read-only property for __orig_value"""
340-
return self.__orig_value
339+
"""Read-only property for _orig_value"""
340+
return self._orig_value
341341

342342

343343
############################################################################################################

cmd2/table_creator.py

+21-20
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,9 @@ def _generate_cell_lines(self, cell_data: Any, is_header: bool, col: Column, fil
416416

417417
def generate_row(
418418
self,
419+
row_data: Sequence[Any],
420+
is_header: bool,
419421
*,
420-
row_data: Optional[Sequence[Any]] = None,
421422
fill_char: str = SPACE,
422423
pre_line: str = EMPTY,
423424
inter_cell: str = (2 * SPACE),
@@ -426,8 +427,9 @@ def generate_row(
426427
"""
427428
Generate a header or data table row
428429
429-
:param row_data: If this is None then a header row is generated. Otherwise data should have an entry for each
430-
column in the row. (Defaults to None)
430+
:param row_data: data with an entry for each column in the row
431+
:param is_header: True if writing a header cell, otherwise writing a data cell. This determines whether to
432+
use header or data alignment settings defined in the Columns.
431433
:param fill_char: character that fills remaining space in a cell. Defaults to space. If this is a tab,
432434
then it will be converted to one space. (Cannot be a line breaking character)
433435
:param pre_line: string to print before each line of a row. This can be used for a left row border and
@@ -453,13 +455,8 @@ def __init__(self) -> None:
453455
# Display width of this cell
454456
self.width = 0
455457

456-
if row_data is None:
457-
row_data = [col.header for col in self.cols]
458-
is_header = True
459-
else:
460-
if len(row_data) != len(self.cols):
461-
raise ValueError("Length of row_data must match length of cols")
462-
is_header = False
458+
if len(row_data) != len(self.cols):
459+
raise ValueError("Length of row_data must match length of cols")
463460

464461
# Replace tabs (tabs in data strings will be handled in _generate_cell_lines())
465462
fill_char = fill_char.replace('\t', SPACE)
@@ -654,14 +651,14 @@ def generate_header(self) -> str:
654651

655652
# Apply background color to header text in Columns which allow it
656653
to_display: List[Any] = []
657-
for index, col in enumerate(self.cols):
654+
for col in self.cols:
658655
if col.style_header_text:
659656
to_display.append(self.apply_header_bg(col.header))
660657
else:
661658
to_display.append(col.header)
662659

663660
# Create the header labels
664-
header_labels = self.generate_row(row_data=to_display, fill_char=fill_char, inter_cell=inter_cell)
661+
header_labels = self.generate_row(to_display, is_header=True, fill_char=fill_char, inter_cell=inter_cell)
665662
header_buf.write(header_labels)
666663

667664
# Add the divider if necessary
@@ -696,7 +693,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str:
696693
else:
697694
to_display.append(row_data[index])
698695

699-
return self.generate_row(row_data=to_display, fill_char=fill_char, inter_cell=inter_cell)
696+
return self.generate_row(to_display, is_header=False, fill_char=fill_char, inter_cell=inter_cell)
700697

701698
def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True, row_spacing: int = 1) -> str:
702699
"""
@@ -855,7 +852,8 @@ def generate_table_top_border(self) -> str:
855852
post_line = self.padding * '═' + '╗'
856853

857854
return self.generate_row(
858-
row_data=self.empty_data,
855+
self.empty_data,
856+
is_header=False,
859857
fill_char=self.apply_border_color(fill_char),
860858
pre_line=self.apply_border_color(pre_line),
861859
inter_cell=self.apply_border_color(inter_cell),
@@ -876,7 +874,8 @@ def generate_header_bottom_border(self) -> str:
876874
post_line = self.padding * '═' + '╣'
877875

878876
return self.generate_row(
879-
row_data=self.empty_data,
877+
self.empty_data,
878+
is_header=False,
880879
fill_char=self.apply_border_color(fill_char),
881880
pre_line=self.apply_border_color(pre_line),
882881
inter_cell=self.apply_border_color(inter_cell),
@@ -898,7 +897,8 @@ def generate_row_bottom_border(self) -> str:
898897
post_line = self.padding * '─' + '╢'
899898

900899
return self.generate_row(
901-
row_data=self.empty_data,
900+
self.empty_data,
901+
is_header=False,
902902
fill_char=self.apply_border_color(fill_char),
903903
pre_line=self.apply_border_color(pre_line),
904904
inter_cell=self.apply_border_color(inter_cell),
@@ -919,7 +919,8 @@ def generate_table_bottom_border(self) -> str:
919919
post_line = self.padding * '═' + '╝'
920920

921921
return self.generate_row(
922-
row_data=self.empty_data,
922+
self.empty_data,
923+
is_header=False,
923924
fill_char=self.apply_border_color(fill_char),
924925
pre_line=self.apply_border_color(pre_line),
925926
inter_cell=self.apply_border_color(inter_cell),
@@ -941,7 +942,7 @@ def generate_header(self) -> str:
941942

942943
# Apply background color to header text in Columns which allow it
943944
to_display: List[Any] = []
944-
for index, col in enumerate(self.cols):
945+
for col in self.cols:
945946
if col.style_header_text:
946947
to_display.append(self.apply_header_bg(col.header))
947948
else:
@@ -953,7 +954,7 @@ def generate_header(self) -> str:
953954
header_buf.write('\n')
954955
header_buf.write(
955956
self.generate_row(
956-
row_data=to_display, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
957+
to_display, is_header=True, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
957958
)
958959
)
959960
header_buf.write('\n')
@@ -988,7 +989,7 @@ def generate_data_row(self, row_data: Sequence[Any]) -> str:
988989
to_display.append(row_data[index])
989990

990991
return self.generate_row(
991-
row_data=to_display, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
992+
to_display, is_header=False, fill_char=fill_char, pre_line=pre_line, inter_cell=inter_cell, post_line=post_line
992993
)
993994

994995
def generate_table(self, table_data: Sequence[Sequence[Any]], *, include_header: bool = True) -> str:

tests/test_argparse_completer.py

+16-11
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,15 @@ def do_pos_and_flag(self, args: argparse.Namespace) -> None:
115115
CUSTOM_DESC_HEADER = "Custom Header"
116116

117117
# Lists used in our tests (there is a mix of sorted and unsorted on purpose)
118-
non_negative_int_choices = [1, 2, 3, 0, 22]
119-
int_choices = [-1, 1, -2, 2, 0, -12]
118+
non_negative_num_choices = [1, 2, 3, 0.5, 22]
119+
num_choices = [-1, 1, -2, 2.5, 0, -12]
120120
static_choices_list = ['static', 'choices', 'stop', 'here']
121121
choices_from_provider = ['choices', 'provider', 'probably', 'improved']
122122
completion_item_choices = [CompletionItem('choice_1', 'A description'), CompletionItem('choice_2', 'Another description')]
123123

124+
# This tests that CompletionItems created with numerical values are sorted as numbers.
125+
num_completion_items = [CompletionItem(5, "Five"), CompletionItem(1.5, "One.Five"), CompletionItem(2, "Five")]
126+
124127
def choices_provider(self) -> List[str]:
125128
"""Method that provides choices"""
126129
return self.choices_from_provider
@@ -141,14 +144,12 @@ def completion_item_method(self) -> List[CompletionItem]:
141144
"-p", "--provider", help="a flag populated with a choices provider", choices_provider=choices_provider
142145
)
143146
choices_parser.add_argument(
144-
'-d',
145147
"--desc_header",
146148
help='this arg has a descriptive header',
147149
choices_provider=completion_item_method,
148150
descriptive_header=CUSTOM_DESC_HEADER,
149151
)
150152
choices_parser.add_argument(
151-
'-n',
152153
"--no_header",
153154
help='this arg has no descriptive header',
154155
choices_provider=completion_item_method,
@@ -162,16 +163,19 @@ def completion_item_method(self) -> List[CompletionItem]:
162163
metavar=TUPLE_METAVAR,
163164
nargs=argparse.ONE_OR_MORE,
164165
)
165-
choices_parser.add_argument('-i', '--int', type=int, help='a flag with an int type', choices=int_choices)
166+
choices_parser.add_argument('-n', '--num', type=int, help='a flag with an int type', choices=num_choices)
166167
choices_parser.add_argument('--completion_items', help='choices are CompletionItems', choices=completion_item_choices)
168+
choices_parser.add_argument(
169+
'--num_completion_items', help='choices are numerical CompletionItems', choices=num_completion_items
170+
)
167171

168172
# Positional args for choices command
169173
choices_parser.add_argument("list_pos", help="a positional populated with a choices list", choices=static_choices_list)
170174
choices_parser.add_argument(
171175
"method_pos", help="a positional populated with a choices provider", choices_provider=choices_provider
172176
)
173177
choices_parser.add_argument(
174-
'non_negative_int', type=int, help='a positional with non-negative int choices', choices=non_negative_int_choices
178+
'non_negative_num', type=int, help='a positional with non-negative numerical choices', choices=non_negative_num_choices
175179
)
176180
choices_parser.add_argument('empty_choices', help='a positional with empty choices', choices=[])
177181

@@ -558,10 +562,11 @@ def test_autcomp_flag_completion(ac_app, command_and_args, text, completion_matc
558562
('--list', 's', ['static', 'stop']),
559563
('-p', '', ArgparseCompleterTester.choices_from_provider),
560564
('--provider', 'pr', ['provider', 'probably']),
561-
('-i', '', ArgparseCompleterTester.int_choices),
562-
('--int', '1', ['1 ']),
563-
('--int', '-', [-1, -2, -12]),
564-
('--int', '-1', [-1, -12]),
565+
('-n', '', ArgparseCompleterTester.num_choices),
566+
('--num', '1', ['1 ']),
567+
('--num', '-', [-1, -2, -12]),
568+
('--num', '-1', [-1, -12]),
569+
('--num_completion_items', '', ArgparseCompleterTester.num_completion_items),
565570
],
566571
)
567572
def test_autocomp_flag_choices_completion(ac_app, flag, text, completions):
@@ -592,7 +597,7 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions):
592597
(1, 's', ['static', 'stop']),
593598
(2, '', ArgparseCompleterTester.choices_from_provider),
594599
(2, 'pr', ['provider', 'probably']),
595-
(3, '', ArgparseCompleterTester.non_negative_int_choices),
600+
(3, '', ArgparseCompleterTester.non_negative_num_choices),
596601
(3, '2', [2, 22]),
597602
(4, '', []),
598603
],

0 commit comments

Comments
 (0)