Skip to content

Commit e6585d1

Browse files
committed
Changed arg_tokens to a dictionary
Including tokens from parent parsers in arg_tokens when subcommands are used
1 parent 9a7818b commit e6585d1

File tree

5 files changed

+52
-39
lines changed

5 files changed

+52
-39
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Fixed a bug when running a cmd2 application on Linux without Gtk libraries installed
55
* Enhancements
66
* No longer treating empty text scripts as an error condition
7-
* Choices/Completer functions can now be passed an `argparse.Namespace` that maps command-line tokens to their
7+
* Choices/Completer functions can now be passed a dictionary that maps command-line tokens to their
88
argparse argument. This is helpful when one argument determines what is tab completed for another argument.
99
If these functions have an argument called `arg_tokens`, then AutoCompleter will automatically pass this
1010
Namespace to them.

cmd2/argparse_completer.py

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import inspect
1111
import numbers
1212
import shutil
13-
from typing import Dict, List, Union
13+
from typing import Dict, List, Optional, Union
1414

1515
from . import cmd2
1616
from . import utils
@@ -22,8 +22,8 @@
2222
# If no descriptive header is supplied, then this will be used instead
2323
DEFAULT_DESCRIPTIVE_HEADER = 'Description'
2424

25-
# Name of the choice/completer function argument that, if present, will be passed a Namespace of
26-
# command line tokens up through the token being completed mapped to their argparse destination.
25+
# Name of the choice/completer function argument that, if present, will be passed a dictionary of
26+
# command line tokens up through the token being completed mapped to their argparse destination name.
2727
ARG_TOKENS = 'arg_tokens'
2828

2929

@@ -97,23 +97,31 @@ def __init__(self, arg_action: argparse.Action) -> None:
9797
self.min = self.action.nargs
9898
self.max = self.action.nargs
9999

100-
def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd) -> None:
100+
def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
101+
parent_tokens: Optional[Dict[str, List[str]]] = None) -> None:
101102
"""
102103
Create an AutoCompleter
103104
104105
:param parser: ArgumentParser instance
105106
:param cmd2_app: reference to the Cmd2 application that owns this AutoCompleter
107+
:param parent_tokens: optional dictionary mapping parent parsers' arg names to their tokens
108+
this is only used by AutoCompleter when recursing on subcommand parsers
109+
Defaults to None
106110
"""
107111
self._parser = parser
108112
self._cmd2_app = cmd2_app
109113

114+
if parent_tokens is None:
115+
parent_tokens = dict()
116+
self._parent_tokens = parent_tokens
117+
110118
self._flags = [] # all flags in this command
111119
self._flag_to_action = {} # maps flags to the argparse action object
112120
self._positional_actions = [] # actions for positional arguments (by position index)
113121
self._subcommand_action = None # this will be set if self._parser has subcommands
114122

115123
# Start digging through the argparse structures.
116-
# _actions is the top level container of parameter definitions
124+
# _actions is the top level container of parameter definitions
117125
for action in self._parser._actions:
118126
# if the parameter is flag based, it will have option_strings
119127
if action.option_strings:
@@ -152,13 +160,13 @@ def complete_command(self, tokens: List[str], text: str, line: str, begidx: int,
152160
matched_flags = []
153161

154162
# Keeps track of arguments we've seen and any tokens they consumed
155-
consumed_arg_values = dict() # dict(action -> tokens)
163+
consumed_arg_values = dict() # dict(arg_name -> List[tokens])
156164

157165
def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
158166
"""Consuming token as an argument"""
159167
arg_state.count += 1
160-
consumed_arg_values.setdefault(arg_state.action, [])
161-
consumed_arg_values[arg_state.action].append(token)
168+
consumed_arg_values.setdefault(arg_state.action.dest, [])
169+
consumed_arg_values[arg_state.action.dest].append(token)
162170

163171
#############################################################################################
164172
# Parse all but the last token
@@ -218,14 +226,14 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
218226
argparse._CountAction)):
219227
# Flags with action set to append, append_const, and count can be reused
220228
# Therefore don't erase any tokens already consumed for this flag
221-
consumed_arg_values.setdefault(action, [])
229+
consumed_arg_values.setdefault(action.dest, [])
222230
else:
223231
# This flag is not resusable, so mark that we've seen it
224232
matched_flags.extend(action.option_strings)
225233

