99import datetime
1010import decimal
1111import enum
12+ import functools
1213import inspect
1314import types
1415import uuid
@@ -150,7 +151,11 @@ def _func_positional_only_mixed(self, x: str, /, y: int, *, z: int = 0) -> None:
150151# while hints on real arguments must still raise. ``noqa: F821`` marks the intentionally-undefined names.
151152def _func_unresolvable_self (self : "UnimportableCmd" , name : str , count : int = 1 ) -> None : ... # noqa: F821
152153def _func_unresolvable_cmd2_statement (self , cmd2_statement : "UnimportableStatement" , name : str , count : int = 1 ) -> None : ... # noqa: F821
153- def _func_unresolvable_cmd2_subcommand_func (self , cmd2_subcommand_func : "UnimportableHandler" , verbose : bool = False ) -> None : ... # noqa: F821
154+ def _func_unresolvable_cmd2_subcommand_func (
155+ self ,
156+ cmd2_subcommand_func : "UnimportableHandler" , # noqa: F821
157+ verbose : bool = False ,
158+ ) -> None : ...
154159def _func_unresolvable_return (self , name : str ) -> "UnimportableReturn" : ... # noqa: F821
155160def _func_unresolvable_argument (self , name : "NonExistentType" ) -> None : ... # noqa: F821
156161def _func_unresolvable_argument_base (self , cmd2_subcommand_func , name : "NonExistentType" ) -> None : ... # noqa: F821
@@ -169,6 +174,25 @@ def _arg_names_via_base_command(func: Any) -> set[str]:
169174 return {arg .name for arg in _resolve_parameters (func , base_command = True )}
170175
171176
177+ def _wrap_in_foreign_module (func : Any ) -> Any :
178+ """Wrap *func* as a ``functools.wraps`` decorator would, but give the wrapper a ``__globals__``
179+ that lacks this module's names. This mimics a user decorator defined in *another* module and
180+ stacked under ``@with_annotated``: ``functools.wraps`` copies ``__annotations__`` but not
181+ ``__globals__``, so a forward reference can only be resolved via ``__wrapped__`` (the original
182+ function's module), not the wrapper's. Rebinding the wrapper's globals is the single-module way
183+ to recreate that cross-module split.
184+ """
185+
186+ def wrapper (* args : Any , ** kwargs : Any ) -> Any :
187+ return func (* args , ** kwargs )
188+
189+ foreign = types .FunctionType (
190+ wrapper .__code__ , {"__builtins__" : __builtins__ }, func .__name__ , wrapper .__defaults__ , wrapper .__closure__
191+ )
192+ functools .update_wrapper (foreign , func ) # copies __annotations__ etc. and sets __wrapped__ = func
193+ return foreign
194+
195+
172196def _provider (cmd : cmd2 .Cmd ):
173197 return []
174198
@@ -655,7 +679,9 @@ def test_self_only_method_produces_empty_parser(self) -> None:
655679 [
656680 pytest .param (_func_unresolvable_self , _arg_names_via_parser , {"name" , "count" }, id = "self" ),
657681 pytest .param (_func_unresolvable_cmd2_statement , _arg_names_via_parser , {"name" , "count" }, id = "cmd2_statement" ),
658- pytest .param (_func_unresolvable_cmd2_subcommand_func , _arg_names_via_base_command , {"verbose" }, id = "cmd2_subcommand_func" ),
682+ pytest .param (
683+ _func_unresolvable_cmd2_subcommand_func , _arg_names_via_base_command , {"verbose" }, id = "cmd2_subcommand_func"
684+ ),
659685 pytest .param (_func_unresolvable_return , _arg_names_via_parser , {"name" }, id = "return" ),
660686 ],
661687 )
@@ -684,6 +710,24 @@ def test_unresolvable_hint_on_real_argument_raises(self, func, resolve_arg_names
684710 with pytest .raises (TypeError , match = "Failed to resolve type hints" ):
685711 resolve_arg_names (func )
686712
713+ def test_forward_ref_resolves_through_functools_wraps_wrapper (self ) -> None :
714+ """A forward reference must resolve against the *original* function's module even when the
715+ function reaching the parser builder is a ``functools.wraps`` wrapper defined in another
716+ module (e.g. a user decorator stacked under ``@with_annotated``). ``functools.wraps`` copies
717+ ``__annotations__`` but not ``__globals__``, so resolution must follow ``__wrapped__`` --
718+ mirroring what ``typing.get_type_hints`` does for a bare function.
719+ """
720+
721+ def do_path (self , target : "Path" , count : int = 1 ):
722+ pass
723+
724+ # The wrapper carries a foreign global namespace lacking ``Path``; resolution must use the
725+ # unwrapped function's globals (this module) instead, or it raises "Failed to resolve".
726+ wrapped = _wrap_in_foreign_module (do_path )
727+ parser = build_parser_from_function (wrapped )
728+ dests = {action .dest for action in parser ._actions if action .dest != "help" }
729+ assert dests == {"target" , "count" }
730+
687731 def test_dest_param_raises (self ) -> None :
688732 with pytest .raises (ValueError , match = "dest" ):
689733 build_parser_from_function (_make_func (str , name = "dest" ))
0 commit comments