Skip to content

Commit 41be698

Browse files
committed
Require devs to use Cmd2ArgumentParser-based parsers.
1 parent 88285e3 commit 41be698

10 files changed

Lines changed: 56 additions & 76 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ 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 much be an instance of `Cmd2ArgumentParser` or a child
68+
class of it.
6769
- Enhancements
6870
- New `cmd2.Cmd` parameters
6971
- **auto_suggest**: (boolean) if `True`, provide fish shell style auto-suggestions. These

cmd2/argparse_completer.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
from .argparse_custom import (
3636
ChoicesCallable,
37+
Cmd2ArgumentParser,
3738
generate_range_error,
3839
)
3940
from .command_definition import CommandSet
@@ -49,7 +50,7 @@
4950
ARG_TOKENS = 'arg_tokens'
5051

5152

52-
def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) -> str:
53+
def _build_hint(parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> str:
5354
"""Build completion hint for a given argument."""
5455
# Check if hinting is disabled for this argument
5556
suppress_hint = arg_action.get_suppress_tab_hint() # type: ignore[attr-defined]
@@ -64,12 +65,12 @@ def _build_hint(parser: argparse.ArgumentParser, arg_action: argparse.Action) ->
6465
return formatter.format_help()
6566

6667

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

7172

72-
def _looks_like_flag(token: str, parser: argparse.ArgumentParser) -> bool:
73+
def _looks_like_flag(token: str, parser: Cmd2ArgumentParser) -> bool:
7374
"""Determine if a token looks like a flag.
7475
7576
Unless an argument has nargs set to argparse.REMAINDER, then anything that looks like a flag
@@ -140,12 +141,12 @@ def __init__(self, flag_arg_state: _ArgumentState) -> None:
140141

141142

142143
class _NoResultsError(CompletionError):
143-
def __init__(self, parser: argparse.ArgumentParser, arg_action: argparse.Action) -> None:
144+
def __init__(self, parser: Cmd2ArgumentParser, arg_action: argparse.Action) -> None:
144145
"""CompletionError which occurs when there are no results.
145146
146147
If hinting is allowed on this argument, then its hint text will display.
147148
148-
:param parser: ArgumentParser instance which owns the action being completed
149+
:param parser: Cmd2ArgumentParser instance which owns the action being completed
149150
:param arg_action: action being completed.
150151
"""
151152
# Set apply_style to False because we don't want hints to look like errors
@@ -157,14 +158,14 @@ class ArgparseCompleter:
157158

158159
def __init__(
159160
self,
160-
parser: argparse.ArgumentParser,
161+
parser: Cmd2ArgumentParser,
161162
cmd2_app: 'Cmd',
162163
*,
163164
parent_tokens: Mapping[str, MutableSequence[str]] | None = None,
164165
) -> None:
165166
"""Create an ArgparseCompleter.
166167
167-
:param parser: ArgumentParser instance
168+
:param parser: Cmd2ArgumentParser instance
168169
:param cmd2_app: reference to the Cmd2 application that owns this ArgparseCompleter
169170
:param parent_tokens: optional Mapping of parent parsers' arg names to their tokens
170171
This is only used by ArgparseCompleter when recursing on subcommand parsers
@@ -187,7 +188,7 @@ def __init__(
187188
self._positional_actions: list[argparse.Action] = []
188189

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

192193
# Start digging through the argparse structures.
193194
# _actions is the top level container of parameter definitions

cmd2/argparse_custom.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,9 @@
22
33
It also defines a parser class called Cmd2ArgumentParser which improves error
44
and help output over normal argparse. All cmd2 code uses this parser and it is
5-
recommended that developers of cmd2-based apps either use it or write their own
6-
parser that inherits from it. This will give a consistent look-and-feel between
7-
the help/error output of built-in cmd2 commands and the app-specific commands.
8-
If you wish to override the parser used by cmd2's built-in commands, see
9-
custom_parser.py example.
10-
11-
Since the new capabilities are added by patching at the argparse API level,
12-
they are available whether or not Cmd2ArgumentParser is used. However, the help
13-
and error output of Cmd2ArgumentParser is customized to notate nargs ranges
14-
whereas any other parser class won't be as explicit in their output.
5+
required that developers of cmd2-based apps either use it or write their own
6+
parser that inherits from it. If you wish to override the parser used by cmd2's
7+
built-in commands, see custom_parser.py example.
158
169
1710
**Added capabilities**

cmd2/cmd2.py

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ def __init__(self, msg: str = '') -> None:
206206
)
207207

208208
if TYPE_CHECKING: # pragma: no cover
209-
StaticArgParseBuilder = staticmethod[[], argparse.ArgumentParser]
210-
ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], argparse.ArgumentParser]
209+
StaticArgParseBuilder = staticmethod[[], Cmd2ArgumentParser]
210+
ClassArgParseBuilder = classmethod['Cmd' | CommandSet, [], Cmd2ArgumentParser]
211211
from prompt_toolkit.buffer import Buffer
212212
else:
213213
StaticArgParseBuilder = staticmethod
@@ -237,7 +237,7 @@ def __init__(self, cmd: 'Cmd') -> None:
237237

