Skip to content

Commit 557a11e

Browse files
committed
Implemented protcol class in annotated.py to match needs and remove casts
1 parent 5ed78e9 commit 557a11e

1 file changed

Lines changed: 28 additions & 19 deletions

File tree

‎cmd2/annotated.py‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,11 @@ def do_build(self, target: str, common: CommonArgs):
284284
NamedTuple,
285285
ParamSpec,
286286
Protocol,
287-
TypeAlias,
288287
TypedDict,
289288
TypeGuard,
290289
TypeVar,
291290
Union,
292291
Unpack,
293-
cast,
294292
get_args,
295293
get_origin,
296294
get_type_hints,
@@ -311,19 +309,27 @@ def do_build(self, target: str, common: CommonArgs):
311309
from .exceptions import Cmd2ArgparseError
312310
from .rich_utils import Cmd2HelpFormatter, HelpContent
313311
from .types import (
314-
BoundCommandFunc,
315-
CmdOrSet,
316312
CmdOrSetT,
317313
UnboundChoicesProvider,
318-
UnboundCommandFunc,
319314
UnboundCompleter,
320315
)
321316

322317
#: ``nargs`` values accepted by cmd2's patched ``add_argument`` (incl. ranged tuples).
323318
_NargsValue = int | str | tuple[int] | tuple[int, int] | tuple[int, float]
324319

325320

326-
_CommandFunc: TypeAlias = BoundCommandFunc | UnboundCommandFunc[CmdOrSet, Any]
321+
class _CommandFunc(Protocol):
322+
"""The command-function interface required by the annotation machinery.
323+
324+
The parser builders only depend on a command's name metadata and its
325+
boolean command result; they never depend on whether the function is
326+
bound or unbound, nor on the types of its parsed arguments.
327+
"""
328+
329+
__name__: str
330+
__qualname__: str
331+
332+
def __call__(self, *args: Any, **kwargs: Any) -> bool | None: ...
327333

328334

329335
class Cmd2ParserKwargs(TypedDict, total=False):
@@ -2928,17 +2934,22 @@ def handler(self_arg: Any, ns: Any) -> Any:
29282934

29292935

29302936
_CommandParams = ParamSpec("_CommandParams")
2931-
_CommandReturn = TypeVar("_CommandReturn")
2937+
2938+
2939+
class _AnnotatedCommand(_CommandFunc, Protocol[_CommandParams]):
2940+
"""A callable command method with the metadata used by ``with_annotated``."""
2941+
2942+
def __call__(self, *args: _CommandParams.args, **kwargs: _CommandParams.kwargs) -> bool | None: ...
29322943

29332944

29342945
class _WithAnnotatedDecorator(Protocol):
29352946
"""The signature-preserving decorator ``with_annotated(...)`` returns (generic per call)."""
29362947

2937-
def __call__(self, fn: Callable[_CommandParams, _CommandReturn], /) -> Callable[_CommandParams, _CommandReturn]: ...
2948+
def __call__(self, fn: _AnnotatedCommand[_CommandParams], /) -> _AnnotatedCommand[_CommandParams]: ...
29382949

29392950

29402951
@overload
2941-
def with_annotated(func: Callable[_CommandParams, _CommandReturn]) -> Callable[_CommandParams, _CommandReturn]: ...
2952+
def with_annotated(func: _AnnotatedCommand[_CommandParams], /) -> _AnnotatedCommand[_CommandParams]: ...
29422953

29432954

29442955
@overload
@@ -2965,7 +2976,7 @@ def with_annotated(
29652976

29662977

29672978
def with_annotated(
2968-
func: Callable[_CommandParams, _CommandReturn] | None = None,
2979+
func: _AnnotatedCommand[_CommandParams] | None = None,
29692980
*,
29702981
ns_provider: Callable[..., argparse.Namespace] | None = None,
29712982
preserve_quotes: bool = False,
@@ -2983,7 +2994,7 @@ def with_annotated(
29832994
subcommand_title: str | None = None,
29842995
subcommand_description: str | None = None,
29852996
**parser_kwargs: Unpack[Cmd2ParserKwargs],
2986-
) -> Callable[_CommandParams, _CommandReturn] | _WithAnnotatedDecorator:
2997+
) -> _AnnotatedCommand[_CommandParams] | _WithAnnotatedDecorator:
29872998
"""Decorate a ``do_*`` method to build its argparse parser from type annotations.
29882999
29893000
:param func: the command function (when used without parentheses)
@@ -3044,11 +3055,9 @@ def with_annotated(
30443055
subcommand_description=subcommand_description,
30453056
)
30463057

3047-
def decorator(fn: Callable[_CommandParams, _CommandReturn]) -> Callable[_CommandParams, _CommandReturn]:
3048-
# ``with_annotated`` is publicly typed as signature-preserving. Its runtime contract is
3049-
# narrower: decorated functions must be cmd2 command methods. Keep that narrowing local
3050-
# to the parser-building machinery while retaining the callable's public signature.
3051-
command_func = cast(_CommandFunc, fn)
3058+
def decorator(
3059+
command_func: _AnnotatedCommand[_CommandParams],
3060+
) -> _AnnotatedCommand[_CommandParams]:
30523061
if with_unknown_args:
30533062
unknown_param = inspect.signature(command_func).parameters.get("_unknown")
30543063
if unknown_param is None:
@@ -3078,7 +3087,7 @@ def decorator(fn: Callable[_CommandParams, _CommandReturn]) -> Callable[_Command
30783087
parser_source=subcmd_parser_builder,
30793088
)
30803089
setattr(handler, constants.SUBCOMMAND_ATTR_SPEC, subcommand_spec)
3081-
return cast(Callable[_CommandParams, _CommandReturn], handler)
3090+
return handler
30823091

30833092
command_name = command_func.__name__[len(constants.COMMAND_FUNC_PREFIX) :]
30843093

@@ -3155,9 +3164,9 @@ def cmd_wrapper(*args: Any, **kwargs: Any) -> bool | None:
31553164
)
31563165
setattr(cmd_wrapper, constants.ARGPARSE_COMMAND_ATTR_SPEC, argparse_command_spec)
31573166

3158-
return cast(Callable[_CommandParams, _CommandReturn], cmd_wrapper)
3167+
return cmd_wrapper
31593168

31603169
# Support both @with_annotated and @with_annotated(...)
31613170
if func is not None:
31623171
return decorator(func)
3163-
return cast(_WithAnnotatedDecorator, decorator)
3172+
return decorator

0 commit comments

Comments
 (0)