Skip to content

Commit 6a6868a

Browse files
authored
fix(spy): prefix internal properties with _decoy (#150)
Closes #144 by reducing the risk of attribute collision
1 parent 6481543 commit 6a6868a

File tree

1 file changed

+45
-37
lines changed

1 file changed

+45
-37
lines changed

decoy/spy.py

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,21 @@ def __init__(
3232
spy_creator: "SpyCreator",
3333
) -> None:
3434
"""Initialize a BaseSpy from a call handler and an optional spec object."""
35-
super().__setattr__("_core", core)
36-
super().__setattr__("_call_handler", call_handler)
37-
super().__setattr__("_spy_creator", spy_creator)
38-
super().__setattr__("_spy_children", {})
39-
super().__setattr__("_spy_property_values", {})
40-
super().__setattr__("__signature__", self._core.signature)
35+
super().__setattr__("_decoy_spy_core", core)
36+
super().__setattr__("_decoy_spy_call_handler", call_handler)
37+
super().__setattr__("_decoy_spy_creator", spy_creator)
38+
super().__setattr__("_decoy_spy_children", {})
39+
super().__setattr__("_decoy_spy_property_values", {})
40+
super().__setattr__("__signature__", self._decoy_spy_core.signature)
4141

4242
@property # type: ignore[misc]
4343
def __class__(self) -> Any:
4444
"""Ensure Spy can pass `instanceof` checks."""
45-
return self._core.class_type or type(self)
45+
return self._decoy_spy_core.class_type or type(self)
4646

4747
def __enter__(self) -> Any:
4848
"""Allow a spy to be used as a context manager."""
49-
enter_spy = self._get_or_create_child_spy("__enter__")
49+
enter_spy = self._decoy_spy_get_or_create_child_spy("__enter__")
5050
return enter_spy()
5151

5252
def __exit__(
@@ -56,12 +56,14 @@ def __exit__(
5656
traceback: Optional[TracebackType],
5757
) -> Optional[bool]:
5858
"""Allow a spy to be used as a context manager."""
59-
exit_spy = self._get_or_create_child_spy("__exit__")
59+
exit_spy = self._decoy_spy_get_or_create_child_spy("__exit__")
6060
return cast(Optional[bool], exit_spy(exc_type, exc_value, traceback))
6161

6262
async def __aenter__(self) -> Any:
6363
"""Allow a spy to be used as an async context manager."""
64-
enter_spy = self._get_or_create_child_spy("__aenter__", child_is_async=True)
64+
enter_spy = self._decoy_spy_get_or_create_child_spy(
65+
"__aenter__", child_is_async=True
66+
)
6567
return await enter_spy()
6668

6769
async def __aexit__(
@@ -71,49 +73,53 @@ async def __aexit__(
7173
traceback: Optional[TracebackType],
7274
) -> Optional[bool]:
7375
"""Allow a spy to be used as a context manager."""
74-
exit_spy = self._get_or_create_child_spy("__aexit__", child_is_async=True)
76+
exit_spy = self._decoy_spy_get_or_create_child_spy(
77+
"__aexit__", child_is_async=True
78+
)
7579
return cast(Optional[bool], await exit_spy(exc_type, exc_value, traceback))
7680

7781
def __repr__(self) -> str:
7882
"""Get a helpful string representation of the spy."""
79-
return f"<Decoy mock `{self._core.full_name}`>"
83+
return f"<Decoy mock `{self._decoy_spy_core.full_name}`>"
8084

8185
def __getattr__(self, name: str) -> Any:
8286
"""Get a property of the spy, always returning a child spy."""
8387
# do not attempt to mock magic methods
8488
if name.startswith("__") and name.endswith("__"):
8589
return super().__getattribute__(name)
8690

87-
return self._get_or_create_child_spy(name)
91+
return self._decoy_spy_get_or_create_child_spy(name)
8892

8993
def __setattr__(self, name: str, value: Any) -> None:
9094
"""Set a property on the spy, recording the call."""
9195
event = SpyEvent(
92-
spy=self._core.info,
96+
spy=self._decoy_spy_core.info,
9397
payload=SpyPropAccess(
9498
prop_name=name,
9599
access_type=PropAccessType.SET,
96100
value=value,
97101
),
98102
)
99-
self._call_handler.handle(event)
100-
self._spy_property_values[name] = value
103+
self._decoy_spy_call_handler.handle(event)
104+
self._decoy_spy_property_values[name] = value
101105

102106
def __delattr__(self, name: str) -> None:
103107
"""Delete a property on the spy, recording the call."""
104108
event = SpyEvent(
105-
spy=self._core.info,
109+
spy=self._decoy_spy_core.info,
106110
payload=SpyPropAccess(prop_name=name, access_type=PropAccessType.DELETE),
107111
)
108-
self._call_handler.handle(event)
109-
self._spy_property_values.pop(name, None)
112+
self._decoy_spy_call_handler.handle(event)
113+
self._decoy_spy_property_values.pop(name, None)
110114

111-
def _get_or_create_child_spy(self, name: str, child_is_async: bool = False) -> Any:
115+
def _decoy_spy_get_or_create_child_spy(
116+
self, name: str, child_is_async: bool = False
117+
) -> Any:
112118
"""Lazily construct a child spy, basing it on type hints if available."""
113119
# check for any stubbed behaviors for property getter
114-
get_result = self._call_handler.handle(
120+
get_result = self._decoy_spy_call_handler.handle(
115121
SpyEvent(
116-
spy=self._core.info,
122+
spy=self._decoy_spy_core.info,
117123
payload=SpyPropAccess(
118124
prop_name=name,
119125
access_type=PropAccessType.GET,
@@ -124,30 +130,32 @@ def _get_or_create_child_spy(self, name: str, child_is_async: bool = False) -> A
124130
if get_result:
125131
return get_result.value
126132

127-
if name in self._spy_property_values:
128-
return self._spy_property_values[name]
133+
if name in self._decoy_spy_property_values:
134+
return self._decoy_spy_property_values[name]
129135

130136
# return previously constructed (and cached) child spies
131-
if name in self._spy_children:
132-
return self._spy_children[name]
137+
if name in self._decoy_spy_children:
138+
return self._decoy_spy_children[name]
133139

134-
child_core = self._core.create_child_core(name=name, is_async=child_is_async)
135-
child_spy = self._spy_creator.create(core=child_core)
136-
self._spy_children[name] = child_spy
140+
child_core = self._decoy_spy_core.create_child_core(
141+
name=name, is_async=child_is_async
142+
)
143+
child_spy = self._decoy_spy_creator.create(core=child_core)
144+
self._decoy_spy_children[name] = child_spy
137145

138146
return child_spy
139147

140-
def _call(self, *args: Any, **kwargs: Any) -> Any:
141-
bound_args, bound_kwargs = self._core.bind_args(*args, **kwargs)
148+
def _decoy_spy_call(self, *args: Any, **kwargs: Any) -> Any:
149+
bound_args, bound_kwargs = self._decoy_spy_core.bind_args(*args, **kwargs)
142150
call = SpyEvent(
143-
spy=self._core.info,
151+
spy=self._decoy_spy_core.info,
144152
payload=SpyCall(
145153
args=bound_args,
146154
kwargs=bound_kwargs,
147155
),
148156
)
149157

150-
result = self._call_handler.handle(call)
158+
result = self._decoy_spy_call_handler.handle(call)
151159
return result.value if result else None
152160

153161

@@ -156,7 +164,7 @@ class AsyncSpy(BaseSpy):
156164

157165
async def __call__(self, *args: Any, **kwargs: Any) -> Any:
158166
"""Handle a call to the spy asynchronously."""
159-
result = self._call(*args, **kwargs)
167+
result = self._decoy_spy_call(*args, **kwargs)
160168
return (await result) if inspect.iscoroutine(result) else result
161169

162170

@@ -165,7 +173,7 @@ class Spy(BaseSpy):
165173

166174
def __call__(self, *args: Any, **kwargs: Any) -> Any:
167175
"""Handle a call to the spy."""
168-
return self._call(*args, **kwargs)
176+
return self._decoy_spy_call(*args, **kwargs)
169177

170178

171179
AnySpy = Union[AsyncSpy, Spy]
@@ -175,7 +183,7 @@ class SpyCreator:
175183
"""Spy factory."""
176184

177185
def __init__(self, call_handler: CallHandler) -> None:
178-
self._call_handler = call_handler
186+
self._decoy_spy_call_handler = call_handler
179187

180188
@overload
181189
def create(self, *, core: SpyCore) -> AnySpy:
@@ -208,5 +216,5 @@ def create(
208216
return spy_cls(
209217
core=core,
210218
spy_creator=self,
211-
call_handler=self._call_handler,
219+
call_handler=self._decoy_spy_call_handler,
212220
)

0 commit comments

Comments
 (0)