diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-09-04 01:02:53 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2020-09-05 02:14:40 -0400 |
commit | e6a9a1c972e5e6195278966c6b134a2f450465e1 (patch) | |
tree | 30e1f1101fe64f556ba4a10c5b3e30b9c2e9cf0a /cmd2 | |
parent | 1054dda76b87d9f7f77311d8d804c1017b668996 (diff) | |
download | cmd2-git-e6a9a1c972e5e6195278966c6b134a2f450465e1.tar.gz |
Added always_show_hint setting
Fixed issue where flag names weren't always sorted correctly in argparse tab completion
Diffstat (limited to 'cmd2')
-rw-r--r-- | cmd2/argparse_completer.py | 68 | ||||
-rw-r--r-- | cmd2/cmd2.py | 49 |
2 files changed, 73 insertions, 44 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 0efaebe9..04e18d4a 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -35,6 +35,22 @@ DEFAULT_DESCRIPTIVE_HEADER = 'Description' ARG_TOKENS = 'arg_tokens' +# noinspection PyProtectedMember +def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str: + """Build tab completion hint for a given argument""" + # Check if hinting is disabled for this argument + suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False) + if suppress_hint or arg_action.help == argparse.SUPPRESS: + return '' + else: + # Use the parser's help formatter to display just this action's help text + formatter = parser._get_formatter() + formatter.start_section("Hint") + formatter.add_argument(arg_action) + formatter.end_section() + return formatter.format_help() + + def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool: """Returns if a token is just a single flag prefix character""" return len(token) == 1 and token[0] in parser.prefix_chars @@ -115,7 +131,6 @@ class _UnfinishedFlagError(CompletionError): super().__init__(error) -# noinspection PyProtectedMember class _NoResultsError(CompletionError): def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None: """ @@ -124,19 +139,8 @@ class _NoResultsError(CompletionError): :param parser: ArgumentParser instance which owns the action being tab completed :param arg_action: action being tab completed """ - # Check if hinting is disabled - suppress_hint = getattr(arg_action, ATTR_SUPPRESS_TAB_HINT, False) - if suppress_hint or arg_action.help == argparse.SUPPRESS: - hint_str = '' - else: - # Use the parser's help formatter to print just this action's help text - formatter = parser._get_formatter() - formatter.start_section("Hint") - formatter.add_argument(arg_action) - formatter.end_section() - hint_str = formatter.format_help() # Set apply_style to False because we don't want hints to look like errors - super().__init__(hint_str, apply_style=False) + super().__init__(_build_hint(parser, arg_action), apply_style=False) # noinspection PyProtectedMember @@ -411,6 +415,7 @@ class ArgparseCompleter: # If we have results, then return them if completion_results: + self._cmd2_app.completion_hint = _build_hint(self._parser, flag_arg_state.action) return completion_results # Otherwise, print a hint if the flag isn't finished or text isn't possibly the start of a flag @@ -432,6 +437,7 @@ class ArgparseCompleter: # If we have results, then return them if completion_results: + self._cmd2_app.completion_hint = _build_hint(self._parser, pos_arg_state.action) return completion_results # Otherwise, print a hint if text isn't possibly the start of a flag @@ -566,12 +572,21 @@ class ArgparseCompleter: """ # Check if the arg provides choices to the user if arg_state.action.choices is not None: - arg_choices = arg_state.action.choices + arg_choices = list(arg_state.action.choices) + if not arg_choices: + return [] + + # If these choices are numbers, then sort them now + if all(isinstance(x, numbers.Number) for x in arg_choices): + arg_choices.sort() + self._cmd2_app.matches_sorted = True + + # Since choices can be various types, convert them all to strings + arg_choices = [str(x) for x in arg_choices] else: arg_choices = getattr(arg_state.action, ATTR_CHOICES_CALLABLE, None) - - if arg_choices is None: - return [] + if arg_choices is None: + return [] # If we are going to call a completer/choices function, then set up the common arguments args = [] @@ -612,19 +627,6 @@ class ArgparseCompleter: if isinstance(arg_choices, ChoicesCallable) and not arg_choices.is_completer: arg_choices = arg_choices.to_call(*args, **kwargs) - # Since arg_choices can be any iterable type, convert to a list - arg_choices = list(arg_choices) - - # If these choices are numbers, and have not yet been sorted, then sort them now - if not self._cmd2_app.matches_sorted and all(isinstance(x, numbers.Number) for x in arg_choices): - arg_choices.sort() - self._cmd2_app.matches_sorted = True - - # Since choices can be various types like int, we must convert them to strings - for index, choice in enumerate(arg_choices): - if not isinstance(choice, str): - arg_choices[index] = str(choice) - # Filter out arguments we already used used_values = consumed_arg_values.get(arg_state.action.dest, []) arg_choices = [choice for choice in arg_choices if choice not in used_values] @@ -632,4 +634,10 @@ class ArgparseCompleter: # Do tab completion on the choices results = basic_complete(text, line, begidx, endidx, arg_choices) + if not results: + # Reset the value for matches_sorted. This is because completion of flag names + # may still be attempted after we return and they haven't been sorted yet. + self._cmd2_app.matches_sorted = False + return [] + return self._format_completions(arg_state, results) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index f3a2d88d..cde3892b 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -208,6 +208,7 @@ class Cmd(cmd.Cmd): self.allow_redirection = allow_redirection # Security setting to prevent redirection of stdout # Attributes which ARE dynamically settable via the set command at runtime + self.always_show_hint = False self.debug = False self.echo = False self.editor = Cmd.DEFAULT_EDITOR @@ -375,17 +376,21 @@ class Cmd(cmd.Cmd): # will be added if there is an unmatched opening quote self.allow_closing_quote = True - # An optional header that prints above the tab completion suggestions + # An optional hint which prints above tab completion suggestions + self.completion_hint = '' + + # Header which prints above CompletionItem tables self.completion_header = '' # Used by complete() for readline tab completion self.completion_matches = [] - # Use this list if you are completing strings that contain a common delimiter and you only want to - # display the final portion of the matches as the tab completion suggestions. The full matches - # still must be returned from your completer function. For an example, look at path_complete() - # which uses this to show only the basename of paths as the suggestions. delimiter_complete() also - # populates this list. + # Use this list if you need to display tab completion suggestions that are different than the actual text + # of the matches. For instance, if you are completing strings that contain a common delimiter and you only + # want to display the final portion of the matches as the tab completion suggestions. The full matches + # still must be returned from your completer function. For an example, look at path_complete() which + # uses this to show only the basename of paths as the suggestions. delimiter_complete() also populates + # this list. self.display_matches = [] # Used by functions like path_complete() and delimiter_complete() to properly @@ -788,6 +793,8 @@ class Cmd(cmd.Cmd): ansi.STYLE_NEVER), choices=[ansi.STYLE_TERMINAL, ansi.STYLE_ALWAYS, ansi.STYLE_NEVER])) + self.add_settable(Settable('always_show_hint', bool, + 'Display tab completion hint even when completion suggestions print')) self.add_settable(Settable('debug', bool, "Show full traceback on exception")) self.add_settable(Settable('echo', bool, "Echo command issued into output")) self.add_settable(Settable('editor', str, "Program used by 'edit'")) @@ -984,6 +991,7 @@ class Cmd(cmd.Cmd): """ self.allow_appended_space = True self.allow_closing_quote = True + self.completion_hint = '' self.completion_header = '' self.completion_matches = [] self.display_matches = [] @@ -1479,6 +1487,22 @@ class Cmd(cmd.Cmd): return [cur_match + padding for cur_match in matches_to_display], len(padding) + def _build_completion_metadata_string(self) -> str: # pragma: no cover + """Build completion metadata string which can contain a hint and CompletionItem table header""" + metadata = '' + + # Add hint if one exists and we are supposed to display it + if self.always_show_hint and self.completion_hint: + metadata += '\n' + self.completion_hint + + # Add table header if one exists + if self.completion_header: + if not metadata: + metadata += '\n' + metadata += '\n' + self.completion_header + + return metadata + def _display_matches_gnu_readline(self, substitution: str, matches: List[str], longest_match_length: int) -> None: # pragma: no cover """Prints a match list using GNU readline's rl_display_match_list() @@ -1523,9 +1547,8 @@ class Cmd(cmd.Cmd): strings_array[1:-1] = encoded_matches strings_array[-1] = None - # Print the header if one exists - if self.completion_header: - sys.stdout.write('\n\n' + self.completion_header) + # Print any metadata like a hint or table header + sys.stdout.write(self._build_completion_metadata_string()) # Call readline's display function # rl_display_match_list(strings_array, number of completion matches, longest match length) @@ -1551,10 +1574,8 @@ class Cmd(cmd.Cmd): # Add padding for visual appeal matches_to_display, _ = self._pad_matches_to_display(matches_to_display) - # Print the header if one exists - if self.completion_header: - # noinspection PyUnresolvedReferences - readline.rl.mode.console.write('\n\n' + self.completion_header) + # Print any metadata like a hint or table header + readline.rl.mode.console.write(sys.stdout.write(self._build_completion_metadata_string())) # Display matches using actual display function. This also redraws the prompt and line. orig_pyreadline_display(matches_to_display) @@ -3317,7 +3338,7 @@ class Cmd(cmd.Cmd): # Create the parser for the set command set_parser = DEFAULT_ARGUMENT_PARSER(parents=[set_parser_parent]) set_parser.add_argument('value', nargs=argparse.OPTIONAL, help='new value for settable', - completer_method=complete_set_value) + completer_method=complete_set_value, suppress_tab_hint=True) # Preserve quotes so users can pass in quoted empty strings and flags (e.g. -h) as the value @with_argparser(set_parser, preserve_quotes=True) |