Skip to content

Commit e7dac06

Browse files
committed
Ruggedize a few types
1 parent de4ba20 commit e7dac06

3 files changed

Lines changed: 44 additions & 33 deletions

File tree

‎cmd2/annotated.py‎

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,7 @@ def do_build(self, target: str, common: CommonArgs):
290290
TypeVar,
291291
Union,
292292
Unpack,
293+
cast,
293294
get_args,
294295
get_origin,
295296
get_type_hints,
@@ -321,7 +322,7 @@ def do_build(self, target: str, common: CommonArgs):
321322
_NargsValue = int | str | tuple[int] | tuple[int, int] | tuple[int, float]
322323

323324
# Parser construction works with a command method before or after descriptor binding.
324-
_CommandFunc: TypeAlias = BoundCommandFunc | UnboundCommandFunc[Any, ...]
325+
_CommandFunc: TypeAlias = BoundCommandFunc[...] | UnboundCommandFunc[Any, ...]
325326

326327

327328
class Cmd2ParserKwargs(TypedDict, total=False):
@@ -2880,28 +2881,36 @@ def parser_builder() -> Cmd2ArgumentParser:
28802881
_DecoratorParams = ParamSpec("_DecoratorParams")
28812882

28822883

2883-
class _AnnotatedCommand(Protocol[_CommandParams]):
2884+
class _AnnotatedCommand(Protocol[CmdOrSetT, _CommandParams]):
28842885
"""A callable command method with the metadata used by ``with_annotated``."""
28852886

28862887
__name__: str
28872888
__qualname__: str
28882889

2889-
def __call__(self, *args: _CommandParams.args, **kwargs: _CommandParams.kwargs) -> bool | None: ...
2890+
def __call__(self, __self: CmdOrSetT, /, *args: _CommandParams.args, **kwargs: _CommandParams.kwargs) -> bool | None: ...
2891+
2892+
@overload
2893+
def __get__(self, instance: None, owner: Any = ...) -> "_AnnotatedCommand[CmdOrSetT, _CommandParams]": ...
2894+
2895+
@overload
2896+
def __get__(self, instance: CmdOrSetT, owner: Any = ...) -> BoundCommandFunc[_CommandParams]: ...
28902897

28912898

28922899
class _WithAnnotatedDecorator(Protocol):
28932900
"""The signature-preserving decorator ``with_annotated(...)`` returns (generic per call)."""
28942901

2895-
def __call__(self, fn: _AnnotatedCommand[_CommandParams], /) -> _AnnotatedCommand[_CommandParams]: ...
2902+
def __call__(
2903+
self, fn: _AnnotatedCommand[CmdOrSetT, _CommandParams], /
2904+
) -> _AnnotatedCommand[CmdOrSetT, _CommandParams]: ...
28962905

28972906

28982907
def _build_subcommand_handler(
2899-
func: _AnnotatedCommand[_CommandParams],
2908+
func: _AnnotatedCommand[Any, ...],
29002909
subcommand_to: str,
29012910
*,
29022911
base_command: bool = False,
29032912
options: _ParserBuildOptions,
2904-
) -> tuple[_AnnotatedCommand[_CommandParams], str, Callable[[], Cmd2ArgumentParser]]:
2913+
) -> tuple[_AnnotatedCommand[Any, ...], str, Callable[[], Cmd2ArgumentParser]]:
29052914
"""Build a subcommand's parser and a handler that unpacks the Namespace into typed kwargs.
29062915
29072916
:param func: the subcommand handler function
@@ -2927,7 +2936,7 @@ def _build_subcommand_handler(
29272936
_leading_names, _var_positional_name = _var_positional_call_plan(func)
29282937

29292938
@functools.wraps(func)
2930-
def handler(*args: _CommandParams.args, **kwargs: _CommandParams.kwargs) -> bool | None:
2939+
def handler(*args: Any, **kwargs: Any) -> bool | None:
29312940
"""Unpack Namespace into typed kwargs for the subcommand handler."""
29322941
if kwargs:
29332942
raise TypeError("subcommand handlers do not accept keyword arguments")
@@ -2951,11 +2960,11 @@ def handler(*args: _CommandParams.args, **kwargs: _CommandParams.kwargs) -> bool
29512960
raise TypeError("annotated command functions must return bool or None")
29522961

29532962
parser_builder = _make_parser_builder(func, skip_params=_SKIP_PARAMS, base_command=base_command, options=options)
2954-
return handler, subcmd_name, parser_builder
2963+
return cast(_AnnotatedCommand[Any, ...], handler), subcmd_name, parser_builder
29552964

29562965

29572966
@overload
2958-
def with_annotated(func: _AnnotatedCommand[_CommandParams], /) -> _AnnotatedCommand[_CommandParams]: ...
2967+
def with_annotated(func: _AnnotatedCommand[CmdOrSetT, _CommandParams], /) -> _AnnotatedCommand[CmdOrSetT, _CommandParams]: ...
29592968

29602969

29612970
@overload
@@ -2982,7 +2991,7 @@ def with_annotated(
29822991

29832992

29842993
def with_annotated(
2985-
func: _AnnotatedCommand[_CommandParams] | None = None,
2994+
func: _AnnotatedCommand[CmdOrSetT, _CommandParams] | None = None,
29862995
*,
29872996
ns_provider: Callable[..., argparse.Namespace] | None = None,
29882997
preserve_quotes: bool = False,
@@ -3000,7 +3009,7 @@ def with_annotated(
30003009
subcommand_title: str | None = None,
30013010
subcommand_description: str | None = None,
30023011
**parser_kwargs: Unpack[Cmd2ParserKwargs],
3003-
) -> _AnnotatedCommand[_CommandParams] | _WithAnnotatedDecorator:
3012+
) -> _AnnotatedCommand[CmdOrSetT, _CommandParams] | _WithAnnotatedDecorator:
30043013
"""Decorate a ``do_*`` method to build its argparse parser from type annotations.
30053014
30063015
:param func: the command function (when used without parentheses)
@@ -3062,8 +3071,8 @@ def with_annotated(
30623071
)
30633072

