diff options
-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): |