Skip to content

Commit 9d30993

Browse files
committed
Now maintains a command->CommandSet mapping and passes the CommandSet
through to the ArgparseCompleter if one is registered. For subcommands, the registered argparse instance for the subcommand is now tagged with the CommandSet from which it originated. If a CommandSet is detected, it's now passed in as 'self' for the completion functions. Fixes some issue found with removing a subcommand. Adds additional tests. Added a check to prevent removal of a CommandSet if it has commands with sub-commands from another CommandSet bound to it. Documentation improvements. Standardized around using CommandSetRegistrationException during commandset install/uninstall related errors. Added support for nested sub-command injection.
1 parent c854ba8 commit 9d30993

File tree

7 files changed

+539
-80
lines changed

7 files changed

+539
-80
lines changed

cmd2/argparse_completer.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CompletionItem,
2424
generate_range_error,
2525
)
26+
from .command_definition import CommandSet
2627
from .table_creator import Column, SimpleTable
2728
from .utils import CompletionError, basic_complete
2829

@@ -181,7 +182,8 @@ def __init__(self, parser: argparse.ArgumentParser, cmd2_app: cmd2.Cmd, *,
181182
if isinstance(action, argparse._SubParsersAction):
182183
self._subcommand_action = action
183184

184-
def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int) -> List[str]:
185+
def complete_command(self, tokens: List[str], text: str, line: str, begidx: int, endidx: int, *,
186+
cmd_set: Optional[CommandSet] = None) -> List[str]:
185187
"""
186188
Complete the command using the argparse metadata and provided argument dictionary
187189
:raises: CompletionError for various types of tab completion errors
@@ -358,7 +360,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
358360

359361
completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app,
360362
parent_tokens=parent_tokens)
361-
return completer.complete_command(tokens[token_index:], text, line, begidx, endidx)
363+
return completer.complete_command(tokens[token_index:], text, line, begidx, endidx,
364+
cmd_set=cmd_set)
362365
else:
363366
# Invalid subcommand entered, so no way to complete remaining tokens
364367
return []
@@ -403,7 +406,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
403406
# Check if we are completing a flag's argument
404407
if flag_arg_state is not None:
405408
completion_results = self._complete_for_arg(flag_arg_state.action, text, line,
406-
begidx, endidx, consumed_arg_values)
409+
begidx, endidx, consumed_arg_values,
410+
cmd_set=cmd_set)
407411

408412
# If we have results, then return them
409413
if completion_results:
@@ -423,7 +427,8 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
423427
pos_arg_state = _ArgumentState(action)
424428

425429
completion_results = self._complete_for_arg(pos_arg_state.action, text, line,
426-
begidx, endidx, consumed_arg_values)
430+
begidx, endidx, consumed_arg_values,
431+
cmd_set=cmd_set)
427432

428433
# If we have results, then return them
429434
if completion_results:
@@ -543,7 +548,8 @@ def format_help(self, tokens: List[str]) -> str:
543548

544549
def _complete_for_arg(self, arg_action: argparse.Action,
545550
text: str, line: str, begidx: int, endidx: int,
546-
consumed_arg_values: Dict[str, List[str]]) -> List[str]:
551+
consumed_arg_values: Dict[str, List[str]], *,
552+
cmd_set: Optional[CommandSet] = None) -> List[str]:
547553
"""
548554
Tab completion routine for an argparse argument
549555
:return: list of completions
@@ -563,6 +569,12 @@ def _complete_for_arg(self, arg_action: argparse.Action,
563569
kwargs = {}
564570
if isinstance(arg_choices, ChoicesCallable):
565571
if arg_choices.is_method:
572+
cmd_set = getattr(self._parser, constants.PARSER_ATTR_COMMANDSET, cmd_set)
573+
if cmd_set is not None:
574+
if isinstance(cmd_set, CommandSet):
575+
# If command is part of a CommandSet, `self` should be the CommandSet and Cmd will be next
576+
if cmd_set is not None:
577+
args.append(cmd_set)
566578
args.append(self._cmd2_app)
567579

568580
# Check if arg_choices.to_call expects arg_tokens

cmd2/argparse_custom.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,32 @@ def my_choices_function():
6060
6161
parser.add_argument('-o', '--options', choices_function=my_choices_function)
6262
63-
``choices_method`` - this is exactly like choices_function, but the function
64-
needs to be an instance method of a cmd2-based class. When ArgparseCompleter
65-
calls the method, it will pass the app instance as the self argument. This is
66-
good in cases where the list of choices being generated relies on state data of
67-
the cmd2-based app
68-
69-
Example::
63+
``choices_method`` - this is equivalent to choices_function, but the function
64+
needs to be an instance method of a cmd2.Cmd or cmd2.CommandSet subclass. When
65+
ArgparseCompleter calls the method, it well detect whether is is bound to a
66+
CommandSet or Cmd subclass.
67+
If bound to a cmd2.Cmd subclass, it will pass the app instance as the `self`
68+
argument. This is good in cases where the list of choices being generated
69+
relies on state data of the cmd2-based app.
70+
If bound to a cmd2.CommandSet subclass, it will pass the CommandSet instance
71+
as the `self` argument, and the app instance as the positional argument.
72+
73+
Example bound to cmd2.Cmd::
7074
7175
def my_choices_method(self):
7276
...
7377
return my_generated_list
7478
79+
parser.add_argument("arg", choices_method=my_choices_method)
80+
81+
Example bound to cmd2.CommandSEt::
82+
83+
def my_choices_method(self, app: cmd2.Cmd):
84+
...
85+
return my_generated_list
86+
87+
parser.add_argument("arg", choices_method=my_choices_method)
88+
7589
``completer_function`` - pass a tab completion function that does custom
7690
completion. Since custom tab completion operations commonly need to modify
7791
cmd2's instance variables related to tab completion, it will be rare to need a
@@ -84,10 +98,16 @@ def my_completer_function(text, line, begidx, endidx):
8498
return completions
8599
parser.add_argument('-o', '--options', completer_function=my_completer_function)
86100
87-
``completer_method`` - this is exactly like completer_function, but the
88-
function needs to be an instance method of a cmd2-based class. When
89-
ArgparseCompleter calls the method, it will pass the app instance as the self
90-
argument. cmd2 provides a few completer methods for convenience (e.g.,
101+
``completer_method`` - this is equivalent to completer_function, but the function
102+
needs to be an instance method of a cmd2.Cmd or cmd2.CommandSet subclass. When
103+
ArgparseCompleter calls the method, it well detect whether is is bound to a
104+
CommandSet or Cmd subclass.
105+
If bound to a cmd2.Cmd subclass, it will pass the app instance as the `self`
106+
argument. This is good in cases where the list of choices being generated
107+
relies on state data of the cmd2-based app.
108+
If bound to a cmd2.CommandSet subclass, it will pass the CommandSet instance
109+
as the `self` argument, and the app instance as the positional argument.
110+
cmd2 provides a few completer methods for convenience (e.g.,
91111
path_complete, delimiter_complete)
92112
93113
Example::
@@ -560,6 +580,10 @@ class so cmd2 can remove subcommands from a parser.
560580
for name in to_remove:
561581
del self._name_parser_map[name]
562582

583+
if name in self.choices:
584+
del self.choices[name]
585+
586+
563587

564588
# noinspection PyProtectedMember
565589
setattr(argparse._SubParsersAction, 'remove_parser', _SubParsersAction_remove_parser)

0 commit comments

Comments
 (0)