30643073
def decorator(
3065-
command_func: _AnnotatedCommand[_DecoratorParams],
3066-
) -> _AnnotatedCommand[_DecoratorParams]:
3074+
command_func: _AnnotatedCommand[CmdOrSetT, _DecoratorParams],
3075+
) -> _AnnotatedCommand[CmdOrSetT, _DecoratorParams]:
30673076
if with_unknown_args:
30683077
unknown_param = inspect.signature(command_func).parameters.get("_unknown")
30693078
if unknown_param is None:
@@ -3093,7 +3102,7 @@ def decorator(
30933102
parser_source=subcmd_parser_builder,
30943103
)
30953104
setattr(handler, constants.SUBCOMMAND_ATTR_SPEC, subcommand_spec)
3096-
return handler
3105+
return cast(_AnnotatedCommand[CmdOrSetT, _DecoratorParams], handler)
30973106

30983107
command_name = command_func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
30993108

@@ -3170,9 +3179,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
31703179
)
31713180
setattr(cmd_wrapper, constants.ARGPARSE_COMMAND_ATTR_SPEC, argparse_command_spec)
31723181

3173-
return cmd_wrapper
3182+
return cast(_AnnotatedCommand[CmdOrSetT, _DecoratorParams], cmd_wrapper)
31743183

31753184
# Support both @with_annotated and @with_annotated(...)
31763185
if func is not None:
31773186
return decorator(func)
3178-
return decorator
3187+
return cast(_WithAnnotatedDecorator, decorator)

‎cmd2/cmd2.py‎

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ class DisabledCommand(NamedTuple):
228228
This data is used to restore its functions when the command is enabled.
229229
"""
230230

231-
command_func: BoundCommandFunc
231+
command_func: BoundCommandFunc[...]
232232
help_func: Callable[[], Any] | None
233233
completer_func: BoundCompleter | None
234234

@@ -251,14 +251,14 @@ def __init__(self, cmd_app: "Cmd") -> None:
251251
self._parsers: dict[str, Cmd2ArgumentParser] = {}
252252

253253
@staticmethod
254-
def _fully_qualified_name(command_method: BoundCommandFunc) -> str:
254+
def _fully_qualified_name(command_method: BoundCommandFunc[...]) -> str:
255255
"""Return the fully qualified name of a method or None if a method wasn't passed in."""
256256
try:
257257
return f"{command_method.__module__}.{command_method.__qualname__}"
258258
except AttributeError:
259259
return ""
260260

261-
def __contains__(self, command_method: BoundCommandFunc) -> bool:
261+
def __contains__(self, command_method: BoundCommandFunc[...]) -> bool:
262262
"""Return whether a given method's parser is in self.
263263
264264
If the parser does not yet exist, it will be created if applicable.
@@ -267,7 +267,7 @@ def __contains__(self, command_method: BoundCommandFunc) -> bool:
267267
parser = self.get(command_method)
268268
return bool(parser)
269269

270-
def get(self, command_method: BoundCommandFunc) -> Cmd2ArgumentParser | None:
270+
def get(self, command_method: BoundCommandFunc[...]) -> Cmd2ArgumentParser | None:
271271
"""Return a given method's parser or None if the method is not argparse-based.
272272
273273
If the parser does not yet exist, it will be created.
@@ -300,7 +300,7 @@ def get(self, command_method: BoundCommandFunc) -> Cmd2ArgumentParser | None:
300300

301301
return self._parsers.get(full_method_name)
302302

