Skip to content

Commit 68e2f28

Browse files
committed
fix: enforce effective signature for functools.partial patch targets
`functools.partial` is not a `FunctionType`, so `mock.create_autospec` fell back to a generic `MagicMock(spec=...)` with no call-signature checking, even when `autospec=True` was passed explicitly. Wrap the partial in a thin function whose `__signature__` is set to `inspect.signature(partial)`, which already computes the post-partial effective signature. `create_autospec` then treats it as a plain function and wires up enforcement correctly. Covers both module-level partials and instance-attribute partials wrapping bound methods.
1 parent 0c5c4f6 commit 68e2f28

4 files changed

Lines changed: 71 additions & 0 deletions

File tree

mockey/fixture.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from __future__ import annotations
1919

2020
import functools
21+
import inspect
2122
import typing
2223
from typing import TYPE_CHECKING, Any, TypeVar
2324
from unittest import mock
@@ -48,6 +49,18 @@ def _lazy_autospec_method(
4849
if mocked_method._mock_check_sig.__dict__.get("autospeced"):
4950
return
5051

52+
# functools.partial is not a FunctionType, so create_autospec falls back to
53+
# a generic MagicMock(spec=...) with no call-signature enforcement. Wrap it
54+
# in a thin function that carries the effective (post-partial) signature so
55+
# create_autospec wires up enforcement correctly.
56+
if isinstance(original_method, functools.partial):
57+
58+
def _sig_wrapper(*args: Any, **kwargs: Any) -> None:
59+
pass
60+
61+
_sig_wrapper.__signature__ = inspect.signature(original_method) # type: ignore[attr-defined]
62+
original_method = _sig_wrapper
63+
5164
_lazy_autospec: Any = mock.create_autospec(original_method)
5265
if eat_self:
5366
# consume self argument.

tests/test_issues_exist.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,33 @@ def test_patch_partialmethod_autospec_true(self):
198198
client = utils._ClientWithPartialMethod()
199199
self.assertRaises(TypeError, client._send_post_request, "https://globex.com")
200200

201+
def test_patch_partial_instance_not_enforced(self):
202+
# Even with explicit autospec=True, upstream does not enforce the
203+
# effective signature of an instance-attribute partial; any call succeeds.
204+
client = utils._ClientWithPartialMethod()
205+
with (
206+
self._restored_patch(),
207+
mock.patch.object(client, "_send_get_request", autospec=True),
208+
):
209+
client._send_get_request() # missing url
210+
# too many args
211+
client._send_get_request(
212+
"https://dunder-mifflin.com",
213+
"worlds_best_boss",
214+
"thats_what_she_said",
215+
)
216+
217+
def test_patch_partial_module_level_not_enforced(self):
218+
# Even with explicit autospec=True, upstream does not enforce the
219+
# effective signature of a module-level partial; any call succeeds.
220+
with (
221+
self._restored_patch(),
222+
mock.patch("tests.utils._send_post_request", autospec=True),
223+
):
224+
utils._send_post_request() # missing url
225+
# too many args
226+
utils._send_post_request("https://weyland-yutani.corp", "xenomorph_egg", "facehugger")
227+
201228
def test_constructor_autospec_not_enforced(self):
202229
# Without the fixture, constructor signature is NOT enforced.
203230
# mock.Mock / mock.MagicMock do not have an autospec argument

tests/test_issues_fixed.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,27 @@ def test_patch_partialmethod(self):
175175
# Unknown kwarg raises TypeError.
176176
self.assertRaises(TypeError, client._send_post_request, "https://initech.com", foo="lish")
177177

178+
def test_patch_partial_instance(self):
179+
# The effective signature of _send_get_request is (url, payload=None);
180+
# "GET" is pre-filled by partial.
181+
client = utils._ClientWithPartialMethod()
182+
with mock.patch.object(client, "_send_get_request"):
183+
client._send_get_request("https://prestige-worldwide.com")
184+
client._send_get_request("https://prestige-worldwide.com", payload="catalina_wine_mixer")
185+
186+
self.assertRaises(TypeError, client._send_get_request)
187+
self.assertRaises(TypeError, client._send_get_request, "https://prestige-worldwide.com", foo="lish")
188+
189+
def test_patch_partial_function(self):
190+
# The effective signature of _send_post_request is (url, payload=None);
191+
# "POST" is pre-filled by partial.
192+
with mock.patch("tests.utils._send_post_request"):
193+
utils._send_post_request("https://aperture.science")
194+
utils._send_post_request("https://aperture.science", payload="the_cake_is_a_lie")
195+
196+
self.assertRaises(TypeError, utils._send_post_request)
197+
self.assertRaises(TypeError, utils._send_post_request, "https://aperture.science", foo="lish")
198+
178199
def test_constructor_autospec(self):
179200
# Correct usage.
180201
m = mock.Mock(autospec=utils._ClassWithInit)

tests/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,17 @@ def get_none(self) -> None:
6464

6565

6666
class _ClientWithPartialMethod:
67+
def __init__(self) -> None:
68+
self._send_get_request = functools.partial(self._send_request, "GET")
69+
6770
def _send_request(self, method: str, url: str, payload: str | None = None) -> None:
6871
pass
6972

7073
_send_post_request = functools.partialmethod(_send_request, "POST")
74+
75+
76+
def _send_request(method: str, url: str, payload: str | None = None) -> None:
77+
pass
78+
79+
80+
_send_post_request = functools.partial(_send_request, "POST")

0 commit comments

Comments
 (0)