4
4
[unittest.mock library](https://docs.python.org/3/library/unittest.mock.html).
5
5
"""
6
6
from __future__ import annotations
7
- from inspect import isclass , iscoroutinefunction
7
+ from inspect import isclass , iscoroutinefunction , isfunction , signature
8
+ from functools import partial
8
9
from typing import get_type_hints , Any , Callable , Dict , NamedTuple , Optional , Tuple
9
10
10
11
@@ -39,6 +40,7 @@ class SpyConfig(NamedTuple):
39
40
handle_call : CallHandler
40
41
spec : Optional [Any ] = None
41
42
name : Optional [str ] = None
43
+ module_name : Optional [str ] = None
42
44
is_async : bool = False
43
45
44
46
@@ -54,13 +56,25 @@ def __init__(
54
56
handle_call : CallHandler ,
55
57
spec : Optional [Any ] = None ,
56
58
name : Optional [str ] = None ,
59
+ module_name : Optional [str ] = None ,
57
60
) -> None :
58
61
"""Initialize a BaseSpy from a call handler and an optional spec object."""
59
- self ._name = name or (spec .__name__ if spec is not None else "spy" )
60
62
self ._spec = spec
61
63
self ._handle_call : CallHandler = handle_call
62
64
self ._spy_children : Dict [str , BaseSpy ] = {}
63
65
66
+ self ._name = name or (spec .__name__ if spec is not None else "spy" )
67
+ self ._module_name = module_name
68
+
69
+ if module_name is None and spec is not None and hasattr (spec , "__module__" ):
70
+ self ._module_name = spec .__module__
71
+
72
+ # ensure spy can pass inspect.signature checks
73
+ try :
74
+ self .__signature__ = signature (spec ) # type: ignore[arg-type]
75
+ except Exception :
76
+ pass
77
+
64
78
@property # type: ignore[misc]
65
79
def __class__ (self ) -> Any :
66
80
"""Ensure Spy can pass `instanceof` checks."""
@@ -69,15 +83,29 @@ def __class__(self) -> Any:
69
83
70
84
return type (self )
71
85
86
+ def __repr__ (self ) -> str :
87
+ """Get a helpful string representation of the spy."""
88
+ name = self ._name
89
+ if self ._module_name :
90
+ name = f"{ self ._module_name } .{ name } "
91
+
92
+ return f"<Decoy mock of { name } >" if self ._spec else "<Decoy spy function>"
93
+
72
94
def __getattr__ (self , name : str ) -> Any :
73
95
"""Get a property of the spy.
74
96
75
97
Lazily constructs child spies, basing them on type hints if available.
76
98
"""
99
+ # do not attempt to mock magic methods
100
+ if name .startswith ("__" ) and name .endswith ("__" ):
101
+ return super ().__getattribute__ (name )
102
+
103
+ # return previously constructed (and cached) child spies
77
104
if name in self ._spy_children :
78
105
return self ._spy_children [name ]
79
106
80
107
child_spec = None
108
+ child_is_async = False
81
109
82
110
if isclass (self ._spec ):
83
111
try :
@@ -98,12 +126,21 @@ def __getattr__(self, name: str) -> Any:
98
126
if isinstance (child_spec , property ):
99
127
hints = get_type_hints (child_spec .fget )
100
128
child_spec = hints .get ("return" )
129
+ elif isclass (self ._spec ) and isfunction (child_spec ):
130
+ # `iscoroutinefunction` does not work for `partial` on Python < 3.8
131
+ # check before we wrap it
132
+ child_is_async = iscoroutinefunction (child_spec )
133
+ # consume the `self` argument of the method to ensure proper
134
+ # signature reporting by wrapping it in a partial
135
+ child_spec = partial (child_spec , None ) # type: ignore[arg-type]
101
136
102
137
spy = create_spy (
103
138
config = SpyConfig (
104
139
handle_call = self ._handle_call ,
105
140
spec = child_spec ,
106
141
name = f"{ self ._name } .{ name } " ,
142
+ module_name = self ._module_name ,
143
+ is_async = child_is_async ,
107
144
),
108
145
)
109
146
@@ -137,7 +174,12 @@ def create_spy(config: SpyConfig) -> Any:
137
174
Functions and classes passed to `spec` will be inspected (and have any type
138
175
annotations inspected) to ensure `AsyncSpy`'s are returned where necessary.
139
176
"""
140
- handle_call , spec , name , is_async = config
177
+ handle_call , spec , name , module_name , is_async = config
141
178
_SpyCls = AsyncSpy if iscoroutinefunction (spec ) or is_async is True else Spy
142
179
143
- return _SpyCls (handle_call = handle_call , spec = spec , name = name )
180
+ return _SpyCls (
181
+ handle_call = handle_call ,
182
+ spec = spec ,
183
+ name = name ,
184
+ module_name = module_name ,
185
+ )
0 commit comments