Skip to content

Commit 205e8a6

Browse files
authored
Refactor and standardize argparse-related naming and metadata (#1670)
- Consistently use full words COMMAND and SUBCOMMAND over abbreviations - Renamed cmd2_subcmd_handler to cmd2_subcommand_func - Consolidate command metadata into ArgparseCommandSpec dataclass - Simplify subcommand examples by using required=True for subparsers - Renamed set_default_argument_parser_type() to set_default_argument_parser() - Renamed set_default_ap_completer_type() to set_default_argparse_completer() - Renamed Cmd2ArgumentParser.ap_completer_type to completer_class
1 parent 1288fb6 commit 205e8a6

22 files changed

Lines changed: 369 additions & 320 deletions

CHANGELOG.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,14 @@ prompt is displayed.
6767
- Removed `Cmd.ruler` since `cmd2` no longer uses it.
6868
- All parsers used with `cmd2` commands must be an instance of `Cmd2ArgumentParser` or a child
6969
class of it.
70-
- Removed `set_ap_completer_type()` and `get_ap_completer_type()` since `ap_completer_type` is
71-
now a public member of `Cmd2ArgumentParser`.
70+
- Renamed `set_default_argument_parser_type()` to `set_default_argument_parser()`.
71+
- Renamed `set_default_ap_completer_type()` to `set_default_argparse_completer()`.
72+
- Removed `set_ap_completer_type()` and `get_ap_completer_type()` since `completer_class` is now
73+
a public member of `Cmd2ArgumentParser`.
7274
- Moved `set_parser_prog()` to `Cmd2ArgumentParser.update_prog()`.
73-
- Renamed `cmd2_handler` to `cmd2_subcmd_handler` in the `argparse.Namespace` for clarity.
75+
- Renamed `cmd2_handler` to `cmd2_subcommand_func` in the `argparse.Namespace` for clarity.
7476
- Removed `Cmd2AttributeWrapper` class. `argparse.Namespace` objects passed to command functions
75-
now contain direct attributes for `cmd2_statement` and `cmd2_subcmd_handler`.
77+
now contain direct attributes for `cmd2_statement` and `cmd2_subcommand_func`.
7678
- Renamed `cmd2/command_definition.py` to `cmd2/command_set.py`.
7779
- Removed `Cmd.doc_header` and the `with_default_category` decorator. Help categorization is now
7880
driven by the `DEFAULT_CATEGORY` class variable (see **Simplified command categorization** in

cmd2/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
string_utils,
1313
)
1414
from .annotated import with_annotated
15-
from .argparse_completer import set_default_ap_completer_type
15+
from .argparse_completer import set_default_argparse_completer
1616
from .argparse_utils import (
1717
Cmd2ArgumentParser,
1818
SubcommandRecord,
1919
register_argparse_argument_parameter,
20-
set_default_argument_parser_type,
20+
set_default_argument_parser,
2121
)
2222
from .cmd2 import Cmd
2323
from .colors import Color
@@ -75,8 +75,8 @@
7575
"Cmd2ArgumentParser",
7676
"SubcommandRecord",
7777
"register_argparse_argument_parameter",
78-
"set_default_ap_completer_type",
79-
"set_default_argument_parser_type",
78+
"set_default_argparse_completer",
79+
"set_default_argument_parser",
8080
# Cmd2
8181
"Cmd",
8282
"CommandResult",

cmd2/annotated.py

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
flag (``dry_run`` -> ``--dry-run``); pass an explicit ``Option("--my_flag")`` to opt out.
1919
Positional-only parameters (before ``/``) and ``**kwargs`` raise ``TypeError``. The parameter
2020
names ``dest`` and ``subcommand`` are reserved; ``cmd2_statement`` receives the parsed
21-
``Statement`` and (with ``base_command=True``) ``cmd2_handler`` receives the subcommand handler:
21+
``Statement`` and (with ``base_command=True``) ``cmd2_subcommand_func`` receives the subcommand handler:
2222
2323
class MyApp(cmd2.Cmd):
2424
@cmd2.with_annotated
@@ -118,7 +118,7 @@ def do_paint(
118118
692 ``**parser_kwargs: Unpack[Cmd2ParserKwargs]``. Anything the parser ctor accepts -- ``description``,
119119
``epilog``, ``prog``, ``usage``, ``parents``, ``argument_default``, ``prefix_chars``,
120120
``fromfile_prefix_chars``, ``conflict_handler``, ``add_help``, ``allow_abbrev``, ``exit_on_error``,
121-
``formatter_class``, ``ap_completer_type``, and on Python >= 3.14 ``suggest_on_error`` / ``color`` --
121+
``formatter_class``, ``completer_class``, and on Python >= 3.14 ``suggest_on_error`` / ``color`` --
122122
flows straight through; the [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs] ``TypedDict`` is the single source of truth
123123
and gives type-checkers/IDEs autocomplete on the decorator's call site. ``parser_class`` stays as
124124
its own explicit kwarg because it selects the class itself, not a value passed to it. Two
@@ -181,8 +181,16 @@ def do_paint(
181181
import functools
182182
import inspect
183183
import types
184-
from collections.abc import Callable, Container, Iterable, Sequence
185-
from dataclasses import dataclass, field
184+
from collections.abc import (
185+
Callable,
186+
Container,
187+
Iterable,
188+
Sequence,
189+
)
190+
from dataclasses import (
191+
dataclass,
192+
field,
193+
)
186194
from pathlib import Path
187195
from typing import (
188196
TYPE_CHECKING,
@@ -203,12 +211,20 @@ def do_paint(
203211
from rich.table import Column
204212

205213
from . import constants
206-
from .argparse_utils import DEFAULT_ARGUMENT_PARSER, Cmd2ArgumentParser, SubcommandSpec
214+
from .argparse_utils import (
215+
ArgparseCommandSpec,
216+
Cmd2ArgumentParser,
217+
SubcommandSpec,
218+
)
207219
from .completion import CompletionItem
208220
from .decorators import _parse_positionals
209221
from .exceptions import Cmd2ArgparseError
210222
from .rich_utils import Cmd2HelpFormatter, HelpContent
211-
from .types import CmdOrSetT, UnboundChoicesProvider, UnboundCompleter
223+
from .types import (
224+
CmdOrSetT,
225+
UnboundChoicesProvider,
226+
UnboundCompleter,
227+
)
212228

213229
if TYPE_CHECKING:
214230
from .argparse_completer import ArgparseCompleter
@@ -240,7 +256,7 @@ class Cmd2ParserKwargs(TypedDict, total=False):
240256
exit_on_error: bool
241257
suggest_on_error: bool
242258
color: bool
243-
ap_completer_type: "type[ArgparseCompleter] | None"
259+
completer_class: "type[ArgparseCompleter] | None"
244260

245261

246262
# ---------------------------------------------------------------------------
@@ -1687,7 +1703,7 @@ def _const_mismatches_type(a: _ArgparseArgument) -> bool:
16871703

16881704
# Parameters handled specially by the decorator and not added to the parser. The first positional
16891705
# parameter (self/cls) is always skipped by position; these cover additional decorator-managed names.
1690-
_SKIP_PARAMS = frozenset({"cmd2_handler", "cmd2_statement"})
1706+
_SKIP_PARAMS = frozenset({constants.NS_ATTR_SUBCOMMAND_FUNC, constants.NS_ATTR_STATEMENT})
16911707

16921708

16931709
def _link_group_membership(
@@ -1718,14 +1734,17 @@ def _resolve_parameters(
17181734
"""Resolve a function signature into a list of argparse-argument builders.
17191735
17201736
``base_command`` marks each argument's context for the base-command :data:`_CONSTRAINTS` rows and
1721-
drives the function-level ``cmd2_handler`` check below. ``groups``/``mutually_exclusive_groups``
1737+
drives the function-level ``cmd2_subcommand_func`` check below. ``groups``/``mutually_exclusive_groups``
17221738
are linked onto each argument as membership facts for the cross-config constraint rows.
17231739
"""
17241740
sig = inspect.signature(func)
17251741
# Function-level check (not a per-argument _CONSTRAINTS row): a base command dispatches through
1726-
# cmd2_handler, so it must exist. Here so it also fires when the function has zero parameters.
1727-
if base_command and "cmd2_handler" not in sig.parameters:
1728-
raise TypeError(f"with_annotated(base_command=True) requires a 'cmd2_handler' parameter in {func.__qualname__}")
1742+
# cmd2_subcommand_func, so it must exist. Here so it also fires when the function has zero parameters.
1743+
if base_command and constants.NS_ATTR_SUBCOMMAND_FUNC not in sig.parameters:
1744+
raise TypeError(
1745+
f"with_annotated(base_command=True) requires a '{constants.NS_ATTR_SUBCOMMAND_FUNC}' "
1746+
f"parameter in {func.__qualname__}"
1747+
)
17291748
try:
17301749
hints = get_type_hints(func, include_extras=True)
17311750
except (NameError, AttributeError, TypeError) as exc:
@@ -1828,14 +1847,10 @@ def _filtered_namespace_kwargs(
18281847
exclude_subcommand: bool = False,
18291848
) -> dict[str, Any]:
18301849
"""Filter a parsed Namespace down to user-visible kwargs."""
1831-
from .constants import NS_ATTR_SUBCMD_HANDLER
1832-
18331850
filtered: dict[str, Any] = {}
18341851
for key, value in vars(ns).items():
18351852
if accepted is not None and key not in accepted:
18361853
continue
1837-
if key == NS_ATTR_SUBCMD_HANDLER:
1838-
continue
18391854
if exclude_subcommand and key == "subcommand":
18401855
continue
18411856
filtered[key] = value
@@ -1952,7 +1967,9 @@ def build_parser_from_function(
19521967
:param parser_kwargs: forwarded [`Cmd2ParserKwargs`][cmd2.annotated.Cmd2ParserKwargs]
19531968
:return: a fully configured ``Cmd2ArgumentParser``
19541969
"""
1955-
parser_cls = parser_class or DEFAULT_ARGUMENT_PARSER
1970+
from . import argparse_utils
1971+
1972+
parser_cls = parser_class or argparse_utils.DEFAULT_ARGUMENT_PARSER
19561973
if "description" not in parser_kwargs:
19571974
auto_description = _docstring_first_paragraph(func.__doc__)
19581975
if auto_description is not None:
@@ -2105,10 +2122,10 @@ def _build_subcommand_handler(
21052122
def handler(self_arg: Any, ns: Any) -> Any:
21062123
"""Unpack Namespace into typed kwargs for the subcommand handler."""
21072124
filtered = _filtered_namespace_kwargs(ns, accepted=_accepted)
2108-
if "cmd2_handler" in filtered:
2109-
cmd2_h = filtered["cmd2_handler"]
2125+
if constants.NS_ATTR_SUBCOMMAND_FUNC in filtered:
2126+
cmd2_h = filtered[constants.NS_ATTR_SUBCOMMAND_FUNC]
21102127
if isinstance(cmd2_h, functools.partial) and cmd2_h.func is handler:
2111-
filtered["cmd2_handler"] = None
2128+
filtered[constants.NS_ATTR_SUBCOMMAND_FUNC] = None
21122129
return _invoke_command_func(
21132130
func, self_arg, filtered, leading_names=_leading_names, var_positional_name=_var_positional_name
21142131
)
@@ -2170,7 +2187,7 @@ def with_annotated(
21702187
:param ns_provider: callable returning a prepopulated Namespace (not with ``subcommand_to``)
21712188
:param preserve_quotes: preserve quotes in arguments (not with ``subcommand_to``)
21722189
:param with_unknown_args: capture unknown args as the ``_unknown`` kwarg (not with ``subcommand_to``)
2173-
:param base_command: add ``add_subparsers()``; requires a ``cmd2_handler`` param and no positionals
2190+
:param base_command: add ``add_subparsers()``; requires a ``cmd2_subcommand_func`` param and no positionals
21742191
:param subcommand_to: parent command name; function must be named ``{parent_underscored}_{subcommand}``
21752192
:param help: subcommand help text (only with ``subcommand_to``)
21762193
:param aliases: alternative subcommand names (only with ``subcommand_to``)
@@ -2232,9 +2249,10 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
22322249
if unknown_param.kind is inspect.Parameter.POSITIONAL_ONLY:
22332250
raise TypeError("Parameter _unknown must be keyword-compatible when with_unknown_args=True")
22342251

2235-
if not base_command and "cmd2_handler" in inspect.signature(fn).parameters:
2252+
if not base_command and constants.NS_ATTR_SUBCOMMAND_FUNC in inspect.signature(fn).parameters:
22362253
raise TypeError(
2237-
f"Parameter 'cmd2_handler' in {fn.__qualname__} is only valid when with_annotated(base_command=True) is used."
2254+
f"Parameter '{constants.NS_ATTR_SUBCOMMAND_FUNC}' in {fn.__qualname__} "
2255+
"is only valid when with_annotated(base_command=True) is used."
22382256
)
22392257

22402258
if subcommand_to is not None:
@@ -2244,15 +2262,15 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
22442262
base_command=base_command,
22452263
options=options,
22462264
)
2247-
spec = SubcommandSpec(
2265+
subcommand_spec = SubcommandSpec(
22482266
name=subcmd_name,
22492267
command=subcommand_to,
22502268
help=help,
22512269
aliases=tuple(aliases),
22522270
deprecated=deprecated,
22532271
parser_source=subcmd_parser_builder,
22542272
)
2255-
setattr(handler, constants.SUBCMD_ATTR_SPEC, spec)
2273+
setattr(handler, constants.SUBCOMMAND_ATTR_SPEC, subcommand_spec)
22562274
return handler
22572275

22582276
command_name = fn.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
@@ -2296,10 +2314,10 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
22962314
raise Cmd2ArgparseError from exc
22972315

22982316
setattr(ns, constants.NS_ATTR_STATEMENT, statement)
2299-
handler = getattr(ns, constants.NS_ATTR_SUBCMD_HANDLER, None)
2317+
handler = getattr(ns, constants.NS_ATTR_SUBCOMMAND_FUNC, None)
23002318
if base_command and handler is not None:
23012319
handler = functools.partial(handler, ns)
2302-
ns.cmd2_handler = handler
2320+
setattr(ns, constants.NS_ATTR_SUBCOMMAND_FUNC, handler)
23032321

23042322
func_kwargs = _filtered_namespace_kwargs(ns, accepted=accepted, exclude_subcommand=base_command)
23052323

@@ -2312,8 +2330,11 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
23122330
)
23132331
return result
23142332

2315-
setattr(cmd_wrapper, constants.CMD_ATTR_PARSER_SOURCE, parser_builder)
2316-
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)
2333+
argparse_command_spec = ArgparseCommandSpec(
2334+
parser_source=parser_builder,
2335+
preserve_quotes=preserve_quotes,
2336+
)
2337+
setattr(cmd_wrapper, constants.ARGPARSE_COMMAND_ATTR_SPEC, argparse_command_spec)
23172338

23182339
return cmd_wrapper
23192340

cmd2/argparse_completer.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,7 @@ def consume_argument(arg_state: _ArgumentState, arg_token: str) -> None:
366366
parent_tokens[action.dest] = [token]
367367

368368
parser = self._subcommand_action.choices[token]
369-
completer_type = self._cmd_app._determine_ap_completer_type(parser)
370-
completer = completer_type(parser, self._cmd_app, parent_tokens=parent_tokens)
369+
completer = parser.completer_class(parser, self._cmd_app, parent_tokens=parent_tokens)
371370
return completer.complete(text, line, begidx, endidx, tokens[token_index + 1 :], cmd_set=cmd_set)
372371

373372
# Invalid subcommand entered, so no way to complete remaining tokens
@@ -668,8 +667,7 @@ def complete_subcommand_help(self, text: str, line: str, begidx: int, endidx: in
668667
for token_index, token in enumerate(tokens):
669668
if token in self._subcommand_action.choices:
670669
parser = self._subcommand_action.choices[token]
671-
completer_type = self._cmd_app._determine_ap_completer_type(parser)
672-
completer = completer_type(parser, self._cmd_app)
670+
completer = parser.completer_class(parser, self._cmd_app)
673671
return completer.complete_subcommand_help(text, line, begidx, endidx, tokens[token_index + 1 :])
674672

675673
if token_index == len(tokens) - 1:
@@ -690,8 +688,7 @@ def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None
690688
if tokens and self._subcommand_action is not None:
691689
parser = self._subcommand_action.choices.get(tokens[0])
692690
if parser is not None:
693-
completer_type = self._cmd_app._determine_ap_completer_type(parser)
694-
completer = completer_type(parser, self._cmd_app)
691+
completer = parser.completer_class(parser, self._cmd_app)
695692
completer.print_help(tokens[1:], file)
696693
return
697694
self._parser.print_help(file)
@@ -800,13 +797,13 @@ def _complete_arg(
800797

801798

802799
# The default ArgparseCompleter class for a cmd2 app
803-
DEFAULT_AP_COMPLETER: type[ArgparseCompleter] = ArgparseCompleter
800+
DEFAULT_ARGPARSE_COMPLETER: type[ArgparseCompleter] = ArgparseCompleter
804801

805802

806-
def set_default_ap_completer_type(completer_type: type[ArgparseCompleter]) -> None:
803+
def set_default_argparse_completer(completer_class: type[ArgparseCompleter]) -> None:
807804
"""Set the default ArgparseCompleter class for a cmd2 app.
808805
809-
:param completer_type: Type that is a subclass of ArgparseCompleter.
806+
:param completer_class: Type that is a subclass of ArgparseCompleter.
810807
"""
811-
global DEFAULT_AP_COMPLETER # noqa: PLW0603
812-
DEFAULT_AP_COMPLETER = completer_type
808+
global DEFAULT_ARGPARSE_COMPLETER # noqa: PLW0603
809+
DEFAULT_ARGPARSE_COMPLETER = completer_class

0 commit comments

Comments
 (0)