Skip to content

Commit 81072ed

Browse files
authored
fix(spy): gracefully degrade when a class's type hints can't be resolved at runtime (#47)
Fixes #46
1 parent f59ab66 commit 81072ed

File tree

2 files changed

+29
-14
lines changed

2 files changed

+29
-14
lines changed

decoy/spy.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@ class SpyConfig(NamedTuple):
2323
is_async: bool = False
2424

2525

26+
def _get_type_hints(obj: Any) -> Dict[str, Any]:
27+
"""Get type hints for an object, if possible.
28+
29+
The builtin `typing.get_type_hints` may fail at runtime,
30+
e.g. if a type is subscriptable according to mypy but not
31+
according to Python.
32+
"""
33+
try:
34+
return get_type_hints(obj)
35+
except Exception:
36+
return {}
37+
38+
2639
class BaseSpy:
2740
"""Spy object base class.
2841
@@ -88,23 +101,15 @@ def __getattr__(self, name: str) -> Any:
88101

89102
if isclass(self._spec):
90103
try:
91-
# NOTE: `get_type_hints` may fail at runtime,
92-
# e.g. if a type is subscriptable according to mypy but not
93-
# according to Python, `get_type_hints` will raise.
94-
# Rather than fail to create a spy with an inscrutable error,
95-
# gracefully fallback to a specification-less spy.
96-
hints = get_type_hints(self._spec)
97-
child_spec = getattr(
98-
self._spec,
99-
name,
100-
hints.get(name),
101-
)
104+
child_hint = _get_type_hints(self._spec).get(name)
102105
except Exception:
103-
pass
106+
child_hint = None
107+
108+
child_spec = getattr(self._spec, name, child_hint)
104109

105110
if isinstance(child_spec, property):
106-
hints = get_type_hints(child_spec.fget)
107-
child_spec = hints.get("return")
111+
child_spec = _get_type_hints(child_spec.fget).get("return")
112+
108113
elif isclass(self._spec) and isfunction(child_spec):
109114
# `iscoroutinefunction` does not work for `partial` on Python < 3.8
110115
# check before we wrap it

tests/test_spy.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,13 @@ async def test_create_nested_spy_using_non_runtime_type_hints() -> None:
265265
class _SomeClass:
266266
_property: "None[str]"
267267

268+
async def _do_something_async(self) -> None:
269+
pass
270+
268271
calls = []
269272
spy = create_spy(SpyConfig(spec=_SomeClass, handle_call=lambda c: calls.append(c)))
270273
spy._property.do_something(7, eight=8, nine=9)
274+
await spy._do_something_async()
271275

272276
assert calls == [
273277
SpyCall(
@@ -276,6 +280,12 @@ class _SomeClass:
276280
args=(7,),
277281
kwargs={"eight": 8, "nine": 9},
278282
),
283+
SpyCall(
284+
spy_id=id(spy._do_something_async),
285+
spy_name="_SomeClass._do_something_async",
286+
args=(),
287+
kwargs={},
288+
),
279289
]
280290

281291

0 commit comments

Comments
 (0)