Skip to content

Commit 5c1fc15

Browse files
authored
Updates to type hints for with_argparser and with_argument_list to correctly validate subclasses of Cmd. (#1276)
Addresses #1273
1 parent d4a2fc3 commit 5c1fc15

File tree

3 files changed

+33
-37
lines changed

3 files changed

+33
-37
lines changed

cmd2/decorators.py

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Optional,
1111
Sequence,
1212
Tuple,
13+
TypeVar,
1314
Union,
1415
)
1516

@@ -72,7 +73,10 @@ def cat_decorator(func: CommandFunc) -> CommandFunc:
7273
##########################
7374

7475

75-
RawCommandFuncOptionalBoolReturn = Callable[[Union[CommandSet, 'cmd2.Cmd'], Union[Statement, str]], Optional[bool]]
76+
CommandParent = TypeVar('CommandParent', bound=Union['cmd2.Cmd', CommandSet])
77+
78+
79+
RawCommandFuncOptionalBoolReturn = Callable[[CommandParent, Union[Statement, str]], Optional[bool]]
7680

7781

7882
def _parse_positionals(args: Tuple[Any, ...]) -> Tuple['cmd2.Cmd', Union[Statement, str]]:
@@ -116,38 +120,36 @@ def _arg_swap(args: Union[Sequence[Any]], search_arg: Any, *replace_arg: Any) ->
116120

117121
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
118122
#: and optionally returns a boolean
119-
ArgListCommandFuncOptionalBoolReturn = Union[
120-
Callable[['cmd2.Cmd', List[str]], Optional[bool]],
121-
Callable[[CommandSet, List[str]], Optional[bool]],
122-
]
123+
ArgListCommandFuncOptionalBoolReturn = Callable[[CommandParent, List[str]], Optional[bool]]
123124
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
124125
#: and returns a boolean
125-
ArgListCommandFuncBoolReturn = Union[
126-
Callable[['cmd2.Cmd', List[str]], bool],
127-
Callable[[CommandSet, List[str]], bool],
128-
]
126+
ArgListCommandFuncBoolReturn = Callable[[CommandParent, List[str]], bool]
129127
#: Function signature for an Command Function that accepts a pre-processed argument list from user input
130128
#: and returns Nothing
131-
ArgListCommandFuncNoneReturn = Union[
132-
Callable[['cmd2.Cmd', List[str]], None],
133-
Callable[[CommandSet, List[str]], None],
134-
]
129+
ArgListCommandFuncNoneReturn = Callable[[CommandParent, List[str]], None]
135130

136131
#: Aggregate of all accepted function signatures for Command Functions that accept a pre-processed argument list
137-
ArgListCommandFunc = Union[ArgListCommandFuncOptionalBoolReturn, ArgListCommandFuncBoolReturn, ArgListCommandFuncNoneReturn]
132+
ArgListCommandFunc = Union[
133+
ArgListCommandFuncOptionalBoolReturn[CommandParent],
134+
ArgListCommandFuncBoolReturn[CommandParent],
135+
ArgListCommandFuncNoneReturn[CommandParent],
136+
]
138137

139138

140139
def with_argument_list(
141-
func_arg: Optional[ArgListCommandFunc] = None,
140+
func_arg: Optional[ArgListCommandFunc[CommandParent]] = None,
142141
*,
143142
preserve_quotes: bool = False,
144-
) -> Union[RawCommandFuncOptionalBoolReturn, Callable[[ArgListCommandFunc], RawCommandFuncOptionalBoolReturn]]:
143+
) -> Union[
144+
RawCommandFuncOptionalBoolReturn[CommandParent],
145+
Callable[[ArgListCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]],
146+
]:
145147
"""
146148
A decorator to alter the arguments passed to a ``do_*`` method. Default
147149
passes a string of whatever the user typed. With this decorator, the
148150
decorated method will receive a list of arguments parsed from user input.
149151
150-
:param func_arg: Single-element positional argument list containing ``do_*`` method
152+
:param func_arg: Single-element positional argument list containing ``doi_*`` method
151153
this decorator is wrapping
152154
:param preserve_quotes: if ``True``, then argument quotes will not be stripped
153155
:return: function that gets passed a list of argument strings
@@ -161,7 +163,7 @@ def with_argument_list(
161163
"""
162164
import functools
163165

