diff options
-rw-r--r-- | cmd2/argparse_completer.py | 43 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 51 | ||||
-rwxr-xr-x | cmd2/cmd2.py | 46 | ||||
-rwxr-xr-x | tests/test_completion.py | 16 |
4 files changed, 75 insertions, 81 deletions
diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index d373a822..72241df1 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -9,7 +9,7 @@ See the header of argparse_custom.py for instructions on how to use these featur import argparse import numbers import shutil -from typing import List, Union +from typing import Dict, List, Union from . import cmd2 from . import utils @@ -144,20 +144,13 @@ class AutoCompleter(object): flag_arg_state = None matched_flags = [] - consumed_arg_values = {} # dict(arg_name -> [values, ...]) + consumed_arg_values = {} # dict(action -> [values, ...]) def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None: """Consuming token as an argument""" arg_state.count += 1 - - # Does this complete an option item for the flag? - arg_choices = self._resolve_choices_for_arg(arg_state.action) - - # If the current token is in the flag argument's autocomplete list, - # then track that we've used it already. - if token in arg_choices: - consumed_arg_values.setdefault(arg_state.action, []) - consumed_arg_values[arg_state.action].append(token) + consumed_arg_values.setdefault(arg_state.action, []) + consumed_arg_values[arg_state.action].append(token) ############################################################################################# # Parse all but the last token @@ -299,9 +292,8 @@ class AutoCompleter(object): # Check if we are completing a flag's argument if flag_arg_state is not None: - consumed = consumed_arg_values.get(flag_arg_state.action, []) completion_results = self._complete_for_arg(flag_arg_state.action, text, line, - begidx, endidx, consumed) + begidx, endidx, consumed_arg_values) # If we have results, then return them if completion_results: @@ -322,9 +314,8 @@ class AutoCompleter(object): action = self._positional_actions[pos_index] pos_arg_state = AutoCompleter._ArgumentState(action) - consumed = consumed_arg_values.get(pos_arg_state.action, []) completion_results = self._complete_for_arg(pos_arg_state.action, text, line, - begidx, endidx, consumed) + begidx, endidx, consumed_arg_values) # If we have results, then return them if completion_results: @@ -434,7 +425,8 @@ class AutoCompleter(object): return self._parser.format_help() def _complete_for_arg(self, arg: argparse.Action, - text: str, line: str, begidx: int, endidx: int, used_values=()) -> List[str]: + text: str, line: str, begidx: int, endidx: int, + consumed_arg_values: Dict[argparse.Action, List[str]]) -> List[str]: """Tab completion routine for argparse arguments""" # Check the arg provides choices to the user @@ -448,19 +440,30 @@ class AutoCompleter(object): # Check if the argument uses a specific tab completion function to provide its choices if isinstance(arg_choices, ChoicesCallable) and arg_choices.is_completer: + args = [] if arg_choices.is_method: - results = arg_choices.to_call(self._cmd2_app, text, line, begidx, endidx) - else: - results = arg_choices.to_call(text, line, begidx, endidx) + args.append(self._cmd2_app) + + args.extend([text, line, begidx, endidx]) + + if arg_choices.pass_parsed_args: + # Convert consumed_arg_values into an argparse Namespace + parsed_args = argparse.Namespace() + for key, val in consumed_arg_values.items(): + setattr(parsed_args, key.dest, val) + args.append(parsed_args) + + results = arg_choices.to_call(*args) # Otherwise use basic_complete on the choices else: + used_values = consumed_arg_values.get(arg, []) results = utils.basic_complete(text, line, begidx, endidx, self._resolve_choices_for_arg(arg, used_values)) return self._format_completions(arg, results) - def _resolve_choices_for_arg(self, arg: argparse.Action, used_values=()) -> List[str]: + def _resolve_choices_for_arg(self, arg: argparse.Action, used_values: List[str]) -> List[str]: """Retrieve a list of choices that are available for a particular argument""" # Check the arg provides choices to the user diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 2a7be287..334b93d9 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -159,10 +159,9 @@ argparse.ArgumentParser._match_argument - adds support to for nargs ranges import argparse import re import sys - # noinspection PyUnresolvedReferences,PyProtectedMember from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _ -from typing import Any, Callable, Iterable, List, Optional, Tuple, Union +from typing import Callable, Optional, Tuple, Union from .ansi import ansi_aware_write, style_error @@ -239,17 +238,21 @@ class ChoicesCallable: Enables using a callable as the choices provider for an argparse argument. While argparse has the built-in choices attribute, it is limited to an iterable. """ - def __init__(self, is_method: bool, is_completer: bool, to_call: Callable): + def __init__(self, is_method: bool, is_completer: bool, to_call: Callable, pass_parsed_args: bool): """ Initializer :param is_method: True if to_call is an instance method of a cmd2 app. False if it is a function. :param is_completer: True if to_call is a tab completion routine which expects the args: text, line, begidx, endidx :param to_call: the callable object that will be called to provide choices for the argument + :param pass_parsed_args: if True, then to_call will be passed an argparse Namespace of arguments + parsed by AutoCompleter. This is useful if the value of a particular argument + affects what data will be tab-completed. """ self.is_method = is_method self.is_completer = is_completer self.to_call = to_call + self.pass_parsed_args = pass_parsed_args def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCallable) -> None: @@ -272,26 +275,28 @@ def _set_choices_callable(action: argparse.Action, choices_callable: ChoicesCall setattr(action, ATTR_CHOICES_CALLABLE, choices_callable) -def set_choices_function(action: argparse.Action, choices_function: Callable[[], Iterable[Any]]) -> None: +def set_choices_function(action: argparse.Action, choices_function: Callable, pass_parsed_args: bool) -> None: """Set choices_function on an argparse action""" - _set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=False, to_call=choices_function)) + _set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=False, + to_call=choices_function, pass_parsed_args=pass_parsed_args)) -def set_choices_method(action: argparse.Action, choices_method: Callable[[Any], Iterable[Any]]) -> None: +def set_choices_method(action: argparse.Action, choices_method: Callable, pass_parsed_args: bool) -> None: """Set choices_method on an argparse action""" - _set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=False, to_call=choices_method)) + _set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=False, + to_call=choices_method, pass_parsed_args=pass_parsed_args)) -def set_completer_function(action: argparse.Action, - completer_function: Callable[[str, str, int, int], List[str]]) -> None: +def set_completer_function(action: argparse.Action, completer_function: Callable, pass_parsed_args: bool) -> None: """Set completer_function on an argparse action""" - _set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=True, to_call=completer_function)) + _set_choices_callable(action, ChoicesCallable(is_method=False, is_completer=True, + to_call=completer_function, pass_parsed_args=pass_parsed_args)) -def set_completer_method(action: argparse.Action, - completer_method: Callable[[Any, str, str, int, int], List[str]]) -> None: +def set_completer_method(action: argparse.Action, completer_method: Callable, pass_parsed_args: bool) -> None: """Set completer_method on an argparse action""" - _set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=True, to_call=completer_method)) + _set_choices_callable(action, ChoicesCallable(is_method=True, is_completer=True, + to_call=completer_method, pass_parsed_args=pass_parsed_args)) ############################################################################################################ @@ -305,10 +310,11 @@ orig_actions_container_add_argument = argparse._ActionsContainer.add_argument def _add_argument_wrapper(self, *args, nargs: Union[int, str, Tuple[int], Tuple[int, int], None] = None, - choices_function: Optional[Callable[[], Iterable[Any]]] = None, - choices_method: Optional[Callable[[Any], Iterable[Any]]] = None, - completer_function: Optional[Callable[[str, str, int, int], List[str]]] = None, - completer_method: Optional[Callable[[Any, str, str, int, int], List[str]]] = None, + choices_function: Optional[Callable] = None, + choices_method: Optional[Callable] = None, + completer_function: Optional[Callable] = None, + completer_method: Optional[Callable] = None, + pass_parsed_args: bool = False, suppress_tab_hint: bool = False, descriptive_header: Optional[str] = None, **kwargs) -> argparse.Action: @@ -328,6 +334,9 @@ def _add_argument_wrapper(self, *args, :param choices_method: cmd2-app method that provides choices for this argument :param completer_function: tab-completion function that provides choices for this argument :param completer_method: cmd2-app tab-completion method that provides choices for this argument + :param pass_parsed_args: if True, then to_call will be passed an argparse Namespace of arguments + parsed by AutoCompleter. This is useful if the value of a particular argument + affects what data will be tab-completed. :param suppress_tab_hint: when AutoCompleter has no results to show during tab completion, it displays the current argument's help text as a hint. Set this to True to suppress the hint. If this argument's help text is set to argparse.SUPPRESS, then tab hints will not display regardless of the @@ -412,13 +421,13 @@ def _add_argument_wrapper(self, *args, setattr(new_arg, ATTR_NARGS_RANGE, nargs_range) if choices_function: - set_choices_function(new_arg, choices_function) + set_choices_function(new_arg, choices_function, pass_parsed_args) elif choices_method: - set_choices_method(new_arg, choices_method) + set_choices_method(new_arg, choices_method, pass_parsed_args) elif completer_function: - set_completer_function(new_arg, completer_function) + set_completer_function(new_arg, completer_function, pass_parsed_args) elif completer_method: - set_completer_method(new_arg, completer_method) + set_completer_method(new_arg, completer_method, pass_parsed_args) setattr(new_arg, ATTR_SUPPRESS_TAB_HINT, suppress_tab_hint) setattr(new_arg, ATTR_DESCRIPTIVE_COMPLETION_HEADER, descriptive_header) diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index a653dc3c..40466446 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2668,49 +2668,39 @@ class Cmd(cmd.Cmd): strs_to_match = list(topics | visible_commands) return utils.basic_complete(text, line, begidx, endidx, strs_to_match) - def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: + def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: int, + parsed_args: argparse.Namespace) -> List[str]: """Completes the subcommand argument of help""" - # Get all tokens through the one being completed - tokens, _ = self.tokens_for_completion(line, begidx, endidx) - - if not tokens: + # Make sure we have a command whose subcommands we will complete + parsed_args.command = parsed_args.command[0] + if not parsed_args.command: return [] - # Must have at least 3 args for 'help command subcommand' - if len(tokens) < 3: + # Check if this command uses argparse + func = self.cmd_func(parsed_args.command) + argparser = getattr(func, CMD_ATTR_ARGPARSER, None) + if func is None or argparser is None: return [] - # Find where the command is by skipping past any flags - cmd_index = 1 - for cur_token in tokens[cmd_index:]: - if not cur_token.startswith('-'): - break - cmd_index += 1 - - if cmd_index >= len(tokens): + # Get all tokens through the one being completed + tokens, _ = self.tokens_for_completion(line, begidx, endidx) + if not tokens: return [] - command = tokens[cmd_index] - matches = [] - - # Check if this command uses argparse - func = self.cmd_func(command) - argparser = getattr(func, CMD_ATTR_ARGPARSER, None) + # Get the index of the command + cmd_index = tokens.index(parsed_args.command) - if func is not None and argparser is not None: - from .argparse_completer import AutoCompleter - completer = AutoCompleter(argparser, self) - matches = completer.complete_subcommand_help(tokens[cmd_index:], text, line, begidx, endidx) - - return matches + from .argparse_completer import AutoCompleter + completer = AutoCompleter(argparser, self) + return completer.complete_subcommand_help(tokens[cmd_index:], text, line, begidx, endidx) help_parser = Cmd2ArgumentParser(description="List available commands or provide " "detailed help for a specific command") help_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to retrieve help for", completer_method=complete_help_command) help_parser.add_argument('subcommand', nargs=argparse.REMAINDER, help="subcommand to retrieve help for", - completer_method=complete_help_subcommand) + completer_method=complete_help_subcommand, pass_parsed_args=True) help_parser.add_argument('-v', '--verbose', action='store_true', help="print a list of all commands with descriptions of each") diff --git a/tests/test_completion.py b/tests/test_completion.py index cf5dcf75..fb0d74e0 100755 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -1179,22 +1179,14 @@ def test_cmd2_help_subcommand_completion_with_flags_before_command(scu_app): first_match = complete_tester(text, line, begidx, endidx, scu_app) assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport'] -def test_complete_help_subcommand_with_no_command(scu_app): - # No command because not enough tokens +def test_complete_help_subcommand_with_blank_command(scu_app): text = '' - line = 'help ' + line = 'help "" {}'.format(text) endidx = len(line) begidx = endidx - len(text) - assert not scu_app.complete_help_subcommand(text, line, begidx, endidx) - - # No command because everything is a flag - text = '-v' - line = 'help -f -v' - endidx = len(line) - begidx = endidx - len(text) - - assert not scu_app.complete_help_subcommand(text, line, begidx, endidx) + first_match = complete_tester(text, line, begidx, endidx, scu_app) + assert first_match is None and not scu_app.completion_matches def test_cmd2_help_subcommand_completion_nomatch_scu(scu_app): |