238238
# Keyed by the fully qualified method names. This is more reliable than
239239
# the methods themselves, since wrapping a method will change its address.
240-
self._parsers: dict[str, argparse.ArgumentParser] = {}
240+
self._parsers: dict[str, Cmd2ArgumentParser] = {}
241241

242242
@staticmethod
243243
def _fully_qualified_name(command_method: CommandFunc) -> str:
@@ -256,7 +256,7 @@ def __contains__(self, command_method: CommandFunc) -> bool:
256256
parser = self.get(command_method)
257257
return bool(parser)
258258

259-
def get(self, command_method: CommandFunc) -> argparse.ArgumentParser | None:
259+
def get(self, command_method: CommandFunc) -> Cmd2ArgumentParser | None:
260260
"""Return a given method's parser or None if the method is not argparse-based.
261261
262262
If the parser does not yet exist, it will be created.
@@ -889,12 +889,9 @@ def register_command_set(self, cmdset: CommandSet) -> None:
889889
def _build_parser(
890890
self,
891891
parent: CmdOrSet,
892-
parser_builder: argparse.ArgumentParser
893-
| Callable[[], argparse.ArgumentParser]
894-
| StaticArgParseBuilder
895-
| ClassArgParseBuilder,
892+
parser_builder: Cmd2ArgumentParser | Callable[[], Cmd2ArgumentParser] | StaticArgParseBuilder | ClassArgParseBuilder,
896893
prog: str,
897-
) -> argparse.ArgumentParser:
894+
) -> Cmd2ArgumentParser:
898895
"""Build argument parser for a command/subcommand.
899896
900897
:param parent: object which owns the command using the parser.
@@ -911,7 +908,7 @@ def _build_parser(
911908
parser = parser_builder.__func__(parent.__class__)
912909
elif callable(parser_builder):
913910
parser = parser_builder()
914-
elif isinstance(parser_builder, argparse.ArgumentParser):
911+
elif isinstance(parser_builder, Cmd2ArgumentParser):
915912
parser = copy.deepcopy(parser_builder)
916913
else:
917914
raise TypeError(f"Invalid type for parser_builder: {type(parser_builder)}")
@@ -1021,7 +1018,7 @@ def unregister_command_set(self, cmdset: CommandSet) -> None:
10211018
self._installed_command_sets.remove(cmdset)
10221019

10231020
def _check_uninstallable(self, cmdset: CommandSet) -> None:
1024-
def check_parser_uninstallable(parser: argparse.ArgumentParser) -> None:
1021+
def check_parser_uninstallable(parser: Cmd2ArgumentParser) -> None:
10251022
cmdset_id = id(cmdset)
10261023
for action in parser._actions:
10271024
if isinstance(action, argparse._SubParsersAction):
@@ -1098,9 +1095,7 @@ def _register_subcommands(self, cmdset: Union[CommandSet, 'Cmd']) -> None:
10981095
f"Could not find argparser for command '{command_name}' needed by subcommand: {method}"
10991096
)
11001097

