diff options
-rw-r--r-- | CHANGELOG.md | 7 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 2 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 2 | ||||
-rw-r--r-- | cmd2/cmd2.py | 54 | ||||
-rw-r--r-- | cmd2/parsing.py | 13 | ||||
-rw-r--r-- | docs/features/history.rst | 2 | ||||
-rw-r--r-- | tests/conftest.py | 8 | ||||
-rw-r--r-- | tests/test_argparse_completer.py | 14 | ||||
-rw-r--r-- | tests/test_completion.py | 6 | ||||
-rw-r--r-- | tests/test_history.py | 2 | ||||
-rw-r--r-- | tests/test_parsing.py | 2 | ||||
-rw-r--r-- | tests/test_utils.py | 3 |
12 files changed, 63 insertions, 52 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 14aff012..615e282d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ * Moved `basic_complete` to utils.py * Made optional arguments on the following completer methods keyword-only: `delimiter_complete`, `flag_based_complete`, `index_based_complete`. `path_complete`, `shell_cmd_complete` + * Renamed history option from `--output-file` to `--output_file` + * Renamed `matches_sort_key` to `default_sort_key`. This value determines the default sort ordering of string + results like alias, command, category, macro, settable, and shortcut names. Unsorted tab-completion results + also are sorted with this key. Its default value (ALPHABETICAL_SORT_KEY) performs a case-insensitive alphabetical + sort, but it can be changed to a natural sort by setting the value to NATURAL_SORT_KEY. + * `StatementParser` now expects shortcuts to be passed in as dictionary. This eliminates the step of converting the + shortcuts dictionary into a tuple before creating `StatementParser`. ## 0.9.14 (June 29, 2019) * Enhancements diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 737286c1..95ccf7b4 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -391,7 +391,7 @@ class AutoCompleter(object): # If the user has not already sorted the CompletionItems, then sort them before appending the descriptions if not self._cmd2_app.matches_sorted: - completions.sort(key=self._cmd2_app.matches_sort_key) + completions.sort(key=self._cmd2_app.default_sort_key) self._cmd2_app.matches_sorted = True token_width = ansi_safe_wcswidth(action.dest) diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 1cdb7840..5d8e76ef 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -74,7 +74,7 @@ Tab Completion: completer_method This is exactly like completer_function, but the function needs to be an instance method of a cmd2-based class. When AutoCompleter calls the method, it will pass the app instance as the self argument. cmd2 provides - a few completer methods for convenience (e.g. path_complete, delimiter_complete) + a few completer methods for convenience (e.g., path_complete, delimiter_complete) Example: This adds file-path completion to an argument diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index a4036a8e..0255d1ce 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -397,9 +397,6 @@ class Cmd(cmd.Cmd): self._py_history = [] self.pyscript_name = 'app' - if shortcuts is None: - shortcuts = constants.DEFAULT_SHORTCUTS - shortcuts = sorted(shortcuts.items(), reverse=True) self.statement_parser = StatementParser(allow_redirection=allow_redirection, terminators=terminators, multiline_commands=multiline_commands, @@ -468,11 +465,13 @@ class Cmd(cmd.Cmd): elif transcript_files: self._transcript_files = transcript_files - # The default key for sorting tab completion matches. This only applies when the matches are not - # already marked as sorted by setting self.matches_sorted to True. Its default value performs a - # case-insensitive alphabetical sort. If natural sorting preferred, then set this to NATURAL_SORT_KEY. - # Otherwise it can be set to any custom key to meet your needs. - self.matches_sort_key = ALPHABETICAL_SORT_KEY + # The default key for sorting string results. Its default value performs a case-insensitive alphabetical sort. + # If natural sorting is preferred, then set this to NATURAL_SORT_KEY. + # cmd2 uses this key for sorting: + # command and category names + # alias, macro, settable, and shortcut names + # tab completion results when self.matches_sorted is False + self.default_sort_key = ALPHABETICAL_SORT_KEY ############################################################################################################ # The following variables are used by tab-completion functions. They are reset each time complete() is run @@ -501,8 +500,8 @@ class Cmd(cmd.Cmd): # quote matches that are completed in a delimited fashion self.matches_delimited = False - # Set to True before returning matches to complete() in cases where matches are sorted with custom ordering. - # If False, then complete() will sort the matches using self.matches_sort_key before they are displayed. + # Set to True before returning matches to complete() in cases where matches have already been sorted. + # If False, then complete() will sort the matches using self.default_sort_key before they are displayed. self.matches_sorted = False # Set the pager(s) for use with the ppaged() method for displaying output using a pager @@ -1107,7 +1106,7 @@ class Cmd(cmd.Cmd): self.allow_closing_quote = False # Sort the matches before any trailing slashes are added - matches.sort(key=self.matches_sort_key) + matches.sort(key=self.default_sort_key) self.matches_sorted = True # Build display_matches and add a slash to directories @@ -1553,8 +1552,8 @@ class Cmd(cmd.Cmd): # Sort matches if they haven't already been sorted if not self.matches_sorted: - self.completion_matches.sort(key=self.matches_sort_key) - self.display_matches.sort(key=self.matches_sort_key) + self.completion_matches.sort(key=self.default_sort_key) + self.display_matches.sort(key=self.default_sort_key) self.matches_sorted = True try: @@ -2326,8 +2325,7 @@ class Cmd(cmd.Cmd): else: self.perror("Alias '{}' not found".format(cur_name)) else: - sorted_aliases = utils.alphabetical_sort(self.aliases) - for cur_alias in sorted_aliases: + for cur_alias in sorted(self.aliases, key=self.default_sort_key): self.poutput("alias create {} {}".format(cur_alias, self.aliases[cur_alias])) # Top-level parser for alias @@ -2507,8 +2505,7 @@ class Cmd(cmd.Cmd): else: self.perror("Macro '{}' not found".format(cur_name)) else: - sorted_macros = utils.alphabetical_sort(self.macros) - for cur_macro in sorted_macros: + for cur_macro in sorted(self.macros, key=self.default_sort_key): self.poutput("macro create {} {}".format(cur_macro, self.macros[cur_macro].value)) # Top-level parser for macro @@ -2692,10 +2689,10 @@ class Cmd(cmd.Cmd): """Show a list of commands which help can be displayed for. """ # Get a sorted list of help topics - help_topics = utils.alphabetical_sort(self.get_help_topics()) + help_topics = sorted(self.get_help_topics(), key=self.default_sort_key) # Get a sorted list of visible command names - visible_commands = utils.alphabetical_sort(self.get_visible_commands()) + visible_commands = sorted(self.get_visible_commands(), key=self.default_sort_key) cmds_doc = [] cmds_undoc = [] @@ -2730,7 +2727,7 @@ class Cmd(cmd.Cmd): # Categories found, Organize all commands by category self.poutput('{}'.format(str(self.doc_leader))) self.poutput('{}'.format(str(self.doc_header)), end="\n\n") - for category in sorted(cmds_cats.keys()): + for category in sorted(cmds_cats.keys(), key=self.default_sort_key): self._print_topics(category, cmds_cats[category], verbose) self._print_topics('Other', cmds_doc, verbose) @@ -2816,7 +2813,9 @@ class Cmd(cmd.Cmd): @with_argparser(ArgParser()) def do_shortcuts(self, _: argparse.Namespace) -> None: """List available shortcuts""" - result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.statement_parser.shortcuts)) + # Sort the shortcut tuples by name + sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0])) + result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts) self.poutput("Shortcuts for other commands:\n{}".format(result)) @with_argparser(ArgParser(epilog=INTERNAL_COMMAND_EPILOG)) @@ -2903,7 +2902,7 @@ class Cmd(cmd.Cmd): maxlen = max(maxlen, len(result[p])) if result: - for p in sorted(result): + for p in sorted(result, key=self.default_sort_key): if args.long: self.poutput('{} # {}'.format(result[p].ljust(maxlen), self.settable[p])) else: @@ -3275,11 +3274,11 @@ class Cmd(cmd.Cmd): history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_action_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items') - history_action_group.add_argument('-o', '--output-file', metavar='FILE', + history_action_group.add_argument('-o', '--output_file', metavar='FILE', help='output commands to a script file, implies -s', completer_method=path_complete) - history_action_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file,\n' - 'implies -s', + history_action_group.add_argument('-t', '--transcript', metavar='TRANSCRIPT_FILE', + help='output commands and results to a transcript file,\nimplies -s', completer_method=path_complete) history_action_group.add_argument('-c', '--clear', action='store_true', help='clear all history') @@ -3598,11 +3597,12 @@ class Cmd(cmd.Cmd): "Script should contain one command per line, just like the command would be\n" "typed in the console.\n" "\n" - "If the -r/--record_transcript flag is used, this command instead records\n" + "If the -t/--transcript flag is used, this command instead records\n" "the output of the script commands to a transcript for testing purposes.\n") run_script_parser = ArgParser(description=run_script_description) - run_script_parser.add_argument('-t', '--transcript', help='record the output of the script as a transcript file', + run_script_parser.add_argument('-t', '--transcript', metavar='TRANSCRIPT_FILE', + help='record the output of the script as a transcript file', completer_method=path_complete) run_script_parser.add_argument('script_path', help="path to the script file", completer_method=path_complete) diff --git a/cmd2/parsing.py b/cmd2/parsing.py index 2e94516a..dbfabc80 100644 --- a/cmd2/parsing.py +++ b/cmd2/parsing.py @@ -249,7 +249,7 @@ class StatementParser: terminators: Optional[Iterable[str]] = None, multiline_commands: Optional[Iterable[str]] = None, aliases: Optional[Dict[str, str]] = None, - shortcuts: Optional[Iterable[Tuple[str, str]]] = None) -> None: + shortcuts: Optional[Dict[str, str]] = None) -> None: """Initialize an instance of StatementParser. The following will get converted to an immutable tuple before storing internally: @@ -261,7 +261,7 @@ class StatementParser: :param terminators: iterable containing strings which should terminate multiline commands :param multiline_commands: iterable containing the names of commands that accept multiline input :param aliases: dictionary containing aliases - :param shortcuts: an iterable of tuples with each tuple containing the shortcut and the expansion + :param shortcuts: dictionary containing shortcuts """ self.allow_redirection = allow_redirection if terminators is None: @@ -276,10 +276,13 @@ class StatementParser: self.aliases = dict() else: self.aliases = aliases + if shortcuts is None: - self.shortcuts = tuple() - else: - self.shortcuts = tuple(shortcuts) + shortcuts = constants.DEFAULT_SHORTCUTS + + # Sort the shortcuts in descending order by name length because the longest match + # should take precedence. (e.g., @@file should match '@@' and not '@'. + self.shortcuts = tuple(sorted(shortcuts.items(), key=lambda x: len(x[0]), reverse=True)) # commands have to be a word, so make a regular expression # that matches the first word in the line. This regex has three diff --git a/docs/features/history.rst b/docs/features/history.rst index 8aa305c0..c1806065 100644 --- a/docs/features/history.rst +++ b/docs/features/history.rst @@ -191,7 +191,7 @@ reference to identify previously entered commands. However, when creating a script or a transcript, the command numbers would prevent the script from loading properly. The ``-s`` or ``--script`` option instructs the ``history`` command to suppress the line numbers. This option is automatically set by the -``--output-file``, ``--transcript``, and ``--edit`` options. If you want to +``--output_file``, ``--transcript``, and ``--edit`` options. If you want to output the history commands with line numbers to a file, you can do it with output redirection:: diff --git a/tests/conftest.py b/tests/conftest.py index c0aea4a6..e09e07b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,8 +44,8 @@ def verify_help_text(cmd2_app: cmd2.Cmd, help_output: Union[str, List[str]]) -> # Help text for the history command -HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT | -c] [-s] [-x] [-v] - [-a] +HELP_HISTORY = """Usage: history [-h] [-r | -e | -o FILE | -t TRANSCRIPT_FILE | -c] [-s] [-x] + [-v] [-a] [arg] View, run, edit, save, or clear previously entered commands @@ -61,9 +61,9 @@ optional arguments: -h, --help show this help message and exit -r, --run run selected history items -e, --edit edit and then run selected history items - -o, --output-file FILE + -o, --output_file FILE output commands to a script file, implies -s - -t, --transcript TRANSCRIPT + -t, --transcript TRANSCRIPT_FILE output commands and results to a transcript file, implies -s -c, --clear clear all history diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index 4ad4c560..19ec551b 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -263,7 +263,7 @@ def test_complete_help(ac_app, command, text, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize('command_and_args, text, completions', [ @@ -320,7 +320,7 @@ def test_autcomp_flag_completion(ac_app, command_and_args, text, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize('flag, text, completions', [ @@ -346,7 +346,7 @@ def test_autocomp_flag_choices_completion(ac_app, flag, text, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize('pos, text, completions', [ @@ -369,7 +369,7 @@ def test_autocomp_positional_choices_completion(ac_app, pos, text, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize('flag, text, completions', [ @@ -389,7 +389,7 @@ def test_autocomp_flag_completers(ac_app, flag, text, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize('pos, text, completions', [ @@ -410,7 +410,7 @@ def test_autocomp_positional_completers(ac_app, pos, text, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) def test_autocomp_blank_token(ac_app): @@ -548,7 +548,7 @@ def test_autcomp_nargs(ac_app, args, completions): else: assert first_match is None - assert ac_app.completion_matches == sorted(completions, key=ac_app.matches_sort_key) + assert ac_app.completion_matches == sorted(completions, key=ac_app.default_sort_key) @pytest.mark.parametrize('command_and_args, text, is_error', [ diff --git a/tests/test_completion.py b/tests/test_completion.py index 1411cc49..3cee1955 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -174,20 +174,20 @@ def test_complete_macro(base_app, request): assert first_match is not None and base_app.completion_matches == expected -def test_matches_sort_key(cmd2_app): +def test_default_sort_key(cmd2_app): text = '' line = 'test_sort_key {}'.format(text) endidx = len(line) begidx = endidx - len(text) # First do alphabetical sorting - cmd2_app.matches_sort_key = cmd2.cmd2.ALPHABETICAL_SORT_KEY + cmd2_app.default_sort_key = cmd2.cmd2.ALPHABETICAL_SORT_KEY expected = ['1', '11', '2'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected # Now switch to natural sorting - cmd2_app.matches_sort_key = cmd2.cmd2.NATURAL_SORT_KEY + cmd2_app.default_sort_key = cmd2.cmd2.NATURAL_SORT_KEY expected = ['1', '2', '11'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected diff --git a/tests/test_history.py b/tests/test_history.py index add93ea6..88f38172 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -276,7 +276,7 @@ def parser(): 'l': '!ls -al', 'anothermultiline': 'multiline', 'fake': 'run_pyscript'}, - shortcuts=[('?', 'help'), ('!', 'shell')] + shortcuts={'?': 'help', '!': 'shell'} ) return parser diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 13a535c0..a629d9fa 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -21,7 +21,7 @@ def parser(): 'l': '!ls -al', 'anothermultiline': 'multiline', 'fake': 'run_pyscript'}, - shortcuts=[('?', 'help'), ('!', 'shell')] + shortcuts={'?': 'help', '!': 'shell'} ) return parser diff --git a/tests/test_utils.py b/tests/test_utils.py index 262e6c54..edb6ca68 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -234,7 +234,8 @@ def test_proc_reader_send_sigint(pr_none): else: assert ret_code == -signal.SIGINT -@pytest.mark.skipif(sys.platform == 'linux', reason="Test doesn't work correctly on TravisCI") +@pytest.mark.skipif(not sys.platform.startswith('win'), + reason="Test doesn't work correctly on TravisCI and is unreliable on Azure DevOps macOS") def test_proc_reader_terminate(pr_none): assert pr_none._proc.poll() is None pr_none.terminate() |