Skip to content

Commit f5dc60d

Browse files
authored
fix(spy): match inspect.signature for staticmethods (#51)
1 parent 846c405 commit f5dc60d

File tree

3 files changed

+30
-3
lines changed

3 files changed

+30
-3
lines changed

decoy/spy.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Classes in this module are heavily inspired by the
44
[unittest.mock library](https://docs.python.org/3/library/unittest.mock.html).
55
"""
6-
from inspect import isclass, iscoroutinefunction, isfunction, signature
6+
from inspect import getattr_static, isclass, iscoroutinefunction, isfunction, signature
77
from functools import partial
88
from typing import get_type_hints, Any, Callable, Dict, NamedTuple, Optional
99

@@ -105,15 +105,19 @@ def __getattr__(self, name: str) -> Any:
105105
except Exception:
106106
child_hint = None
107107

108-
child_spec = getattr(self._spec, name, child_hint)
108+
child_spec = getattr_static(self._spec, name, child_hint)
109109

110110
if isinstance(child_spec, property):
111111
child_spec = _get_type_hints(child_spec.fget).get("return")
112112

113+
if isinstance(child_spec, staticmethod):
114+
child_spec = child_spec.__func__
115+
113116
elif isclass(self._spec) and isfunction(child_spec):
114117
# `iscoroutinefunction` does not work for `partial` on Python < 3.8
115118
# check before we wrap it
116119
child_is_async = iscoroutinefunction(child_spec)
120+
117121
# consume the `self` argument of the method to ensure proper
118122
# signature reporting by wrapping it in a partial
119123
child_spec = partial(child_spec, None) # type: ignore[arg-type]

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ theme:
3131
- media: "(prefers-color-scheme: light)"
3232
scheme: default
3333
primary: black
34+
accent: indigo
3435
toggle:
3536
icon: material/weather-sunny
3637
name: Switch to dark mode
3738
- media: "(prefers-color-scheme: dark)"
3839
scheme: slate
3940
primary: amber
41+
accent: yellow
4042
toggle:
4143
icon: material/weather-night
4244
name: Switch to light mode

tests/test_spy.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from typing import Any
55

66
from decoy.spy_calls import SpyCall
7-
from decoy.spy import create_spy, AsyncSpy, SpyConfig
7+
from decoy.spy import create_spy, AsyncSpy, Spy, SpyConfig
88

99
from .common import (
1010
noop,
@@ -349,6 +349,27 @@ def test_spy_matches_signature() -> None:
349349
)
350350

351351

352+
def test_spy_matches_static_signature() -> None:
353+
"""It should pass `inspect.signature` checks on static methods."""
354+
355+
class _StaticClass:
356+
@staticmethod
357+
def foo(bar: int) -> float:
358+
raise NotImplementedError()
359+
360+
@staticmethod
361+
async def bar(baz: str) -> float:
362+
raise NotImplementedError()
363+
364+
class_spy = create_spy(SpyConfig(spec=_StaticClass, handle_call=noop))
365+
actual_instance = _StaticClass()
366+
367+
assert inspect.signature(class_spy.foo) == inspect.signature(actual_instance.foo)
368+
assert inspect.signature(class_spy.bar) == inspect.signature(actual_instance.bar)
369+
assert isinstance(class_spy.foo, Spy)
370+
assert isinstance(class_spy.bar, AsyncSpy)
371+
372+
352373
def test_spy_repr() -> None:
353374
"""It should have an informative repr."""
354375
class_spy = create_spy(SpyConfig(spec=SomeClass, handle_call=noop))

0 commit comments

Comments
 (0)