1101-
def find_subcommand(
1102-
action: argparse.ArgumentParser, subcmd_names: MutableSequence[str]
1103-
) -> argparse.ArgumentParser:
1098+
def find_subcommand(action: Cmd2ArgumentParser, subcmd_names: MutableSequence[str]) -> Cmd2ArgumentParser:
11041099
if not subcmd_names:
11051100
return action
11061101
cur_subcmd = subcmd_names.pop(0)
@@ -2349,7 +2344,7 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com
23492344
return compfunc(text, line, begidx, endidx)
23502345

23512346
@staticmethod
2352-
def _determine_ap_completer_type(parser: argparse.ArgumentParser) -> type[argparse_completer.ArgparseCompleter]:
2347+
def _determine_ap_completer_type(parser: Cmd2ArgumentParser) -> type[argparse_completer.ArgparseCompleter]:
23532348
"""Determine what type of ArgparseCompleter to use on a given parser.
23542349
23552350
If the parser does not have one set, then use argparse_completer.DEFAULT_AP_COMPLETER.
@@ -3455,7 +3450,7 @@ def _resolve_completer(
34553450
choices: Iterable[Any] | None = None,
34563451
choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None,
34573452
completer: CompleterUnbound[CmdOrSet] | None = None,
3458-
parser: argparse.ArgumentParser | None = None,
3453+
parser: Cmd2ArgumentParser | None = None,
34593454
) -> Completer:
34603455
"""Determine the appropriate completer based on provided arguments."""
34613456
if not any((parser, choices, choices_provider, completer)):
@@ -3487,7 +3482,7 @@ def read_input(
34873482
choices: Iterable[Any] | None = None,
34883483
choices_provider: ChoicesProviderUnbound[CmdOrSet] | None = None,
34893484
completer: CompleterUnbound[CmdOrSet] | None = None,
3490-
parser: argparse.ArgumentParser | None = None,
3485+
parser: Cmd2ArgumentParser | None = None,
34913486
) -> str:
34923487
"""Read a line of input with optional completion and history.
34933488

cmd2/decorators.py

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
)
1414

1515
from . import constants
16-
from .argparse_custom import Cmd2AttributeWrapper
16+
from .argparse_custom import (
17+
Cmd2ArgumentParser,
18+
Cmd2AttributeWrapper,
19+
)
1720
from .command_definition import (
1821
CommandFunc,
1922
CommandSet,
@@ -184,19 +187,19 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
184187
return arg_decorator
185188

186189

187-
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
190+
#: Function signatures for command functions that use a Cmd2ArgumentParser to process user input
188191
#: and optionally return a boolean
189192
ArgparseCommandFuncOptionalBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], bool | None]
190193
ArgparseCommandFuncWithUnknownArgsOptionalBoolReturn: TypeAlias = Callable[
191194
[CmdOrSet, argparse.Namespace, list[str]], bool | None
192195
]
193196

194-
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
197+
#: Function signatures for command functions that use a Cmd2ArgumentParser to process user input
195198
#: and return a boolean
196199
ArgparseCommandFuncBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], bool]
197200
ArgparseCommandFuncWithUnknownArgsBoolReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace, list[str]], bool]
198201

199-
#: Function signatures for command functions that use an argparse.ArgumentParser to process user input
202+
#: Function signatures for command functions that use a Cmd2ArgumentParser to process user input
200203
#: and return nothing
201204
ArgparseCommandFuncNoneReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace], None]
202205
ArgparseCommandFuncWithUnknownArgsNoneReturn: TypeAlias = Callable[[CmdOrSet, argparse.Namespace, list[str]], None]
@@ -213,17 +216,17 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
213216

214217