226234
# It's possible we already have consumed values for this flag if it was used
227235
# earlier in the command line. Reset them now for this use of it.
228-
consumed_arg_values[action] = []
236+
consumed_arg_values[action.dest] = []
229237

230238
new_arg_state = AutoCompleter._ArgumentState(action)
231239

@@ -256,7 +264,15 @@ def consume_argument(arg_state: AutoCompleter._ArgumentState) -> None:
256264
# Are we at a subcommand? If so, forward to the matching completer
257265
if action == self._subcommand_action:
258266
if token in self._subcommand_action.choices:
259-
completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app)
267+
# Merge self._parent_tokens and consumed_arg_values
268+
parent_tokens = {**self._parent_tokens, **consumed_arg_values}
269+
270+
# Include the subcommand name if its destination was set
271+
if action.dest != argparse.SUPPRESS:
272+
parent_tokens[action.dest] = [token]
273+
274+
completer = AutoCompleter(self._subcommand_action.choices[token], self._cmd2_app,
275+
parent_tokens=parent_tokens)
260276
return completer.complete_command(tokens[token_index:], text, line, begidx, endidx)
261277
else:
262278
# Invalid subcommand entered, so no way to complete remaining tokens
@@ -439,7 +455,7 @@ def format_help(self, tokens: List[str]) -> str:
439455

440456
def _complete_for_arg(self, arg_action: argparse.Action,
441457
text: str, line: str, begidx: int, endidx: int,
442-
consumed_arg_values: Dict[argparse.Action, List[str]]) -> List[str]:
458+
consumed_arg_values: Dict[str, List[str]]) -> List[str]:
443459
"""Tab completion routine for an argparse argument"""
444460
# Check if the arg provides choices to the user
445461
if arg_action.choices is not None:
@@ -457,18 +473,15 @@ def _complete_for_arg(self, arg_action: argparse.Action,
457473
if arg_choices.is_method:
458474
args.append(self._cmd2_app)
459475

460-
# If arg_choices.to_call accepts an argument called arg_tokens, then convert
461-
# consumed_arg_values into an argparse Namespace and pass it to the function
476+
# Check if arg_choices.to_call expects arg_tokens
462477
to_call_params = inspect.signature(arg_choices.to_call).parameters
463478
if ARG_TOKENS in to_call_params:
464-
arg_tokens = argparse.Namespace()
465-
for action, tokens in consumed_arg_values.items():
466-
setattr(arg_tokens, action.dest, tokens)
479+
# Merge self._parent_tokens and consumed_arg_values
480+
arg_tokens = {**self._parent_tokens, **consumed_arg_values}
467481

468-
# Include the token being completed in the Namespace
469-
tokens = getattr(arg_tokens, arg_action.dest, [])
470-
tokens.append(text)
471-
setattr(arg_tokens, arg_action.dest, tokens)
482+
# Include the token being completed
483+
arg_tokens.setdefault(arg_action.dest, [])
484+
arg_tokens[arg_action.dest].append(text)
472485

473486
# Add the namespace to the keyword arguments for the function we are calling
474487
kwargs[ARG_TOKENS] = arg_tokens
@@ -498,7 +511,7 @@ def _complete_for_arg(self, arg_action: argparse.Action,
498511
arg_choices[index] = str(choice)
499512

500513
# Filter out arguments we already used
501-
used_values = consumed_arg_values.get(arg_action, [])
514+
used_values = consumed_arg_values.get(arg_action.dest, [])
502515
arg_choices = [choice for choice in arg_choices if choice not in used_values]
503516

504517
# Do tab completion on the choices

cmd2/argparse_custom.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,17 @@ def my_completer_function(text, line, begidx, endidx):
103103
set_completer_method(action, method)
104104
105105
There are times when what's being tab completed is determined by a previous argument on the command line.
106-
In theses cases, Autocompleter can pass an argparse Namespace that maps the command line tokens up through the
107-
one being completed to their argparse argument. To receive this Namespace, your choices/completer function
106+
In theses cases, Autocompleter can pass a dictionary that maps the command line tokens up through the one
107+
being completed to their argparse argument name. To receive this dictionary, your choices/completer function
108108
should have an argument called arg_tokens.
109109
110110
Example:
111111
def my_choices_method(self, arg_tokens)
112112
def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
113113
114-
All members of the arg_tokens Namespace are lists, even if a particular argument expects only 1 token. Since
114+
All values of the arg_tokens dictionary are lists, even if a particular argument expects only 1 token. Since
115115
AutoCompleter is for tab completion, it does not convert the tokens to their actual argument types or validate
116-
their values. All tokens are stored in the Namespace as the raw strings provided on the command line. It is up to
116+
their values. All tokens are stored in the dictionary as the raw strings provided on the command line. It is up to
117117
the developer to determine if the user entered the correct argument type (e.g. int) and validate their values.
118118
119119
CompletionItem Class:

cmd2/cmd2.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2669,11 +2669,11 @@ def complete_help_command(self, text: str, line: str, begidx: int, endidx: int)
26692669
return utils.basic_complete(text, line, begidx, endidx, strs_to_match)
26702670

26712671
def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: int,
2672-
arg_tokens: argparse.Namespace) -> List[str]:
2672+
arg_tokens: Dict[str, List[str]]) -> List[str]:
26732673
"""Completes the subcommand argument of help"""
26742674

