diff --git a/Lib/test/test_unittest/testmock/testhelpers.py b/Lib/test/test_unittest/testmock/testhelpers.py index 0e82c723ec3eaa..f97161a9fee945 100644 --- a/Lib/test/test_unittest/testmock/testhelpers.py +++ b/Lib/test/test_unittest/testmock/testhelpers.py @@ -1,3 +1,4 @@ +import functools import inspect import time import types @@ -954,6 +955,45 @@ def __getattr__(self, attribute): self.assertNotHasAttr(autospec, '__name__') + def test_autospec_partial_function_signature_enforced(self): + # A mock created from a functools.partial object enforces the + # partial's effective, post-partial-application signature: "method" + # is already bound to "POST", so only "url" and "payload" remain. + def send_request(method, url, payload=None): + return method, url, payload + + send_post_request = partial(send_request, "POST") + send_post_request("https://example.com") + send_post_request("https://example.com", payload="data") + + mocked = create_autospec(send_post_request) + mocked("https://example.com") + mocked("https://example.com", payload="data") + self.assertRaises(TypeError, mocked) + self.assertRaises(TypeError, mocked, "a", "b", "c") + self.assertRaises(TypeError, mocked, "https://example.com", foo="lish") + + + def test_autospec_partialmethod_self_skipped(self): + # _must_skip() didn't recognize functools.partialmethod class + # attributes, so `self` was never dropped from the enforced + # signature when autospeccing a whole class. + class Foo: + def method(self, a, b, c=3): + return a, b, c + partial_method = functools.partialmethod(method, 1) + + real = Foo() + real.partial_method(2, 3) + + mocked = create_autospec(Foo) + instance = mocked() + instance.partial_method(2) + instance.partial_method(2, c=4) + self.assertRaises(TypeError, instance.partial_method) + self.assertRaises(TypeError, instance.partial_method, 2, 3, 4) + + def test_autospec_signature_staticmethod(self): class Foo: @staticmethod diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 49011faaa51eae..1a41189e060225 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -37,7 +37,7 @@ from dataclasses import fields, is_dataclass from types import CodeType, ModuleType, MethodType from unittest.util import safe_repr -from functools import wraps, partial +from functools import partialmethod, wraps, partial from threading import RLock @@ -107,6 +107,14 @@ def _get_signature_object(func, as_instance, eat_self): eat_self = True # Use the original decorated method to extract the correct function signature func = func.__func__ + elif isinstance(func, partial): + # inspect.signature() already knows how to compute the effective, + # post-partial-application signature of a functools.partial object + # directly. Don't fall through to the generic func.__call__ lookup + # below: partial.__call__ is a C slot wrapper whose own signature is + # just the unenforceable "(*args, **kwargs)" of the functools.partial + # constructor, not of the wrapped callable. + pass elif not isinstance(func, FunctionTypes): # If we really want to model an instance of the passed type, # __call__ should be looked up, not __init__. @@ -2917,9 +2925,12 @@ def _must_skip(spec, entry, is_type): continue if isinstance(result, (staticmethod, classmethod)): return False - elif isinstance(result, FunctionTypes): + elif isinstance(result, (FunctionTypes, partialmethod)): # Normal method => skip if looked up on type # (if looked up on instance, self is already skipped) + # functools.partialmethod resolves to a plain function carrying + # `self` when looked up via getattr() on the type, see + # functools.partialmethod.__get__. return is_type else: return False diff --git a/Misc/NEWS.d/next/Library/2026-07-10-11-12-13.gh-issue-79644.mWr00f.rst b/Misc/NEWS.d/next/Library/2026-07-10-11-12-13.gh-issue-79644.mWr00f.rst new file mode 100644 index 00000000000000..26122f0e2d9ef7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-10-11-12-13.gh-issue-79644.mWr00f.rst @@ -0,0 +1,9 @@ +Fix :func:`unittest.mock.create_autospec` (and ``mock.patch(..., +autospec=True)``) computing the wrong signature for :class:`functools.partial` +and :class:`functools.partialmethod` targets. A ``functools.partial`` mock +previously accepted calls with any number of arguments, since its +effective, post-partial-application signature was never consulted. A +``functools.partialmethod`` class attribute, when autospecced as part of a +whole class, previously had its ``self`` parameter checked against the +partialmethod's own pre-bound argument instead of being skipped, so calls +were validated against the wrong signature shape.