Skip to content

Commit d404476

Browse files
authored
Refactor argparse custom (#1621)
* Require use of Cmd2ArgumentParser-based parsers. * Moved previously-patched functions into Cmd2ArgumentParser. * Simplified creation of cmd2-specific argparse.Action attributes. * Made common prefix for all private cmd2 attributes.
1 parent b726c52 commit d404476

17 files changed

+424
-650
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ prompt is displayed.
6464
before calling it like the previous functions did.
6565
- Removed `Cmd.default_to_shell`.
6666
- Removed `Cmd.ruler` since `cmd2` no longer uses it.
67+
- All parsers used with `cmd2` commands must be an instance of `Cmd2ArgumentParser` or a child
68+
class of it.
69+
- Removed `set_ap_completer_type()` and `get_ap_completer_type()` since `ap_completer_type` is
70+
now a public member of `Cmd2ArgumentParser`.
6771
- Enhancements
6872
- New `cmd2.Cmd` parameters
6973
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These

cmd2/argparse_completer.py

Lines changed: 67 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,11 @@
2222
cast,
2323
)
2424

25-
from rich.text import Text
26-
27-
from .constants import INFINITY
28-
from .rich_utils import Cmd2SimpleTable
29-
30-
if TYPE_CHECKING: # pragma: no cover
31-
from .cmd2 import Cmd
32-
3325
from rich.table import Column
26+
from rich.text import Text
3427

3528
from .argparse_custom import (
36-
ChoicesCallable,
29+
Cmd2ArgumentParser,
3730
generate_range_error,
3831
)
3932
from .command_definition import CommandSet
@@ -42,14 +35,25 @@
4235
Completions,
4336
all_display_numeric,
4437
)
38+
from .constants import INFINITY
4539
from .exceptions import CompletionError
40+
from .rich_utils import Cmd2SimpleTable
41+
from .types import (
42+
ChoicesProviderUnbound,
43+
CmdOrSet,
44+
CompleterUnbound,
45+
)
46+
47+
if TYPE_CHECKING: # pragma: no cover
48+
from .cmd2 import Cmd
49+
4650

4751
# Name of the choice/completer function argument that, if present, will be passed a dictionary of
4852
# command line tokens up through the token being completed mapped to their argparse destination name.
4953
ARG_TOKENS = 'arg_tokens'
5054

5155

52-
def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str:
56+
def _build_hint(parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> str:
5357
"""Build completion hint for a given argument."""
5458
# Check if hinting is disabled for this argument
5559
suppress_hint = arg_action.get_suppress_tab_hint() # type: ignore[attr-defined]
@@ -64,12 +68,12 @@ def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) ->
6468
return formatter.format_help()
6569

6670

67-
def _single_prefix_char(token: str, parser: argparse.ArgumentParser) -> bool:
71+
def _single_prefix_char(token: str, parser: Cmd2ArgumentParser) -> bool:
6872
"""Is a token just a single flag prefix character."""
6973
return len(token) == 1 and token[0] in parser.prefix_chars
7074

7175

