diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2021-08-19 14:45:28 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2021-08-19 15:50:24 -0400 |
commit | 8ba05ef8bcdd53bdd54999cc9885ab310b766d9c (patch) | |
tree | 4a683b34d41710f94e7f259580ceb0ff1f46cc4b | |
parent | df1925db8607b06079ba78d497701ca961b855ab (diff) | |
download | cmd2-git-8ba05ef8bcdd53bdd54999cc9885ab310b766d9c.tar.gz |
set command output now uses SimpleTable.
Tabled tab completion now includes divider row.
Tab completion results for aliases, macros, and Settables wrap long fields.
SimpleTable now accepts blank for the divider character. It is identical to passing None.
Removed --verbose flag from set command so the descriptions always show.
-rw-r--r-- | CHANGELOG.md | 10 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 2 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 1 | ||||
-rw-r--r-- | cmd2/cmd2.py | 89 | ||||
-rw-r--r-- | cmd2/table_creator.py | 5 | ||||
-rw-r--r-- | docs/features/builtin_commands.rst | 26 | ||||
-rw-r--r-- | docs/features/settings.rst | 8 | ||||
-rw-r--r-- | tests/conftest.py | 40 | ||||
-rw-r--r-- | tests/test_argparse_completer.py | 24 | ||||
-rwxr-xr-x | tests/test_cmd2.py | 47 | ||||
-rw-r--r-- | tests/test_table_creator.py | 7 | ||||
-rw-r--r-- | tests/transcripts/regex_set.txt | 31 | ||||
-rw-r--r-- | tests_isolated/test_commandset/conftest.py | 22 | ||||
-rw-r--r-- | tests_isolated/test_commandset/test_commandset.py | 12 |
14 files changed, 189 insertions, 135 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 06db3f6d..9f8f840a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.2.0 (TBD, 2021) +* Enhancements + * Using `SimpleTable` in the output for the following commands to improve appearance. + * help + * set (command and tab completion of Settables) + * alias tab completion + * macro tab completion + * Tab completion of `CompletionItems` now includes divider row comprised of `Cmd.ruler` character. + * Removed `--verbose` flag from set command since descriptions always show now. + ## 2.1.2 (July 5, 2021) * Enhancements * Added the following accessor methods for cmd2-specific attributes to the `argparse.Action` class diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index a244941c..967e3f1c 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -578,7 +578,7 @@ class ArgparseCompleter: cols.append(Column(destination.upper(), width=token_width)) cols.append(Column(desc_header, width=desc_width)) - hint_table = SimpleTable(cols, divider_char=None) + hint_table = SimpleTable(cols, divider_char=self._cmd2_app.ruler) table_data = [[item, item.description] for item in completion_items] self._cmd2_app.formatted_completions = hint_table.generate_table(table_data, row_spacing=0) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 8d0630fe..faeb0789 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -130,6 +130,7 @@ tokens with descriptions instead of just a table of tokens:: The user sees this: ITEM_ID Item Name + ============================ 1 My item 2 Another item 3 Yet another item diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index d479e484..35398088 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2156,17 +2156,44 @@ class Cmd(cmd.Cmd): if command not in self.hidden_commands and command not in self.disabled_commands ] + # Table displayed when tab completing aliases + _alias_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None) + def _get_alias_completion_items(self) -> List[CompletionItem]: - """Return list of current alias names and values as CompletionItems""" - return [CompletionItem(cur_key, self.aliases[cur_key]) for cur_key in self.aliases] + """Return list of alias names and values as CompletionItems""" + results: List[CompletionItem] = [] + + for cur_key in self.aliases: + row_data = [self.aliases[cur_key]] + results.append(CompletionItem(cur_key, self._alias_completion_table.generate_data_row(row_data))) + + return results + + # Table displayed when tab completing macros + _macro_completion_table = SimpleTable([Column('Value', width=80)], divider_char=None) def _get_macro_completion_items(self) -> List[CompletionItem]: - """Return list of current macro names and values as CompletionItems""" - return [CompletionItem(cur_key, self.macros[cur_key].value) for cur_key in self.macros] + """Return list of macro names and values as CompletionItems""" + results: List[CompletionItem] = [] + + for cur_key in self.macros: + row_data = [self.macros[cur_key].value] + results.append(CompletionItem(cur_key, self._macro_completion_table.generate_data_row(row_data))) + + return results + + # Table displayed when tab completing Settables + _settable_completion_table = SimpleTable([Column('Value', width=30), Column('Description', width=60)], divider_char=None) def _get_settable_completion_items(self) -> List[CompletionItem]: - """Return list of current settable names and descriptions as CompletionItems""" - return [CompletionItem(cur_key, self.settables[cur_key].description) for cur_key in self.settables] + """Return list of Settable names, values, and descriptions as CompletionItems""" + results: List[CompletionItem] = [] + + for cur_key in self.settables: + row_data = [self.settables[cur_key].get_value(), self.settables[cur_key].description] + results.append(CompletionItem(cur_key, self._settable_completion_table.generate_data_row(row_data))) + + return results def _get_commands_aliases_and_macros_for_completion(self) -> List[str]: """Return a list of visible commands, aliases, and macros for tab completion""" @@ -3167,7 +3194,7 @@ class Cmd(cmd.Cmd): nargs=argparse.ZERO_OR_MORE, help='alias(es) to delete', choices_provider=_get_alias_completion_items, - descriptive_header='Value', + descriptive_header=_alias_completion_table.generate_header(), ) @as_subcommand_to('alias', 'delete', alias_delete_parser, help=alias_delete_help) @@ -3201,7 +3228,7 @@ class Cmd(cmd.Cmd): nargs=argparse.ZERO_OR_MORE, help='alias(es) to list', choices_provider=_get_alias_completion_items, - descriptive_header='Value', + descriptive_header=_alias_completion_table.generate_header(), ) @as_subcommand_to('alias', 'list', alias_list_parser, help=alias_list_help) @@ -3393,7 +3420,7 @@ class Cmd(cmd.Cmd): nargs=argparse.ZERO_OR_MORE, help='macro(s) to delete', choices_provider=_get_macro_completion_items, - descriptive_header='Value', + descriptive_header=_macro_completion_table.generate_header(), ) @as_subcommand_to('macro', 'delete', macro_delete_parser, help=macro_delete_help) @@ -3427,7 +3454,7 @@ class Cmd(cmd.Cmd): nargs=argparse.ZERO_OR_MORE, help='macro(s) to list', choices_provider=_get_macro_completion_items, - descriptive_header='Value', + descriptive_header=_macro_completion_table.generate_header(), ) @as_subcommand_to('macro', 'list', macro_list_parser, help=macro_list_help) @@ -3683,12 +3710,11 @@ class Cmd(cmd.Cmd): # Find the widest command widest = max([ansi.style_aware_wcswidth(command) for command in cmds]) - # Define the topic table + # Define the table structure name_column = Column('', width=max(widest, 20)) desc_column = Column('', width=80) - divider_char = self.ruler if self.ruler else None - topic_table = SimpleTable([name_column, desc_column], divider_char=divider_char) + topic_table = SimpleTable([name_column, desc_column], divider_char=self.ruler) # Build the topic table table_str_buf = io.StringIO() @@ -3875,14 +3901,11 @@ class Cmd(cmd.Cmd): ) set_parser_parent = DEFAULT_ARGUMENT_PARSER(description=set_description, add_help=False) set_parser_parent.add_argument( - '-v', '--verbose', action='store_true', help='include description of parameters when viewing' - ) - set_parser_parent.add_argument( 'param', nargs=argparse.OPTIONAL, help='parameter to set or view', choices_provider=_get_settable_completion_items, - descriptive_header='Description', + descriptive_header=_settable_completion_table.generate_header(), ) # Create the parser for the set command @@ -3924,21 +3947,25 @@ class Cmd(cmd.Cmd): # Show all settables to_show = list(self.settables.keys()) - # Build the result strings - max_len = 0 - results = dict() - for param in to_show: + # Define the table structure + name_label = 'Name' + max_name_width = max([ansi.style_aware_wcswidth(param) for param in to_show]) + max_name_width = max(max_name_width, ansi.style_aware_wcswidth(name_label)) + + cols: List[Column] = [ + Column(name_label, width=max_name_width), + Column('Value', width=30), + Column('Description', width=60), + ] + + table = SimpleTable(cols, divider_char=self.ruler) + self.poutput(table.generate_header()) + + # Build the table + for param in sorted(to_show, key=self.default_sort_key): settable = self.settables[param] - results[param] = f"{param}: {settable.get_value()!r}" - max_len = max(max_len, ansi.style_aware_wcswidth(results[param])) - - # Display the results - for param in sorted(results, key=self.default_sort_key): - result_str = results[param] - if args.verbose: - self.poutput(f'{utils.align_left(result_str, width=max_len)} # {self.settables[param].description}') - else: - self.poutput(result_str) + row_data = [param, settable.get_value(), settable.description] + self.poutput(table.generate_data_row(row_data)) shell_parser = DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt") shell_parser.add_argument('command', help='the command to run', completer=shell_cmd_complete) diff --git a/cmd2/table_creator.py b/cmd2/table_creator.py index 38102a07..5420ebec 100644 --- a/cmd2/table_creator.py +++ b/cmd2/table_creator.py @@ -545,7 +545,7 @@ class SimpleTable(TableCreator): :param column_spacing: how many spaces to place between columns. Defaults to 2. :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. Set this to None if you don't + :param divider_char: optional character used to build the header divider row. Set this to blank or None if you don't want a divider row. Defaults to dash. (Cannot be a line breaking character) :raises: ValueError if column_spacing is less than 0 :raises: ValueError if tab_width is less than 1 @@ -556,6 +556,9 @@ class SimpleTable(TableCreator): raise ValueError("Column spacing cannot be less than 0") self.inter_cell = column_spacing * SPACE + if divider_char == '': + divider_char = None + if divider_char is not None: if len(ansi.strip_style(divider_char)) != 1: raise TypeError("Divider character must be exactly one character long") diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst index 14f340c2..97b160b5 100644 --- a/docs/features/builtin_commands.rst +++ b/docs/features/builtin_commands.rst @@ -102,16 +102,22 @@ within a running application: .. code-block:: text - (Cmd) set --verbose - allow_style: 'Terminal' # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) - always_show_hint: False # Display tab completion hint even when completion suggestions print - debug: True # Show full traceback on exception - echo: False # Echo command issued into output - editor: 'vi' # Program used by 'edit' - feedback_to_output: False # Include nonessentials in '|', '>' results - max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion - quiet: False # Don't print nonessential feedback - timing: False # Report execution times + (Cmd) set + Name Value Description + ================================================================================================================== + allow_style Terminal Allow ANSI text style sequences in output (valid values: + Terminal, Always, Never) + always_show_hint False Display tab completion hint even when completion suggestions + print + debug True Show full traceback on exception + echo False Echo command issued into output + editor vi Program used by 'edit' + feedback_to_output False Include nonessentials in '|', '>' results + max_completion_items 50 Maximum number of CompletionItems to display during tab + completion + quiet False Don't print nonessential feedback + timing False Report execution times + Any of these user-settable parameters can be set while running your app with the ``set`` command like so: diff --git a/docs/features/settings.rst b/docs/features/settings.rst index c21b3258..0be46292 100644 --- a/docs/features/settings.rst +++ b/docs/features/settings.rst @@ -134,10 +134,10 @@ changes a setting, and will receive both the old value and the new value. .. code-block:: text - (Cmd) set --verbose | grep sunny - sunny: False # Is it sunny outside? - (Cmd) set --verbose | grep degrees - degrees_c: 22 # Temperature in Celsius + (Cmd) set | grep sunny + sunny False Is it sunny outside? + (Cmd) set | grep degrees + degrees_c 22 Temperature in Celsius (Cmd) sunbathe Too dim. (Cmd) set degrees_c 41 diff --git a/tests/conftest.py b/tests/conftest.py index a5c47d97..0829da2f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,29 +104,23 @@ SHORTCUTS_TXT = """Shortcuts for other commands: @@: _relative_run_script """ -# Output from the show command with default settings -SHOW_TXT = """allow_style: 'Terminal' -always_show_hint: False -debug: False -echo: False -editor: 'vim' -feedback_to_output: False -max_completion_items: 50 -quiet: False -timing: False -""" - -SHOW_LONG = """ -allow_style: 'Terminal' # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) -always_show_hint: False # Display tab completion hint even when completion suggestions print -debug: False # Show full traceback on exception -echo: False # Echo command issued into output -editor: 'vim' # Program used by 'edit' -feedback_to_output: False # Include nonessentials in '|', '>' results -max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion -quiet: False # Don't print nonessential feedback -timing: False # Report execution times -""" +# Output from the set command +SET_TXT = ( + "Name Value Description \n" + "==================================================================================================================\n" + "allow_style Terminal Allow ANSI text style sequences in output (valid values: \n" + " Terminal, Always, Never) \n" + "always_show_hint False Display tab completion hint even when completion suggestions\n" + " print \n" + "debug False Show full traceback on exception \n" + "echo False Echo command issued into output \n" + "editor vim Program used by 'edit' \n" + "feedback_to_output False Include nonessentials in '|', '>' results \n" + "max_completion_items 50 Maximum number of CompletionItems to display during tab \n" + " completion \n" + "quiet False Don't print nonessential feedback \n" + "timing False Report execution times \n" +) def normalize(block): diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 6002a856..25a13157 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -725,10 +725,14 @@ def test_completion_items(ac_app, num_aliases, show_description): assert bool(ac_app.formatted_completions) == show_description if show_description: - # If show_description is True, the table will show both the alias name and result - first_result_line = normalize(ac_app.formatted_completions)[1] - assert 'fake_alias0' in first_result_line - assert 'help' in first_result_line + # If show_description is True, the table will show both the alias name and value + description_displayed = False + for line in ac_app.formatted_completions.splitlines(): + if 'fake_alias0' in line and 'help' in line: + description_displayed = True + break + + assert description_displayed def test_completion_item_choices(ac_app): @@ -742,10 +746,14 @@ def test_completion_item_choices(ac_app): assert len(ac_app.completion_matches) == len(ac_app.completion_item_choices) assert len(ac_app.display_matches) == len(ac_app.completion_item_choices) - # Make sure a completion table was created - first_result_line = normalize(ac_app.formatted_completions)[1] - assert 'choice_1' in first_result_line - assert 'A description' in first_result_line + # The table will show both the choice and description + description_displayed = False + for line in ac_app.formatted_completions.splitlines(): + if 'choice_1' in line and 'A description' in line: + description_displayed = True + break + + assert description_displayed @pytest.mark.parametrize( diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 928c323a..c541259e 100755 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -31,9 +31,8 @@ from cmd2 import ( from .conftest import ( HELP_HISTORY, + SET_TXT, SHORTCUTS_TXT, - SHOW_LONG, - SHOW_TXT, complete_tester, normalize, odd_file_names, @@ -107,7 +106,7 @@ def test_base_argparse_help(base_app): def test_base_invalid_option(base_app): out, err = run_cmd(base_app, 'set -z') - assert err[0] == 'Usage: set [-h] [-v] [param] [value]' + assert err[0] == 'Usage: set [-h] [param] [value]' assert 'Error: unrecognized arguments: -z' in err[1] @@ -123,19 +122,11 @@ def test_command_starts_with_shortcut(): assert "Invalid command name 'help'" in str(excinfo.value) -def test_base_show(base_app): +def test_base_set(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' out, err = run_cmd(base_app, 'set') - expected = normalize(SHOW_TXT) - assert out == expected - - -def test_base_show_long(base_app): - # force editor to be 'vim' so test is repeatable across platforms - base_app.editor = 'vim' - out, err = run_cmd(base_app, 'set -v') - expected = normalize(SHOW_LONG) + expected = normalize(SET_TXT) assert out == expected @@ -150,7 +141,14 @@ now: True assert out == expected out, err = run_cmd(base_app, 'set quiet') - assert out == ['quiet: True'] + expected = normalize( + """ +Name Value Description +=================================================================================================== +quiet True Don't print nonessential feedback +""" + ) + assert out == expected def test_set_val_empty(base_app): @@ -1752,7 +1750,8 @@ def test_get_alias_completion_items(base_app): for cur_res in results: assert cur_res in base_app.aliases - assert cur_res.description == base_app.aliases[cur_res] + # Strip trailing spaces from table output + assert cur_res.description.rstrip() == base_app.aliases[cur_res] def test_get_macro_completion_items(base_app): @@ -1764,14 +1763,26 @@ def test_get_macro_completion_items(base_app): for cur_res in results: assert cur_res in base_app.macros - assert cur_res.description == base_app.macros[cur_res].value + # Strip trailing spaces from table output + assert cur_res.description.rstrip() == base_app.macros[cur_res].value def test_get_settable_completion_items(base_app): results = base_app._get_settable_completion_items() + assert len(results) == len(base_app.settables) + for cur_res in results: - assert cur_res in base_app.settables - assert cur_res.description == base_app.settables[cur_res].description + cur_settable = base_app.settables.get(cur_res) + assert cur_settable is not None + + # These CompletionItem descriptions are a two column table (Settable Value and Settable Description) + # First check if the description text starts with the value + str_value = str(cur_settable.get_value()) + assert cur_res.description.startswith(str_value) + + # The second column is likely to have wrapped long text. So we will just examine the + # first couple characters to look for the Settable's description. + assert cur_settable.description[0:10] in cur_res.description def test_alias_no_subcommand(base_app): diff --git a/tests/test_table_creator.py b/tests/test_table_creator.py index 70b77bad..e1bc8883 100644 --- a/tests/test_table_creator.py +++ b/tests/test_table_creator.py @@ -364,9 +364,12 @@ def test_simple_table_creation(): # No divider st = SimpleTable([column_1, column_2], divider_char=None) - table = st.generate_table(row_data) + no_divider_1 = st.generate_table(row_data) - assert table == ( + st = SimpleTable([column_1, column_2], divider_char='') + no_divider_2 = st.generate_table(row_data) + + assert no_divider_1 == no_divider_2 == ( 'Col 1 Col 2 \n' 'Col 1 Row 1 Col 2 Row 1 \n' '\n' diff --git a/tests/transcripts/regex_set.txt b/tests/transcripts/regex_set.txt index 623df8ed..68e61e30 100644 --- a/tests/transcripts/regex_set.txt +++ b/tests/transcripts/regex_set.txt @@ -3,14 +3,25 @@ # The regex for editor will match whatever program you use. # Regexes on prompts just make the trailing space obvious +(Cmd) set allow_style Terminal +allow_style - was: '/.*/' +now: 'Terminal' +(Cmd) set editor vim +editor - was: '/.*/' +now: 'vim' (Cmd) set -allow_style: /'(Terminal|Always|Never)'/ -always_show_hint: False -debug: False -echo: False -editor: /'.*'/ -feedback_to_output: False -max_completion_items: 50 -maxrepeats: 3 -quiet: False -timing: False +Name Value Description/ +/ +================================================================================================================== +allow_style Terminal Allow ANSI text style sequences in output (valid values:/ +/ + Terminal, Always, Never)/ +/ +always_show_hint False Display tab completion hint even when completion suggestions + print/ +/ +debug False Show full traceback on exception/ +/ +echo False Echo command issued into output/ +/ +editor vim Program used by 'edit'/ +/ +feedback_to_output False Include nonessentials in '|', '>' results/ +/ +max_completion_items 50 Maximum number of CompletionItems to display during tab/ +/ + completion/ +/ +maxrepeats 3 Max number of `--repeat`s allowed/ +/ +quiet False Don't print nonessential feedback/ +/ +timing False Report execution times/ +/ diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/test_commandset/conftest.py index 32def64d..43e2af3e 100644 --- a/tests_isolated/test_commandset/conftest.py +++ b/tests_isolated/test_commandset/conftest.py @@ -107,28 +107,6 @@ SHORTCUTS_TXT = """Shortcuts for other commands: @@: _relative_run_script """ -# Output from the show command with default settings -SHOW_TXT = """allow_style: 'Terminal' -debug: False -echo: False -editor: 'vim' -feedback_to_output: False -max_completion_items: 50 -quiet: False -timing: False -""" - -SHOW_LONG = """ -allow_style: 'Terminal' # Allow ANSI text style sequences in output (valid values: Terminal, Always, Never) -debug: False # Show full traceback on exception -echo: False # Echo command issued into output -editor: 'vim' # Program used by 'edit' -feedback_to_output: False # Include nonessentials in '|', '>' results -max_completion_items: 50 # Maximum number of CompletionItems to display during tab completion -quiet: False # Don't print nonessential feedback -timing: False # Report execution times -""" - def normalize(block): """Normalize a block of text to perform comparison. diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset/test_commandset.py index 7e4e1821..89bac976 100644 --- a/tests_isolated/test_commandset/test_commandset.py +++ b/tests_isolated/test_commandset/test_commandset.py @@ -987,9 +987,10 @@ def test_commandset_settables(): # verify the settable shows up out, err = run_cmd(app, 'set') - assert 'arbitrary_value: 5' in out + any(['arbitrary_value' in line and '5' in line for line in out]) + out, err = run_cmd(app, 'set arbitrary_value') - assert out == ['arbitrary_value: 5'] + any(['arbitrary_value' in line and '5' in line for line in out]) # change the value and verify the value changed out, err = run_cmd(app, 'set arbitrary_value 10') @@ -999,7 +1000,7 @@ now: 10 """ assert out == normalize(expected) out, err = run_cmd(app, 'set arbitrary_value') - assert out == ['arbitrary_value: 10'] + any(['arbitrary_value' in line and '10' in line for line in out]) # can't add to cmd2 now because commandset already has this settable with pytest.raises(KeyError): @@ -1058,9 +1059,10 @@ Parameter 'arbitrary_value' not supported (type 'set' for list of parameters). assert 'some.arbitrary_value' in app.settables.keys() out, err = run_cmd(app, 'set') - assert 'some.arbitrary_value: 5' in out + any(['some.arbitrary_value' in line and '5' in line for line in out]) + out, err = run_cmd(app, 'set some.arbitrary_value') - assert out == ['some.arbitrary_value: 5'] + any(['some.arbitrary_value' in line and '5' in line for line in out]) # verify registering a commandset with duplicate prefix and settable names fails with pytest.raises(CommandSetRegistrationError): |