164-
def arg_decorator(func: ArgListCommandFunc) -> RawCommandFuncOptionalBoolReturn:
166+
def arg_decorator(func: ArgListCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]:
165167
"""
166168
Decorator function that ingests an Argument List function and returns a raw command function.
167169
The returned function will process the raw input into an argument list to be passed to the wrapped function.
@@ -243,28 +245,19 @@ def _set_parser_prog(parser: argparse.ArgumentParser, prog: str) -> None:
243245

244246
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
245247
#: and optionally returns a boolean
246-
ArgparseCommandFuncOptionalBoolReturn = Union[
247-
Callable[['cmd2.Cmd', argparse.Namespace], Optional[bool]],
248-
Callable[[CommandSet, argparse.Namespace], Optional[bool]],
249-
]
248+
ArgparseCommandFuncOptionalBoolReturn = Callable[[CommandParent, argparse.Namespace], Optional[bool]]
250249
#: Function signature for a Command Function that uses an argparse.ArgumentParser to process user input
251250
#: and returns a boolean
252-
ArgparseCommandFuncBoolReturn = Union[
253-
Callable[['cmd2.Cmd', argparse.Namespace], bool],
254-
Callable[[CommandSet, argparse.Namespace], bool],
255-
]
251+
ArgparseCommandFuncBoolReturn = Callable[[CommandParent, argparse.Namespace], bool]
256252
#: Function signature for an Command Function that uses an argparse.ArgumentParser to process user input
257253
#: and returns nothing
258-
ArgparseCommandFuncNoneReturn = Union[
259-
Callable[['cmd2.Cmd', argparse.Namespace], None],
260-
Callable[[CommandSet, argparse.Namespace], None],
261-
]
254+
ArgparseCommandFuncNoneReturn = Callable[[CommandParent, argparse.Namespace], None]
262255

263256
#: Aggregate of all accepted function signatures for an argparse Command Function
264257
ArgparseCommandFunc = Union[
265-
ArgparseCommandFuncOptionalBoolReturn,
266-
ArgparseCommandFuncBoolReturn,
267-
ArgparseCommandFuncNoneReturn,
258+
ArgparseCommandFuncOptionalBoolReturn[CommandParent],
259+
ArgparseCommandFuncBoolReturn[CommandParent],
260+
ArgparseCommandFuncNoneReturn[CommandParent],
268261
]
269262

270263

@@ -274,7 +267,7 @@ def with_argparser(
274267
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
275268
preserve_quotes: bool = False,
276269
with_unknown_args: bool = False,
277-
) -> Callable[[ArgparseCommandFunc], RawCommandFuncOptionalBoolReturn]:
270+
) -> Callable[[ArgparseCommandFunc[CommandParent]], RawCommandFuncOptionalBoolReturn[CommandParent]]:
278271
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
279272
with the given instance of argparse.ArgumentParser.
280273
@@ -320,7 +313,7 @@ def with_argparser(
320313
"""
321314
import functools
322315

323-
def arg_decorator(func: ArgparseCommandFunc) -> RawCommandFuncOptionalBoolReturn:
316+
def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> RawCommandFuncOptionalBoolReturn[CommandParent]:
324317
"""
325318
Decorator function that ingests an Argparse Command Function and returns a raw command function.
326319
The returned function will process the raw input into an argparse Namespace to be passed to the wrapped function.
@@ -409,7 +402,7 @@ def as_subcommand_to(
409402
*,
410403
help: Optional[str] = None,
411404
aliases: Optional[List[str]] = None,
412-
) -> Callable[[ArgparseCommandFunc], ArgparseCommandFunc]:
405+
) -> Callable[[ArgparseCommandFunc[CommandParent]], ArgparseCommandFunc[CommandParent]]:
413406
"""
414407
Tag this method as a subcommand to an existing argparse decorated command.
415408
@@ -423,7 +416,7 @@ def as_subcommand_to(
423416
:return: Wrapper function that can receive an argparse.Namespace
424417
"""
425418

426-
def arg_decorator(func: ArgparseCommandFunc) -> ArgparseCommandFunc:
419+
def arg_decorator(func: ArgparseCommandFunc[CommandParent]) -> ArgparseCommandFunc[CommandParent]:
427420
_set_parser_prog(parser, command + ' ' + subcommand)
428421

429422
# If the description has not been set, then use the method docstring if one exists

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ exclude = '''
1414
| \.mypy_cache
1515
| \.pytest_cache
1616
| \.tox
17+
| \.nox
1718
| \.venv
1819
| \.vscode
1920
| _build

tasks.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ def mypy(context):
9999
"""Run mypy optional static type checker"""
100100
with context.cd(TASK_ROOT_STR):
101101
context.run("mypy cmd2")
102+
with context.cd(str(TASK_ROOT / 'examples')):
103+
context.run("mypy decorator_example.py")
102104

103105

104106
namespace.add_task(mypy)

0 commit comments

Comments
 (0)