72-
def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
76+
def _looks_like_flag(token: str, parser: Cmd2ArgumentParser) -> bool:
7377
"""Determine if a token looks like a flag.
7478
7579
Unless an argument has nargs set to argparse.REMAINDER, then anything that looks like a flag
@@ -140,12 +144,12 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None:
140144

141145

142146
class _NoResultsError(CompletionError):
143-
def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None:
147+
def __init__(self, parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> None:
144148
"""CompletionError which occurs when there are no results.
145149
146150
If hinting is allowed on this argument, then its hint text will display.
147151
148-
:param parser: ArgumentParser instance which owns the action being completed
152+
:param parser: Cmd2ArgumentParser instance which owns the action being completed
149153
:param arg_action: action being completed.
150154
"""
151155
# Set apply_style to False because we don't want hints to look like errors
@@ -157,14 +161,14 @@ class ArgparseCompleter:
157161

158162
def __init__(
159163
self,
160-
parser: argparse.ArgumentParser,
164+
parser: Cmd2ArgumentParser,
161165
cmd2_app: 'Cmd',
162166
*,
163167
parent_tokens: Mapping[str, MutableSequence[str]] | None = None,
164168
) -> None:
165169
"""Create an ArgparseCompleter.
166170
167-
:param parser: ArgumentParser instance
171+
:param parser: Cmd2ArgumentParser instance
168172
:param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter
169173
:param parent_tokens: optional Mapping of parent parsers' arg names to their tokens
170174
This is only used by ArgparseCompleter when recursing on subcommand parsers
@@ -187,7 +191,7 @@ def __init__(
187191
self._positional_actions: list[argparse.Action] = []
188192

189193
# This will be set if self._parser has subcommands
190-
self._subcommand_action: argparse._SubParsersAction[argparse.ArgumentParser] | None = None
194+
self._subcommand_action: argparse._SubParsersAction[Cmd2ArgumentParser] | None = None
191195

192196
# Start digging through the argparse structures.
193197
# _actions is the top level container of parameter definitions
@@ -707,33 +711,32 @@ def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None
707711
return
708712
self._parser.print_help(file=file)
709713

710-
def _get_raw_choices(self, arg_state: _ArgumentState) -> list[CompletionItem] | ChoicesCallable | None:
711-
"""Extract choices from action or return the choices_callable."""
712-
if arg_state.action.choices is not None:
713-
# If choices are subcommands, then get their help text to populate display_meta.
714-
if isinstance(arg_state.action, argparse._SubParsersAction):
715-
parser_help = {}
716-
for action in arg_state.action._choices_actions:
717-
if action.dest in arg_state.action.choices:
718-
subparser = arg_state.action.choices[action.dest]
719-
parser_help[subparser] = action.help or ''
720-
721-
return [
722-
CompletionItem(name, display_meta=parser_help.get(subparser, ''))
723-
for name, subparser in arg_state.action.choices.items()
724-
]
725-
726-
# Standard choices
714+
def _choices_to_items(self, arg_state: _ArgumentState) -> list[CompletionItem]:
715+
"""Convert choices from action to list of CompletionItems."""
716+
if arg_state.action.choices is None:
717+
return []
718+
719+
# If choices are subcommands, then get their help text to populate display_meta.
720+
if isinstance(arg_state.action, argparse._SubParsersAction):
721+
parser_help = {}
722+
for action in arg_state.action._choices_actions:
723+
if action.dest in arg_state.action.choices:
724+
subparser = arg_state.action.choices[action.dest]
725+
parser_help[subparser] = action.help or ''
726+
727727
return [
728-
choice if isinstance(choice, CompletionItem) else CompletionItem(choice) for choice in arg_state.action.choices
728+
CompletionItem(name, display_meta=parser_help.get(subparser, ''))
729+
for name, subparser in arg_state.action.choices.items()
729730
]
730731

731-
choices_callable: ChoicesCallable | None = arg_state.action.get_choices_callable() # type: ignore[attr-defined]
732-
return choices_callable
732+
# Standard choices
733+
return [
734+
choice if isinstance(choice, CompletionItem) else CompletionItem(choice) for choice in arg_state.action.choices
735+
]
733736

734737
def _prepare_callable_params(
735738
self,
736-
choices_callable: ChoicesCallable,
739+
to_call: ChoicesProviderUnbound[CmdOrSet] | CompleterUnbound[CmdOrSet],
737740
arg_state: _ArgumentState,
738741
text: str,
739742
consumed_arg_values: dict[str, list[str]],
@@ -744,14 +747,14 @@ def _prepare_callable_params(
744747
kwargs: dict[str, Any] = {}
745748

746749
# Resolve the 'self' instance for the method
747-
self_arg = self._cmd2_app._resolve_func_self(choices_callable.to_call, cmd_set)
750+
self_arg = self._cmd2_app._resolve_func_self(to_call, cmd_set)
748751
if self_arg is None:
749-
raise CompletionError("Could not find CommandSet instance matching defining type for completer")
752+
raise CompletionError("Could not find CommandSet instance matching defining type")
750753

751754
args.append(self_arg)
752755

753756
# Check if the function expects 'arg_tokens'
754-
to_call_params = inspect.signature(choices_callable.to_call).parameters
757+
to_call_params = inspect.signature(to_call).parameters
755758
if ARG_TOKENS in to_call_params:
756759
arg_tokens = {**self._parent_tokens, **consumed_arg_values}
757760
arg_tokens.setdefault(arg_state.action.dest, []).append(text)
@@ -775,26 +778,33 @@ def _complete_arg(
775778
:return: a Completions object
776779
:raises CompletionError: if the completer or choices function this calls raises one
777780
"""
778-
raw_choices = self._get_raw_choices(arg_state)
779-
if not raw_choices:
780-
return Completions()
781-
782-
# Check if the argument uses a completer function
783-
if isinstance(raw_choices, ChoicesCallable) and raw_choices.is_completer:
784-
args, kwargs = self._prepare_callable_params(raw_choices, arg_state, text, consumed_arg_values, cmd_set)
781+
# Check if the argument uses a completer
782+
completer = arg_state.action.get_completer() # type: ignore[attr-defined]
783+
if completer is not None:
784+
args, kwargs = self._prepare_callable_params(
785+
completer,
786+
arg_state,
787+
text,
788+
consumed_arg_values,
789+
cmd_set,
790+
)
785791
args.extend([text, line, begidx, endidx])
786-
completions = raw_choices.completer(*args, **kwargs)
792+
completions: Completions = completer(*args, **kwargs)
787793

788-
# Otherwise it uses a choices list or choices provider function
794+
# Otherwise it uses a choices provider or choices list
789795
else:
790-
all_choices: list[CompletionItem] = []
791-
792-
if isinstance(raw_choices, ChoicesCallable):
793-
args, kwargs = self._prepare_callable_params(raw_choices, arg_state, text, consumed_arg_values, cmd_set)
794-
choices_func = raw_choices.choices_provider
795-
all_choices = list(choices_func(*args, **kwargs))
796+
choices_provider = arg_state.action.get_choices_provider() # type: ignore[attr-defined]
797+
if choices_provider is not None:
798+
args, kwargs = self._prepare_callable_params(
799+
choices_provider,
800+
arg_state,
801+
text,
802+
consumed_arg_values,
803+
cmd_set,
804+
)
805+
all_choices = list(choices_provider(*args, **kwargs))
796806
else:
797-
all_choices = raw_choices
807+
all_choices = self._choices_to_items(arg_state)
798808

799809
# Filter used values and run basic completion
800810
used_values = consumed_arg_values.get(arg_state.action.dest, [])

0 commit comments

Comments
 (0)