Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
* 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
Expand Down
2 changes: 1 addition & 1 deletion cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ def _format_completions(self, action, completions: List[Union[str, CompletionIte

# 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)
Expand Down
2 changes: 1 addition & 1 deletion cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def my_completer_function(text, line, begidx, endidx):
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
Expand Down
43 changes: 21 additions & 22 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,9 +397,6 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
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,
Expand Down Expand Up @@ -468,11 +465,13 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
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
Expand Down Expand Up @@ -501,8 +500,8 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
# 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
Expand Down Expand Up @@ -1107,7 +1106,7 @@ def complete_users() -> List[str]:
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
Expand Down Expand Up @@ -1553,8 +1552,8 @@ def _complete_worker(self, text: str, state: int) -> Optional[str]:

# 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:
Expand Down Expand Up @@ -2326,8 +2325,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
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
Expand Down Expand Up @@ -2507,8 +2505,7 @@ def _macro_list(self, args: argparse.Namespace) -> None:
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
Expand Down Expand Up @@ -2692,10 +2689,10 @@ def _help_menu(self, verbose: bool = False) -> None:
"""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 = []
Expand Down Expand Up @@ -2730,7 +2727,7 @@ def _help_menu(self, verbose: bool = False) -> None:
# 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)

Expand Down Expand Up @@ -2816,7 +2813,9 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
@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))
Expand Down Expand Up @@ -2903,7 +2902,7 @@ def _show(self, args: argparse.Namespace, parameter: str = '') -> None:
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:
Expand Down
13 changes: 8 additions & 5 deletions cmd2/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def __init__(self,
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:
Expand All @@ -261,7 +261,7 @@ def __init__(self,
: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:
Expand All @@ -276,10 +276,13 @@ def __init__(self,
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
Expand Down
14 changes: 7 additions & 7 deletions tests/test_argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down Expand Up @@ -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', [
Expand All @@ -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', [
Expand All @@ -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', [
Expand All @@ -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', [
Expand All @@ -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):
Expand Down Expand Up @@ -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', [
Expand Down
6 changes: 3 additions & 3 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/test_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def parser():
'l': '!ls -al',
'anothermultiline': 'multiline',
'fake': 'run_pyscript'},
shortcuts=[('?', 'help'), ('!', 'shell')]
shortcuts={'?': 'help', '!': 'shell'}
)
return parser

Expand Down
2 changes: 1 addition & 1 deletion tests/test_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def parser():
'l': '!ls -al',
'anothermultiline': 'multiline',
'fake': 'run_pyscript'},
shortcuts=[('?', 'help'), ('!', 'shell')]
shortcuts={'?': 'help', '!': 'shell'}
)
return parser

Expand Down