summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd2/argparse_completer.py43
-rw-r--r--cmd2/argparse_custom.py51
-rwxr-xr-xcmd2/cmd2.py46
-rwxr-xr-xtests/test_completion.py16
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):