Skip to content

Commit bf558c5

Browse files
committed
Refactored custom ArgparseCompleter functionality so they will now be set using methods on ArgumentParser objects.
This fixes issue where subcommands did not use the correct custom ArgparseCompleter type.
1 parent 30b30cd commit bf558c5

File tree

10 files changed

+131
-61
lines changed

10 files changed

+131
-61
lines changed

CHANGELOG.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@
22
* Bug Fixes
33
* Fixed extra space appended to each alias by "alias list" command
44
* Enhancements
5-
* New function `set_default_command_completer_type()` allows developer to extend and modify the
6-
behavior of `ArgparseCompleter`.
5+
* New function `set_default_ap_completer_type()` allows developer to extend and modify the
6+
behavior of `ArgparseCompleter`.
7+
* Added `ArgumentParser.get_ap_completer_type()` and `ArgumentParser.set_ap_completer_type()`. These
8+
methods allow developers to enable custom tab completion behavior for a given parser by using a custom
9+
`ArgparseCompleter`-based class.
710
* New function `register_argparse_argument_parameter()` allows developers to specify custom
811
parameters to be passed to the argparse parser's `add_argument()` method. These parameters will
912
become accessible in the resulting argparse Action object when modifying `ArgparseCompleter` behavior.

cmd2/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
Cmd2AttributeWrapper,
2626
CompletionItem,
2727
register_argparse_argument_parameter,
28-
set_default_argument_parser,
28+
set_default_argument_parser_type,
2929
)
3030

3131
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER.
@@ -38,7 +38,7 @@
3838

3939
importlib.import_module(cmd2_parser_module)
4040

41-
from .argparse_completer import set_default_command_completer_type
41+
from .argparse_completer import set_default_ap_completer_type
4242

4343
from .cmd2 import Cmd
4444
from .command_definition import CommandSet, with_default_category
@@ -63,8 +63,8 @@
6363
'Cmd2AttributeWrapper',
6464
'CompletionItem',
6565
'register_argparse_argument_parameter',
66-
'set_default_argument_parser',
67-
'set_default_command_completer_type',
66+
'set_default_argument_parser_type',
67+
'set_default_ap_completer_type',
6868
# Cmd2
6969
'Cmd',
7070
'CommandResult',

cmd2/argparse_completer.py

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,15 @@ def update_mutex_groups(arg_action: argparse.Action) -> None:
407407
if action.dest != argparse.SUPPRESS:
408408
parent_tokens[action.dest] = [token]
409409