303-
def remove(self, command_method: BoundCommandFunc) -> None:
303+
def remove(self, command_method: BoundCommandFunc[...]) -> None:
304304
"""Remove a given method's parser if it exists."""
305305
full_method_name = self._fully_qualified_name(command_method)
306306
if full_method_name in self._parsers:
@@ -919,7 +919,7 @@ def register_command_set(self, cmdset: CommandSet[Any]) -> None:
919919

920920
cmdset.on_register(self)
921921
methods = cast(
922-
list[tuple[str, BoundCommandFunc]],
922+
list[tuple[str, BoundCommandFunc[...]]],
923923
inspect.getmembers(
924924
cmdset,
925925
predicate=lambda meth: ( # type: ignore[arg-type]
@@ -1021,7 +1021,9 @@ def _build_parser(
10211021

10221022
return parser
10231023

1024-
def _install_command_function(self, command_func_name: str, command_method: BoundCommandFunc, context: str = "") -> None:
1024+
def _install_command_function(
1025+
self, command_func_name: str, command_method: BoundCommandFunc[...], context: str = ""
1026+
) -> None:
10251027
"""Install a new command function into the CLI.
10261028
10271029
:param command_func_name: name of command function to add
@@ -1086,8 +1088,8 @@ def unregister_command_set(self, cmdset: CommandSet[Any]) -> None:
10861088
cmdset.on_unregister()
10871089
self._unregister_subcommands(cmdset)
10881090

1089-
methods: list[tuple[str, BoundCommandFunc]] = cast(
1090-
list[tuple[str, BoundCommandFunc]],
1091+
methods: list[tuple[str, BoundCommandFunc[...]]] = cast(
1092+
list[tuple[str, BoundCommandFunc[...]]],
10911093
inspect.getmembers(
10921094
cmdset,
10931095
predicate=lambda meth: ( # type: ignore[arg-type]
@@ -1161,8 +1163,8 @@ def check_parser_uninstallable(parser: Cmd2ArgumentParser) -> None:
11611163
)
11621164
check_parser_uninstallable(subparser)
11631165

1164-
methods: list[tuple[str, BoundCommandFunc]] = cast(
1165-
list[tuple[str, BoundCommandFunc]],
1166+
methods: list[tuple[str, BoundCommandFunc[...]]] = cast(
1167+
list[tuple[str, BoundCommandFunc[...]]],
11661168
inspect.getmembers(
11671169
cmdset,
11681170
predicate=lambda meth: ( # type: ignore[arg-type]
@@ -3413,17 +3415,17 @@ def _restore_output(self, statement: Statement, saved_redir_state: utils.Redirec
34133415
self._cur_pipe_proc_reader = saved_redir_state.saved_pipe_proc_reader
34143416
self._redirecting = saved_redir_state.saved_redirecting
34153417

3416-
def get_command_func(self, command: str) -> BoundCommandFunc | None:
3418+
def get_command_func(self, command: str) -> BoundCommandFunc[...] | None:
34173419
"""Get the bound command function for a command.
34183420
34193421
:param command: the name of the command
34203422
:return: the bound function implementing the command, or None if not found
34213423
"""
34223424
command_func_name = constants.COMMAND_FUNC_PREFIX + command
34233425
command_func = getattr(self, command_func_name, None)
3424-
return cast(BoundCommandFunc, command_func) if callable(command_func) else None
3426+
return cast(BoundCommandFunc[...], command_func) if callable(command_func) else None
34253427

3426-
def _get_command_category(self, func: BoundCommandFunc) -> str:
3428+
def _get_command_category(self, func: BoundCommandFunc[...]) -> str:
34273429
"""Determine the category for a command.
34283430
34293431
:param func: the do_* function implementing the command

‎cmd2/types.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@
6969

7070
# A bound cmd2 command function (e.g. do_command).
7171
# The 'self' argument is already tied to an instance and is omitted.
72-
class BoundCommandFunc(Protocol):
72+
class BoundCommandFunc(Protocol[P]):
7373
"""Protocol for a command function bound to a command instance."""
7474

7575
__name__: str
7676
__qualname__: str
7777

78-
def __call__(self, *args: Any, **kwargs: Any) -> bool | None:
78+
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> bool | None:
7979
"""Invoke the bound command function."""
8080

8181

@@ -94,7 +94,7 @@ def __call__(self, __self: CmdOrSetT, /, *args: P.args, **kwargs: P.kwargs) -> b
9494
def __get__(self, instance: None, owner: Any = ...) -> "UnboundCommandFunc[CmdOrSetT, P]": ...
9595

9696
@overload
97-
def __get__(self, instance: CmdOrSetT, owner: Any = ...) -> BoundCommandFunc: ...
97+
def __get__(self, instance: CmdOrSetT, owner: Any = ...) -> BoundCommandFunc[P]: ...
9898

9999

100100
##################################################################################################

0 commit comments

Comments
 (0)