6
6
from __future__ import annotations
7
7
from dataclasses import dataclass
8
8
from inspect import isclass , iscoroutinefunction
9
- from typing import get_type_hints , Any , Callable , Dict , Optional , Tuple
9
+ from typing import get_type_hints , Any , Callable , Dict , Optional , Tuple , Type
10
10
11
11
12
12
@dataclass (frozen = True )
@@ -20,9 +20,21 @@ class SpyCall:
20
20
"""
21
21
22
22
spy_id : int
23
+ spy_name : str
23
24
args : Tuple [Any , ...]
24
25
kwargs : Dict [str , Any ]
25
26
27
+ def __str__ (self ) -> str :
28
+ """Stringify the call to something human readable.
29
+
30
+ `SpyCall(spy_id=42, spy_name="name", args=(1,), kwargs={"foo": False})`
31
+ would stringify as `"name(1, foo=False)"`
32
+ """
33
+ args_list = [repr (arg ) for arg in self .args ]
34
+ kwargs_list = [f"{ key } ={ repr (val )} " for key , val in self .kwargs .items ()]
35
+
36
+ return f"{ self .spy_name } ({ ', ' .join (args_list + kwargs_list )} )"
37
+
26
38
27
39
CallHandler = Callable [[SpyCall ], Any ]
28
40
@@ -34,8 +46,14 @@ class BaseSpy:
34
46
- Lazily constructs child spies when an attribute is accessed
35
47
"""
36
48
37
- def __init__ (self , handle_call : CallHandler , spec : Optional [Any ] = None ) -> None :
49
+ def __init__ (
50
+ self ,
51
+ handle_call : CallHandler ,
52
+ spec : Optional [Any ] = None ,
53
+ name : Optional [str ] = None ,
54
+ ) -> None :
38
55
"""Initialize a BaseSpy from a call handler and an optional spec object."""
56
+ self ._name = name or (spec .__name__ if spec is not None else "spy" )
39
57
self ._spec = spec
40
58
self ._handle_call : CallHandler = handle_call
41
59
self ._spy_children : Dict [str , BaseSpy ] = {}
@@ -73,6 +91,7 @@ def __getattr__(self, name: str) -> Any:
73
91
spy = create_spy (
74
92
handle_call = self ._handle_call ,
75
93
spec = child_spec ,
94
+ name = f"{ self ._name } .{ name } " ,
76
95
)
77
96
78
97
self ._spy_children [name ] = spy
@@ -85,28 +104,31 @@ class Spy(BaseSpy):
85
104
86
105
def __call__ (self , * args : Any , ** kwargs : Any ) -> Any :
87
106
"""Handle a call to the spy."""
88
- return self ._handle_call (SpyCall (id (self ), args , kwargs ))
107
+ return self ._handle_call (SpyCall (id (self ), self . _name , args , kwargs ))
89
108
90
109
91
- class AsyncSpy (Spy ):
110
+ class AsyncSpy (BaseSpy ):
92
111
"""An object that records all async. calls made to itself and its children."""
93
112
94
113
async def __call__ (self , * args : Any , ** kwargs : Any ) -> Any :
95
114
"""Handle a call to the spy asynchronously."""
96
- return self ._handle_call (SpyCall (id (self ), args , kwargs ))
115
+ return self ._handle_call (SpyCall (id (self ), self . _name , args , kwargs ))
97
116
98
117
99
118
def create_spy (
100
119
handle_call : CallHandler ,
101
120
spec : Optional [Any ] = None ,
102
121
is_async : bool = False ,
122
+ name : Optional [str ] = None ,
103
123
) -> Any :
104
124
"""Create a Spy from a spec.
105
125
106
126
Functions and classes passed to `spec` will be inspected (and have any type
107
127
annotations inspected) to ensure `AsyncSpy`'s are returned where necessary.
108
128
"""
129
+ _SpyCls : Type [BaseSpy ] = Spy
130
+
109
131
if iscoroutinefunction (spec ) or is_async is True :
110
- return AsyncSpy ( handle_call )
132
+ _SpyCls = AsyncSpy
111
133
112
- return Spy (handle_call = handle_call , spec = spec )
134
+ return _SpyCls (handle_call = handle_call , spec = spec , name = name )
0 commit comments