410-
completer = ArgparseCompleter(
411-
self._subcommand_action.choices[token], self._cmd2_app, parent_tokens=parent_tokens
412-
)
410+
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
411+
completer_type: Optional[
412+
Type[ArgparseCompleter]
413+
] = parser.get_ap_completer_type() # type: ignore[attr-defined]
414+
if completer_type is None:
415+
completer_type = DEFAULT_AP_COMPLETER
416+
417+
completer = completer_type(parser, self._cmd2_app, parent_tokens=parent_tokens)
418+
413419
return completer.complete(
414420
text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set
415421
)
@@ -609,7 +615,14 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in
609615
if self._subcommand_action is not None:
610616
for token_index, token in enumerate(tokens):
611617
if token in self._subcommand_action.choices:
612-
completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app)
618+
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
619+
completer_type: Optional[
620+
Type[ArgparseCompleter]
621+
] = parser.get_ap_completer_type() # type: ignore[attr-defined]
622+
if completer_type is None:
623+
completer_type = DEFAULT_AP_COMPLETER
624+
625+
completer = completer_type(parser, self._cmd2_app)
613626
return completer.complete_subcommand_help(text, line, begidx, endidx, tokens[token_index + 1 :])
614627
elif token_index == len(tokens) - 1:
615628
# Since this is the last token, we will attempt to complete it
@@ -629,7 +642,14 @@ def format_help(self, tokens: List[str]) -> str:
629642
if self._subcommand_action is not None:
630643
for token_index, token in enumerate(tokens):
631644
if token in self._subcommand_action.choices:
632-
completer = ArgparseCompleter(self._subcommand_action.choices[token], self._cmd2_app)
645+
parser: argparse.ArgumentParser = self._subcommand_action.choices[token]
646+
completer_type: Optional[
647+
Type[ArgparseCompleter]
648+
] = parser.get_ap_completer_type() # type: ignore[attr-defined]
649+
if completer_type is None:
650+
completer_type = DEFAULT_AP_COMPLETER
651+
652+
completer = completer_type(parser, self._cmd2_app)
633653
return completer.format_help(tokens[token_index + 1 :])
634654
else:
635655
break
@@ -740,14 +760,15 @@ def _complete_arg(
740760
return self._format_completions(arg_state, results)
741761

742762

743-
DEFAULT_COMMAND_COMPLETER: Type[ArgparseCompleter] = ArgparseCompleter
763+
# The default ArgparseCompleter class for a cmd2 app
764+
DEFAULT_AP_COMPLETER: Type[ArgparseCompleter] = ArgparseCompleter
744765

745766

746-
def set_default_command_completer_type(completer_type: Type[ArgparseCompleter]) -> None:
767+
def set_default_ap_completer_type(completer_type: Type[ArgparseCompleter]) -> None:
747768
"""
748-
Set the default command completer type. It must be a sub-class of the ArgparseCompleter.
769+
Set the default ArgparseCompleter class for a cmd2 app.
749770
750771
:param completer_type: Type that is a subclass of ArgparseCompleter.
751772
"""
752-
global DEFAULT_COMMAND_COMPLETER
753-
DEFAULT_COMMAND_COMPLETER = completer_type
773+
global DEFAULT_AP_COMPLETER
774+
DEFAULT_AP_COMPLETER = completer_type

cmd2/argparse_custom.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
207207
- ``argparse.Action.set_suppress_tab_hint()`` - See
208208
:func:`_action_set_suppress_tab_hint` for more details.
209209
210+
cmd2 has patched ``argparse.ArgumentParser`` to include the following accessor methods
211+
212+
- ``argparse.ArgumentParser.get_ap_completer_type()`` - See
213+
:func:`_ArgumentParser_get_ap_completer_type` for more details.
214+
- ``argparse.Action.set_ap_completer_type()`` - See
215+
:func:`_ArgumentParser_set_ap_completer_type` for more details.
216+
210217
**Subcommand removal**
211218
212219
cmd2 has patched ``argparse._SubParsersAction`` to include a ``remove_parser()``
@@ -232,6 +239,7 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
232239
)
233240
from typing import (
234241
IO,
242+
TYPE_CHECKING,
235243
Any,
236244
Callable,
237245
Dict,
@@ -264,6 +272,12 @@ def my_completer(self, text, line, begidx, endidx, arg_tokens)
264272
)
265273

266274

