diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index bd85fdcfc472a61..27a0584b892a84d 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -2,6 +2,7 @@ # E-mail: fuzzyman AT voidspace DOT org DOT uk # http://www.voidspace.org.uk/python/mock/ +import functools import os import sys from collections import OrderedDict @@ -1076,6 +1077,30 @@ def class_method(cls, a, b=10, *, c): pass self.assertRaises(TypeError, method, 1, 2, 3, c=4) + def test_autospec_partialmethod(self): + # autospeccing a functools.partialmethod attribute used to + # produce a NonCallableMagicMock (the attribute has no __call__ of + # its own), making the mock entirely uncallable. It should behave + # like autospeccing any other bound method, with `self` and the + # partialmethod's own pre-bound arguments excluded from the + # enforced signature. + class Foo: + def method(self, a, b, c=3): + return a, b, c + partial_method = functools.partialmethod(method, 1) + + Foo().partial_method(2) + + with patch.object(Foo, 'partial_method', autospec=True) as method: + self.assertNotIsInstance(method, NonCallableMagicMock) + foo = Foo() + foo.partial_method(2) + foo.partial_method(2, c=4) + self.assertRaises(TypeError, foo.partial_method) + self.assertRaises(TypeError, foo.partial_method, 2, 3, 4) + self.assertRaises(TypeError, foo.partial_method, 2, foo="lish") + + def test_autospec_with_new(self): patcher = patch('%s.function' % __name__, new=3, autospec=True) self.assertRaises(TypeError, patcher.start) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 49011faaa51eaed..dd5ce20f6fa8da0 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,20 @@ 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, partialmethod): + # partialmethod always consumes `self` itself: real access binds it via the + # descriptor protocol (instance access resolves `self` before applying before + # applying the partialmethod's own bound args). When a mock stands in for the + # raw class attribute directly, there is no descriptor rebinding at all, so no + # caller ever supplies `self` either. Build the effective signature by dropping + # `self` from the wrapped function's signature and applying the partialmethod's + # own pre-bound positional / keyword arguments to what remains. + inner_sig = inspect.signature(func.func, annotation_format=Format.FORWARDREF) + params = list(inner_sig.parameters.values())[1:] + stub = lambda *args, **kwargs: None + stub.__signature__ = inner_sig.replace(parameters=params) + func = partial(stub, *func.args, **func.keywords) + eat_self = False elif not isinstance(func, FunctionTypes): # If we really want to model an instance of the passed type, # __call__ should be looked up, not __init__. @@ -155,6 +169,13 @@ def _callable(obj): return True if isinstance(obj, (staticmethod, classmethod, MethodType)): return _callable(obj.__func__) + if isinstance(obj, partialmethod): + # partialmethod objects have no __call__ of their own, they are non-data + # descriptors that only become callable once resolved via the descriptor + # protocol (instance /class attribute access). Judge callability from the + # wrapped function instead, so autospeccing a partialmethod attribute doesn't + # produce an uncallable mock. + return _callable(obj.func) if getattr(obj, '__call__', None) is not None: return True return False diff --git a/Misc/NEWS.d/next/Library/2026-07-10-11-12-13.gh-issue-153581.a0l3uu.rst b/Misc/NEWS.d/next/Library/2026-07-10-11-12-13.gh-issue-153581.a0l3uu.rst new file mode 100644 index 000000000000000..e044b67518de850 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-10-11-12-13.gh-issue-153581.a0l3uu.rst @@ -0,0 +1,6 @@ +Fix :func:`unittest.mock.create_autospec` (and ``mock.patch(..., +autospec=True)``) producing a completely uncallable +:class:`~unittest.mock.NonCallableMagicMock` when the autospecced attribute +is a :class:`functools.partialmethod`. The mock is now callable and enforces +the partial method's effective signature (``self`` and the partialmethod's +own pre-bound arguments excluded).