66from __future__ import annotations
77from dataclasses import dataclass
88from 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
1010
1111
1212@dataclass (frozen = True )
@@ -20,9 +20,21 @@ class SpyCall:
2020 """
2121
2222 spy_id : int
23+ spy_name : str
2324 args : Tuple [Any , ...]
2425 kwargs : Dict [str , Any ]
2526
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+
2638
2739CallHandler = Callable [[SpyCall ], Any ]
2840
@@ -34,8 +46,14 @@ class BaseSpy:
3446 - Lazily constructs child spies when an attribute is accessed
3547 """
3648
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 :
3855 """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" )
3957 self ._spec = spec
4058 self ._handle_call : CallHandler = handle_call
4159 self ._spy_children : Dict [str , BaseSpy ] = {}
@@ -73,6 +91,7 @@ def __getattr__(self, name: str) -> Any:
7391 spy = create_spy (
7492 handle_call = self ._handle_call ,
7593 spec = child_spec ,
94+ name = f"{ self ._name } .{ name } " ,
7695 )
7796
7897 self ._spy_children [name ] = spy
@@ -85,28 +104,31 @@ class Spy(BaseSpy):
85104
86105 def __call__ (self , * args : Any , ** kwargs : Any ) -> Any :
87106 """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 ))
89108
90109
91- class AsyncSpy (Spy ):
110+ class AsyncSpy (BaseSpy ):
92111 """An object that records all async. calls made to itself and its children."""
93112
94113 async def __call__ (self , * args : Any , ** kwargs : Any ) -> Any :
95114 """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 ))
97116
98117
99118def create_spy (
100119 handle_call : CallHandler ,
101120 spec : Optional [Any ] = None ,
102121 is_async : bool = False ,
122+ name : Optional [str ] = None ,
103123) -> Any :
104124 """Create a Spy from a spec.
105125
106126 Functions and classes passed to `spec` will be inspected (and have any type
107127 annotations inspected) to ensure `AsyncSpy`'s are returned where necessary.
108128 """
129+ _SpyCls : Type [BaseSpy ] = Spy
130+
109131 if iscoroutinefunction (spec ) or is_async is True :
110- return AsyncSpy ( handle_call )
132+ _SpyCls = AsyncSpy
111133
112- return Spy (handle_call = handle_call , spec = spec )
134+ return _SpyCls (handle_call = handle_call , spec = spec , name = name )
0 commit comments