215218
def with_argparser(
216-
parser: argparse.ArgumentParser # existing parser
217-
| Callable[[], argparse.ArgumentParser] # function or staticmethod
218-
| Callable[[CmdOrSetClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod
219+
parser: Cmd2ArgumentParser # existing parser
220+
| Callable[[], Cmd2ArgumentParser] # function or staticmethod
221+
| Callable[[CmdOrSetClass], Cmd2ArgumentParser], # Cmd or CommandSet classmethod
219222
*,
220223
ns_provider: Callable[..., argparse.Namespace] | None = None,
221224
preserve_quotes: bool = False,
222225
with_unknown_args: bool = False,
223226
) -> Callable[[ArgparseCommandFunc[CmdOrSet]], RawCommandFuncOptionalBoolReturn[CmdOrSet]]:
224-
"""Decorate a ``do_*`` method to populate its ``args`` argument with the given instance of argparse.ArgumentParser.
227+
"""Decorate a ``do_*`` method to populate its ``args`` argument with the given instance of Cmd2ArgumentParser.
225228
226-
:param parser: instance of ArgumentParser or a callable that returns an ArgumentParser for this command
229+
:param parser: instance of Cmd2ArgumentParser or a callable that returns an Cmd2ArgumentParser for this command
227230
:param ns_provider: An optional function that accepts a cmd2.Cmd or cmd2.CommandSet object as an argument and returns an
228231
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with state data that
229232
affects parsing.
@@ -347,9 +350,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
347350
def as_subcommand_to(
348351
command: str,
349352
subcommand: str,
350-
parser: argparse.ArgumentParser # existing parser
351-
| Callable[[], argparse.ArgumentParser] # function or staticmethod
352-
| Callable[[CmdOrSetClass], argparse.ArgumentParser], # Cmd or CommandSet classmethod
353+
parser: Cmd2ArgumentParser # existing parser
354+
| Callable[[], Cmd2ArgumentParser] # function or staticmethod
355+
| Callable[[CmdOrSetClass], Cmd2ArgumentParser], # Cmd or CommandSet classmethod
353356
*,
354357
help: str | None = None, # noqa: A002
355358
aliases: Sequence[str] | None = None,
@@ -359,7 +362,7 @@ def as_subcommand_to(
359362
360363
:param command: Command Name. Space-delimited subcommands may optionally be specified
361364
:param subcommand: Subcommand name
362-
:param parser: instance of ArgumentParser or a callable that returns an ArgumentParser for this subcommand
365+
:param parser: instance of Cmd2ArgumentParser or a callable that returns an Cmd2ArgumentParser for this subcommand
363366
:param help: Help message for this subcommand which displays in the list of subcommands of the command we are adding to.
364367
This is passed as the help argument to subparsers.add_parser().
365368
:param aliases: Alternative names for this subcommand. This is passed as the alias argument to

cmd2/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Shared utility functions."""
22

3-
import argparse
43
import contextlib
54
import functools
65
import glob
@@ -36,6 +35,7 @@
3635

3736
if TYPE_CHECKING: # pragma: no cover
3837
PopenTextIO = subprocess.Popen[str]
38+
from .argparse_custom import Cmd2ArgumentParser
3939
else:
4040
PopenTextIO = subprocess.Popen
4141

@@ -734,7 +734,7 @@ def get_defining_class(meth: Callable[..., Any]) -> type[Any] | None:
734734
class CustomCompletionSettings:
735735
"""Used by cmd2.Cmd.complete() to complete strings other than command arguments."""
736736

737-
def __init__(self, parser: argparse.ArgumentParser, *, preserve_quotes: bool = False) -> None:
737+
def __init__(self, parser: 'Cmd2ArgumentParser', *, preserve_quotes: bool = False) -> None:
738738
"""CustomCompletionSettings initializer.
739739
740740
:param parser: arg parser defining format of string being completed

docs/features/argument_processing.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ following for you:
66

77
1. Parsing input and quoted strings in a manner similar to how POSIX shells do it
88
1. Parse the resulting argument list using an instance of
9-
[argparse.ArgumentParser](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)
9+
[Cmd2ArgumentParser](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)
1010
that you provide
1111
1. Passes the resulting
1212
[argparse.Namespace](https://docs.python.org/3/library/argparse.html#argparse.Namespace) object
@@ -39,9 +39,9 @@ command which might have its own argument parsing.
3939
The [@with_argparser][cmd2.with_argparser] decorator can accept the following for its first
4040
argument:
4141

42-
1. An existing instance of `argparse.ArgumentParser`
43-
2. A function or static method which returns an instance of `argparse.ArgumentParser`
44-
3. Cmd or CommandSet class method which returns an instance of `argparse.ArgumentParser`
42+
1. An existing instance of `Cmd2ArgumentParser`
43+
2. A function or static method which returns an instance of `Cmd2ArgumentParser`
44+
3. Cmd or CommandSet class method which returns an instance of `Cmd2ArgumentParser`
4545

4646
In all cases the `@with_argparser` decorator creates a deep copy of the parser instance which it
4747
stores internally. A consequence is that parsers don't need to be unique across commands.
@@ -55,11 +55,11 @@ stores internally. A consequence is that parsers don't need to be unique across
5555
## Argument Parsing
5656

5757
For each command in the `cmd2.Cmd` subclass which requires argument parsing, create an instance of
58-
`argparse.ArgumentParser()` which can parse the input appropriately for the command (or provide a
58+
`Cmd2ArgumentParser` which can parse the input appropriately for the command (or provide a
5959
function/method that returns such a parser). Then decorate the command method with the
6060
`@with_argparser` decorator, passing the argument parser as the first parameter to the decorator.
6161
This changes the second argument of the command method, which will contain the results of
62-
`ArgumentParser.parse_args()`.
62+
`Cmd2ArgumentParser.parse_args()`.
6363

6464
Here's what it looks like:
6565

@@ -97,7 +97,7 @@ def do_speak(self, opts):
9797

9898
By default, `cmd2` uses the docstring of the command method when a user asks for help on the
9999
command. When you use the `@with_argparser` decorator, the docstring for the `do_*` method is used
100-
to set the description for the `argparse.ArgumentParser`.
100+
to set the description for the `Cmd2ArgumentParser`.
101101

102102
!!! tip "description and epilog fields are rich objects"
103103

@@ -135,8 +135,8 @@ optional arguments:
135135
-h, --help show this help message and exit
136136
```
137137

138-
If you would prefer, you can set the `description` while instantiating the `argparse.ArgumentParser`
139-
and leave the docstring on your method blank:
138+
If you would prefer, you can set the `description` while instantiating the `Cmd2ArgumentParser` and
139+
leave the docstring on your method blank:
140140

141141
```py
142142
from cmd2 import Cmd2ArgumentParser, with_argparser

docs/features/completion.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ When using `cmd2`'s [@with_argparser][cmd2.with_argparser] decorator, `cmd2` pro
7777
completion of flag names.
7878

7979
Tab completion of argument values can be configured by using one of three parameters to
80-
[argparse.ArgumentParser.add_argument](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument)
80+
`Cmd2ArgumentParser.add_argument()`.
8181

8282
- `choices`
8383
- `choices_provider`

docs/migrating/next_steps.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ leveraging other `cmd2` features. The three ideas here will get you started. Bro
99
For all but the simplest of commands, it's probably easier to use
1010
[argparse](https://docs.python.org/3/library/argparse.html) to parse user input than to do it
1111
manually yourself for each command. `cmd2` provides a `@with_argparser()` decorator which associates
12-
an `ArgumentParser` object with one of your commands. Using this method will:
12+
an `Cmd2ArgumentParser` object with one of your commands. Using this method will:
1313

1414
1. Pass your command a
1515
[Namespace](https://docs.python.org/3/library/argparse.html#argparse.Namespace) containing the
1616
arguments instead of a string of text
1717
2. Properly handle quoted string input from your users
18-
3. Create a help message for you based on the `ArgumentParser`
18+
3. Create a help message for you based on the `Cmd2ArgumentParser`
1919
4. Give you a big head start adding [Tab Completion](../features/completion.md) to your application
2020
5. Make it much easier to implement subcommands (i.e. `git` has a bunch of subcommands such as
2121
`git pull`, `git diff`, etc)

0 commit comments

Comments
 (0)