275+
if TYPE_CHECKING: # pragma: no cover
276+
from .argparse_completer import (
277+
ArgparseCompleter,
278+
)
279+
280+
267281
def generate_range_error(range_min: int, range_max: Union[int, float]) -> str:
268282
"""Generate an error message when the the number of arguments provided is not within the expected range"""
269283
err_str = "expected "
@@ -659,6 +673,7 @@ def register_argparse_argument_parameter(param_name: str, param_type: Optional[T
659673
and ``set_{param_name}(value)``.
660674
661675
:param param_name: Name of the parameter to add.
676+
:param param_type: Type of the parameter to add.
662677
"""
663678
attr_name = f'{_CUSTOM_ATTRIB_PFX}{param_name}'
664679
if param_name in CUSTOM_ACTION_ATTRIBS or hasattr(argparse.Action, attr_name):
@@ -715,6 +730,7 @@ def _action_set_custom_parameter(self: argparse.Action, value: Any) -> None:
715730
orig_actions_container_add_argument = argparse._ActionsContainer.add_argument
716731

717732

733+
# noinspection PyProtectedMember
718734
def _add_argument_wrapper(
719735
self: argparse._ActionsContainer,
720736
*args: Any,
@@ -916,10 +932,54 @@ def _match_argument_wrapper(self: argparse.ArgumentParser, action: argparse.Acti
916932

917933

918934
############################################################################################################
919-
# Patch argparse._SubParsersAction to add remove_parser function
935+
# Patch argparse.ArgumentParser with accessors for ap_completer_type attribute
920936
############################################################################################################
921937

938+
# An ArgumentParser attribute which specifies a subclass of ArgparseCompleter for custom tab completion behavior on a
939+
# given parser. If this is None or not present, then cmd2 will use argparse_completer.DEFAULT_AP_COMPLETER when tab
940+
# completing a parser's arguments
941+
ATTR_AP_COMPLETER_TYPE = 'ap_completer_type'
942+
943+
922944
# noinspection PyPep8Naming
945+
def _ArgumentParser_get_ap_completer_type(self: argparse.ArgumentParser) -> Optional[Type['ArgparseCompleter']]:
946+
"""
947+
Get the ap_completer_type attribute of an argparse ArgumentParser.
948+
949+
This function is added by cmd2 as a method called ``get_ap_completer_type()`` to ``argparse.ArgumentParser`` class.
950+
951+
To call: ``parser.get_ap_completer_type()``
952+
953+
:param self: ArgumentParser being queried
954+
:return: An ArgparseCompleter-based class or None if attribute does not exist
955+
"""
956+
return cast(Optional[Type['ArgparseCompleter']], getattr(self, ATTR_AP_COMPLETER_TYPE, None))
957+
958+
959+
setattr(argparse.ArgumentParser, 'get_ap_completer_type', _ArgumentParser_get_ap_completer_type)
960+
961+
962+
# noinspection PyPep8Naming
963+
def _ArgumentParser_set_ap_completer_type(self: argparse.ArgumentParser, ap_completer_type: Type['ArgparseCompleter']) -> None:
964+
"""
965+
Set the ap_completer_type attribute of an argparse ArgumentParser.
966+
967+
This function is added by cmd2 as a method called ``set_ap_completer_type()`` to ``argparse.ArgumentParser`` class.
968+
969+
:param self: ArgumentParser being edited
970+
:param ap_completer_type: the custom ArgparseCompleter-based class to use when tab completing arguments for this parser
971+
"""
972+
setattr(self, ATTR_AP_COMPLETER_TYPE, ap_completer_type)
973+
974+
975+
setattr(argparse.ArgumentParser, 'set_ap_completer_type', _ArgumentParser_set_ap_completer_type)
976+
977+
978+
############################################################################################################
979+
# Patch argparse._SubParsersAction to add remove_parser function
980+
############################################################################################################
981+
982+
# noinspection PyPep8Naming,PyProtectedMember
923983
def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str) -> None:
924984
"""
925985
Removes a sub-parser from a sub-parsers group. Used to remove subcommands from a parser.
@@ -964,6 +1024,7 @@ def _SubParsersAction_remove_parser(self: argparse._SubParsersAction, name: str)
9641024
class Cmd2HelpFormatter(argparse.RawTextHelpFormatter):
9651025
"""Custom help formatter to configure ordering of help text"""
9661026

1027+
# noinspection PyProtectedMember
9671028
def _format_usage(
9681029
self,
9691030
usage: Optional[str],
@@ -1207,6 +1268,7 @@ def __init__(
12071268
allow_abbrev=allow_abbrev,
12081269
)
12091270

1271+
# noinspection PyProtectedMember
12101272
def add_subparsers(self, **kwargs: Any) -> argparse._SubParsersAction:
12111273
"""
12121274
Custom override. Sets a default title if one was not given.
@@ -1321,10 +1383,10 @@ def set(self, new_val: Any) -> None:
13211383
DEFAULT_ARGUMENT_PARSER: Type[argparse.ArgumentParser] = Cmd2ArgumentParser
13221384

13231385

1324-
def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None:
1386+
def set_default_argument_parser_type(parser_type: Type[argparse.ArgumentParser]) -> None:
13251387
"""
13261388
Set the default ArgumentParser class for a cmd2 app. This must be called prior to loading cmd2.py if
13271389
you want to override the parser for cmd2's built-in commands. See examples/override_parser.py.
13281390
"""
13291391
global DEFAULT_ARGUMENT_PARSER
1330-
DEFAULT_ARGUMENT_PARSER = parser
1392+
DEFAULT_ARGUMENT_PARSER = parser_type

cmd2/cmd2.py

Lines changed: 21 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,9 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: List[str]) ->
860860
defaults = {constants.NS_ATTR_SUBCMD_HANDLER: method}
861861
attached_parser.set_defaults(**defaults)
862862

863+
# Copy value for custom ArgparseCompleter type, which will be None if not present on subcmd_parser
864+
attached_parser.set_ap_completer_type(subcmd_parser.get_ap_completer_type()) # type: ignore[attr-defined]
865+
863866
# Set what instance the handler is bound to
864867
setattr(attached_parser, constants.PARSER_ATTR_COMMANDSET, cmdset)
865868
break
@@ -1850,10 +1853,6 @@ def _perform_completion(
18501853
:param endidx: the ending index of the prefix text
18511854
:param custom_settings: optional prepopulated completion settings
18521855
"""
1853-
from .argparse_completer import (
1854-
ArgparseCompleter,
1855-
)
1856-
18571856
# If custom_settings is None, then we are completing a command's argument.
18581857
# Parse the command line to get the command token.
18591858
command = ''
@@ -1903,18 +1902,18 @@ def _perform_completion(
19031902
else:
19041903
# There's no completer function, next see if the command uses argparse
19051904
func = self.cmd_func(command)
1906-
argparser = getattr(func, constants.CMD_ATTR_ARGPARSER, None)
1907-
completer_type = getattr(func, constants.CMD_ATTR_COMPLETER, argparse_completer.DEFAULT_COMMAND_COMPLETER)
1908-
if completer_type is None:
1909-
completer_type = argparse_completer.DEFAULT_COMMAND_COMPLETER
1905+
argparser: Optional[argparse.ArgumentParser] = getattr(func, constants.CMD_ATTR_ARGPARSER, None)
19101906

19111907
if func is not None and argparser is not None:
1912-
cmd_set = self._cmd_to_command_sets[command] if command in self._cmd_to_command_sets else None
1913-
if completer_type is not None:
1914-
completer = completer_type(argparser, self)
1915-
else:
1916-
completer = ArgparseCompleter(argparser, self)
1908+
# Get arguments for complete()
19171909
preserve_quotes = getattr(func, constants.CMD_ATTR_PRESERVE_QUOTES)
1910+
cmd_set = self._cmd_to_command_sets[command] if command in self._cmd_to_command_sets else None
1911+
1912+
# Create the argparse completer
1913+
completer_type = argparser.get_ap_completer_type() # type: ignore[attr-defined]
1914+
if completer_type is None:
1915+
completer_type = argparse_completer.DEFAULT_AP_COMPLETER
1916+
completer = completer_type(argparser, self)
19181917

19191918
completer_func = functools.partial(
19201919
completer.complete, tokens=raw_tokens[1:] if preserve_quotes else tokens[1:], cmd_set=cmd_set
@@ -1932,7 +1931,12 @@ def _perform_completion(
19321931

19331932
# Otherwise we are completing the command token or performing custom completion
19341933
else:
1935-
completer = ArgparseCompleter(custom_settings.parser, self)
1934+
# Create the argparse completer
1935+
completer_type = custom_settings.parser.get_ap_completer_type() # type: ignore[attr-defined]
1936+
if completer_type is None:
1937+
completer_type = argparse_completer.DEFAULT_AP_COMPLETER
1938+
completer = completer_type(custom_settings.parser, self)
1939+
19361940
completer_func = functools.partial(
19371941
completer.complete, tokens=raw_tokens if custom_settings.preserve_quotes else tokens, cmd_set=None
19381942
)
@@ -3542,11 +3546,7 @@ def complete_help_subcommands(
35423546
if func is None or argparser is None:
35433547
return []
35443548

3545-
from .argparse_completer import (
3546-
ArgparseCompleter,
3547-
)
3548-
3549-
completer = ArgparseCompleter(argparser, self)
3549+
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
35503550
return completer.complete_subcommand_help(text, line, begidx, endidx, arg_tokens['subcommands'])
35513551

35523552
help_parser = argparse_custom.DEFAULT_ARGUMENT_PARSER(
@@ -3582,11 +3582,7 @@ def do_help(self, args: argparse.Namespace) -> None:
35823582

35833583
# If the command function uses argparse, then use argparse's help
35843584
if func is not None and argparser is not None:
3585-
from .argparse_completer import (
3586-
ArgparseCompleter,
3587-
)
3588-
3589-
completer = ArgparseCompleter(argparser, self)
3585+
completer = argparse_completer.DEFAULT_AP_COMPLETER(argparser, self)
35903586

35913587
# Set end to blank so the help output matches how it looks when "command -h" is used
35923588
self.poutput(completer.format_help(args.subcommands), end='')
@@ -3918,11 +3914,7 @@ def complete_set_value(
39183914
completer=settable.completer,
39193915
)
39203916

3921-
from .argparse_completer import (
3922-
ArgparseCompleter,
3923-
)
3924-
3925-
completer = ArgparseCompleter(settable_parser, self)
3917+
completer = argparse_completer.DEFAULT_AP_COMPLETER(settable_parser, self)
39263918

39273919
# Use raw_tokens since quotes have been preserved
39283920
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)

cmd2/constants.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343

4444
# The argparse parser for the command
4545
CMD_ATTR_ARGPARSER = 'argparser'
46-
CMD_ATTR_COMPLETER = 'command_completer'
4746

4847
# Whether or not tokens are unquoted before sending to argparse
4948
CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes'

0 commit comments

Comments
 (0)