diff options
author | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-09-16 15:36:39 -0400 |
---|---|---|
committer | Kevin Van Brunt <kmvanbrunt@gmail.com> | 2019-09-16 15:36:39 -0400 |
commit | e6585d155810f9afb2a246a71f939e1c0d511694 (patch) | |
tree | 1d41d0c8e2cdd6a1ff50edbdf48f50a3b22892b4 | |
parent | 9a7818b5a0e22e4ee5b107f6fdcceb3d3612ffd4 (diff) | |
download | cmd2-git-e6585d155810f9afb2a246a71f939e1c0d511694.tar.gz |
Changed arg_tokens to a dictionary
Including tokens from parent parsers in arg_tokens when subcommands are used
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | cmd2/argparse_completer.py | 57 | ||||
-rw-r--r-- | cmd2/argparse_custom.py | 8 | ||||
-rwxr-xr-x | cmd2/cmd2.py | 6 | ||||
-rw-r--r-- | tests/test_argparse_completer.py | 18 |
5 files changed, 52 insertions, 39 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 02dc78e3..2216d3a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * Fixed a bug when running a cmd2 application on Linux without Gtk libraries installed * Enhancements * No longer treating empty text scripts as an error condition - * Choices/Completer functions can now be passed an `argparse.Namespace` that maps command-line tokens to their + * Choices/Completer functions can now be passed a dictionary that maps command-line tokens to their argparse argument. This is helpful when one argument determines what is tab completed for another argument. If these functions have an argument called `arg_tokens`, then AutoCompleter will automatically pass this Namespace to them. diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index f1858f23..fb485348 100644 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -10,7 +10,7 @@ import argparse import inspect import numbers import shutil -from typing import Dict, List, Union +from typing import Dict, List, Optional, Union from . import cmd2 from . import utils @@ -22,8 +22,8 @@ from .rl_utils import rl_force_redisplay # If no descriptive header is supplied, then this will be used instead DEFAULT_DESCRIPTIVE_HEADER = 'Description' -# Name of the choice/completer function argument that, if present, will be passed a Namespace of -# command line tokens up through the token being completed mapped to their argparse destination. +# Name of the choice/completer function argument that, if present, will be passed a dictionary of +# command line tokens up through the token being completed mapped to their argparse destination name. ARG_TOKENS = 'arg_tokens' @@ -97,23 +97,31 @@ class AutoCompleter(object): self.min = self.action.nargs self.max = self.action.nargs - def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd) -> None: + def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *, + parent_tokens: Optional[Dict[str, List[str]]] = None) -> None: """ Create an AutoCompleter :param parser: ArgumentParser instance :param cmd2_app: reference to the Cmd2 application that owns this AutoCompleter + :param parent_tokens: optional dictionary mapping parent parsers' arg names to their tokens + this is only used by AutoCompleter when recursing on subcommand parsers + Defaults to None """ self._parser = parser self._cmd2_app = cmd2_app + if parent_tokens is None: + parent_tokens = dict() + self._parent_tokens = parent_tokens + self._flags = [] # all flags in this command self._flag_to_action = {} # maps flags to the argparse action object self._positional_actions = [] # actions for positional arguments (by position index) self._subcommand_action = None # this will be set if self._parser has subcommands # Start digging through the argparse structures. - # _actions is the top level container of parameter definitions + # _actions is the top level container of parameter definitions for action in self._parser._actions: # if the parameter is flag based, it will have option_strings if action.option_strings: @@ -152,13 +160,13 @@ class AutoCompleter(object): matched_flags = [] # Keeps track of arguments we've seen and any tokens they consumed - consumed_arg_values = dict() # dict(action -> tokens) + consumed_arg_values = dict() # dict(arg_name -> List[tokens]) def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None: """Consuming token as an argument""" arg_state.count += 1 - consumed_arg_values.setdefault(arg_state.action, []) - consumed_arg_values[arg_state.action].append(token) + consumed_arg_values.setdefault(arg_state.action.dest, []) + consumed_arg_values[arg_state.action.dest].append(token) ############################################################################################# # Parse all but the last token @@ -218,14 +226,14 @@ class AutoCompleter(object): argparse._CountAction)): # Flags with action set to append, append_const, and count can be reused # Therefore don't erase any tokens already consumed for this flag - consumed_arg_values.setdefault(action, []) + consumed_arg_values.setdefault(action.dest, []) else: # This flag is not resusable, so mark that we've seen it matched_flags.extend(action.option_strings) # It's possible we already have consumed values for this flag if it was used # earlier in the command line. Reset them now for this use of it. - consumed_arg_values[action] = [] + consumed_arg_values[action.dest] = [] new_arg_state = AutoCompleter._ArgumentState(action) @@ -256,7 +264,15 @@ class AutoCompleter(object): # Are we at a subcommand? If so, forward to the matching completer if action == self._subcommand_action: if token in self._subcommand_action.choices: - completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app) + # Merge self._parent_tokens and consumed_arg_values + parent_tokens = {**self._parent_tokens, **consumed_arg_values} + + # Include the subcommand name if its destination was set + if action.dest != argparse.SUPPRESS: + parent_tokens[action.dest] = [token] + + completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app, + parent_tokens=parent_tokens) return completer.complete_command(tokens[token_index:], text, line, begidx, endidx) else: # Invalid subcommand entered, so no way to complete remaining tokens @@ -439,7 +455,7 @@ class AutoCompleter(object): def _complete_for_arg(self, arg_action: argparse.Action, text: str, line: str, begidx: int, endidx: int, - consumed_arg_values: Dict[argparse.Action, List[str]]) -> List[str]: + consumed_arg_values: Dict[str, List[str]]) -> List[str]: """Tab completion routine for an argparse argument""" # Check if the arg provides choices to the user if arg_action.choices is not None: @@ -457,18 +473,15 @@ class AutoCompleter(object): if arg_choices.is_method: args.append(self._cmd2_app) - # If arg_choices.to_call accepts an argument called arg_tokens, then convert - # consumed_arg_values into an argparse Namespace and pass it to the function + # Check if arg_choices.to_call expects arg_tokens to_call_params = inspect.signature(arg_choices.to_call).parameters if ARG_TOKENS in to_call_params: - arg_tokens = argparse.Namespace() - for action, tokens in consumed_arg_values.items(): - setattr(arg_tokens, action.dest, tokens) + # Merge self._parent_tokens and consumed_arg_values + arg_tokens = {**self._parent_tokens, **consumed_arg_values} - # Include the token being completed in the Namespace - tokens = getattr(arg_tokens, arg_action.dest, []) - tokens.append(text) - setattr(arg_tokens, arg_action.dest, tokens) + # Include the token being completed + arg_tokens.setdefault(arg_action.dest, []) + arg_tokens[arg_action.dest].append(text) # Add the namespace to the keyword arguments for the function we are calling kwargs[ARG_TOKENS] = arg_tokens @@ -498,7 +511,7 @@ class AutoCompleter(object): arg_choices[index] = str(choice) # Filter out arguments we already used - used_values = consumed_arg_values.get(arg_action, []) + used_values = consumed_arg_values.get(arg_action.dest, []) arg_choices = [choice for choice in arg_choices if choice not in used_values] # Do tab completion on the choices diff --git a/cmd2/argparse_custom.py b/cmd2/argparse_custom.py index 59a00f4c..940d6064 100644 --- a/cmd2/argparse_custom.py +++ b/cmd2/argparse_custom.py @@ -103,17 +103,17 @@ Tab Completion: set_completer_method(action, method) There are times when what's being tab completed is determined by a previous argument on the command line. - In theses cases, Autocompleter can pass an argparse Namespace that maps the command line tokens up through the - one being completed to their argparse argument. To receive this Namespace, your choices/completer function + In theses cases, Autocompleter can pass a dictionary that maps the command line tokens up through the one + being completed to their argparse argument name. To receive this dictionary, your choices/completer function should have an argument called arg_tokens. Example: def my_choices_method(self, arg_tokens) def my_completer_method(self, text, line, begidx, endidx, arg_tokens) - All members of the arg_tokens Namespace are lists, even if a particular argument expects only 1 token. Since + All values of the arg_tokens dictionary are lists, even if a particular argument expects only 1 token. Since AutoCompleter is for tab completion, it does not convert the tokens to their actual argument types or validate - their values. All tokens are stored in the Namespace as the raw strings provided on the command line. It is up to + their values. All tokens are stored in the dictionary as the raw strings provided on the command line. It is up to the developer to determine if the user entered the correct argument type (e.g. int) and validate their values. CompletionItem Class: diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index e001e75e..2cc412a9 100755 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -2669,11 +2669,11 @@ class Cmd(cmd.Cmd): return utils.basic_complete(text, line, begidx, endidx, strs_to_match) def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: int, - arg_tokens: argparse.Namespace) -> List[str]: + arg_tokens: Dict[str, List[str]]) -> List[str]: """Completes the subcommand argument of help""" # Make sure we have a command whose subcommands we will complete - command = arg_tokens.command[0] + command = arg_tokens['command'][0] if not command: return [] @@ -2684,7 +2684,7 @@ class Cmd(cmd.Cmd): return [] # Combine the command and its subcommand tokens for the AutoCompleter - tokens = [command] + arg_tokens.subcommand + tokens = [command] + arg_tokens['subcommand'] from .argparse_completer import AutoCompleter completer = AutoCompleter(argparser, self) diff --git a/tests/test_argparse_completer.py b/tests/test_argparse_completer.py index c1392676..c7a7c0db 100644 --- a/tests/test_argparse_completer.py +++ b/tests/test_argparse_completer.py @@ -44,18 +44,18 @@ def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[s return basic_complete(text, line, begidx, endidx, completions_from_function) -def choices_takes_namespace(arg_tokens: argparse.Namespace) -> List[str]: +def choices_takes_arg_tokens(arg_tokens: argparse.Namespace) -> List[str]: """Choices function that receives arg_tokens from AutoCompleter""" - if arg_tokens.set_pos[0] == 'set1': + if arg_tokens['set_pos'][0] == 'set1': return set_one_choices else: return set_two_choices -def completer_takes_namespace(text: str, line: str, begidx: int, endidx: int, - arg_tokens: argparse.Namespace) -> List[str]: +def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int, + arg_tokens: argparse.Namespace) -> List[str]: """Completer function that receives arg_tokens from AutoCompleter""" - if arg_tokens.set_pos[0] == 'set1': + if arg_tokens['set_pos'][0] == 'set1': match_against = set_one_choices else: match_against = set_two_choices @@ -253,8 +253,8 @@ class AutoCompleteTester(cmd2.Cmd): ############################################################################################################ arg_tokens_parser = Cmd2ArgumentParser() arg_tokens_parser.add_argument('set_pos', help='determines what will be tab completed') - arg_tokens_parser.add_argument('choices_pos', choices_function=choices_takes_namespace) - arg_tokens_parser.add_argument('completer_pos', completer_function=completer_takes_namespace) + arg_tokens_parser.add_argument('choices_pos', choices_function=choices_takes_arg_tokens) + arg_tokens_parser.add_argument('completer_pos', completer_function=completer_takes_arg_tokens) @with_argparser(arg_tokens_parser) def do_arg_tokens(self, args: argparse.Namespace) -> None: @@ -754,11 +754,11 @@ Hint: @pytest.mark.parametrize('command_and_args, completions', [ - # Exercise a choices function that receives arg_tokens Namespace + # Exercise a choices function that receives arg_tokens dictionary ('arg_tokens set1', set_one_choices), ('arg_tokens set2', set_two_choices), - # Exercise a completer that receives arg_tokens Namespace + # Exercise a completer that receives arg_tokens dictionary ('arg_tokens set1 fake', set_one_choices), ('arg_tokens set2 fake', set_two_choices), ]) |