26752675
# Make sure we have a command whose subcommands we will complete
2676-
command = arg_tokens.command[0]
2676+
command = arg_tokens['command'][0]
26772677
if not command:
26782678
return []
26792679

@@ -2684,7 +2684,7 @@ def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: in
26842684
return []
26852685

26862686
# Combine the command and its subcommand tokens for the AutoCompleter
2687-
tokens = [command] + arg_tokens.subcommand
2687+
tokens = [command] + arg_tokens['subcommand']
26882688

26892689
from .argparse_completer import AutoCompleter
26902690
completer = AutoCompleter(argparser, self)

tests/test_argparse_completer.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,18 +44,18 @@ def completer_function(text: str, line: str, begidx: int, endidx: int) -> List[s
4444
return basic_complete(text, line, begidx, endidx, completions_from_function)
4545

4646

47-
def choices_takes_namespace(arg_tokens: argparse.Namespace) -> List[str]:
47+
def choices_takes_arg_tokens(arg_tokens: argparse.Namespace) -> List[str]:
4848
"""Choices function that receives arg_tokens from AutoCompleter"""
49-
if arg_tokens.set_pos[0] == 'set1':
49+
if arg_tokens['set_pos'][0] == 'set1':
5050
return set_one_choices
5151
else:
5252
return set_two_choices
5353

5454

55-
def completer_takes_namespace(text: str, line: str, begidx: int, endidx: int,
56-
arg_tokens: argparse.Namespace) -> List[str]:
55+
def completer_takes_arg_tokens(text: str, line: str, begidx: int, endidx: int,
56+
arg_tokens: argparse.Namespace) -> List[str]:
5757
"""Completer function that receives arg_tokens from AutoCompleter"""
58-
if arg_tokens.set_pos[0] == 'set1':
58+
if arg_tokens['set_pos'][0] == 'set1':
5959
match_against = set_one_choices
6060
else:
6161
match_against = set_two_choices
@@ -253,8 +253,8 @@ def do_hint(self, args: argparse.Namespace) -> None:
253253
############################################################################################################
254254
arg_tokens_parser = Cmd2ArgumentParser()
255255
arg_tokens_parser.add_argument('set_pos', help='determines what will be tab completed')
256-
arg_tokens_parser.add_argument('choices_pos', choices_function=choices_takes_namespace)
257-
arg_tokens_parser.add_argument('completer_pos', completer_function=completer_takes_namespace)
256+
arg_tokens_parser.add_argument('choices_pos', choices_function=choices_takes_arg_tokens)
257+
arg_tokens_parser.add_argument('completer_pos', completer_function=completer_takes_arg_tokens)
258258

259259
@with_argparser(arg_tokens_parser)
260260
def do_arg_tokens(self, args: argparse.Namespace) -> None:
@@ -754,11 +754,11 @@ def test_autocomp_hint_no_help_text(ac_app, capsys):
754754

755755

756756
@pytest.mark.parametrize('command_and_args, completions', [
757-
# Exercise a choices function that receives arg_tokens Namespace
757+
# Exercise a choices function that receives arg_tokens dictionary
758758
('arg_tokens set1', set_one_choices),
759759
('arg_tokens set2', set_two_choices),
760760
761-
# Exercise a completer that receives arg_tokens Namespace
761+
# Exercise a completer that receives arg_tokens dictionary
762762
('arg_tokens set1 fake', set_one_choices),
763763
('arg_tokens set2 fake', set_two_choices),
764764
])

0 commit comments

Comments
 (0)