Skip to content

Commit cbac9a6

Browse files
committed
gh-153581: Fix create_autospec producing an uncallable mock for functools.partialmethod
functools.partialmethod objects have no __call__ of their own (they are non-data descriptors, only callable once resolved through attribute access), so create_autospec's _callable() check treated them as non-callable and forced Klass = NonCallableMagicMock. Autospeccing such an attribute directly (e.g.: mock.patch.object(cls, name, autospec=True)) therefore produced a mock that raised TypeError on every call, even though the real attribute is perfectly callable. _callable() now judges callability from the wrapped function (partialmethod.func) instead. _get_signature_object() gained a matching branch that computes the partialmethod's effective signature, self dropped, the partialmethod's own pre-bound positional/keyword arguments applied, so the resulting mock also enforces the correct call signature rather than accepting anything.
1 parent c71bf4d commit cbac9a6

3 files changed

Lines changed: 53 additions & 1 deletion

File tree

‎Lib/test/test_unittest/testmock/testpatch.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# E-mail: fuzzyman AT voidspace DOT org DOT uk
33
# http://www.voidspace.org.uk/python/mock/
44

5+
import functools
56
import os
67
import sys
78
from collections import OrderedDict
@@ -1077,6 +1078,30 @@ def class_method(cls, a, b=10, *, c): pass
10771078
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
10781079

10791080

1081+
def test_autospec_partialmethod(self):
1082+
# autospeccing a functools.partialmethod attribute used to
1083+
# produce a NonCallableMagicMock (the attribute has no __call__ of
1084+
# its own), making the mock entirely uncallable. It should behave
1085+
# like autospeccing any other bound method, with `self` and the
1086+
# partialmethod's own pre-bound arguments excluded from the
1087+
# enforced signature.
1088+
class Foo:
1089+
def method(self, a, b, c=3):
1090+
return a, b, c
1091+
partial_method = functools.partialmethod(method, 1)
1092+
1093+
Foo().partial_method(2)
1094+
1095+
with patch.object(Foo, 'partial_method', autospec=True) as method:
1096+
self.assertNotIsInstance(method, NonCallableMagicMock)
1097+
foo = Foo()
1098+
foo.partial_method(2)
1099+
foo.partial_method(2, c=4)
1100+
self.assertRaises(TypeError, foo.partial_method)
1101+
self.assertRaises(TypeError, foo.partial_method, 2, 3, 4)
1102+
self.assertRaises(TypeError, foo.partial_method, 2, foo="lish")
1103+
1104+
10801105
def test_autospec_with_new(self):
10811106
patcher = patch('%s.function' % __name__, new=3, autospec=True)
10821107
self.assertRaises(TypeError, patcher.start)

‎Lib/unittest/mock.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from dataclasses import fields, is_dataclass
3838
from types import CodeType, LazyImportType, ModuleType, MethodType
3939
from unittest.util import safe_repr
40-
from functools import wraps, partial
40+
from functools import partialmethod, wraps, partial
4141
from threading import RLock
4242

4343

@@ -107,6 +107,20 @@ def _get_signature_object(func, as_instance, eat_self):
107107
eat_self = True
108108
# Use the original decorated method to extract the correct function signature
109109
func = func.__func__
110+
elif isinstance(func, partialmethod):
111+
# partialmethod always consumes `self` itself: real access binds it via the
112+
# descriptor protocol (instance access resolves `self` before applying before
113+
# applying the partialmethod's own bound args). When a mock stands in for the
114+
# raw class attribute directly, there is no descriptor rebinding at all, so no
115+
# caller ever supplies `self` either. Build the effective signature by dropping
116+
# `self` from the wrapped function's signature and applying the partialmethod's
117+
# own pre-bound positional / keyword arguments to what remains.
118+
inner_sig = inspect.signature(func.func, annotation_format=Format.FORWARDREF)
119+
params = list(inner_sig.parameters.values())[1:]
120+
stub = lambda *args, **kwargs: None
121+
stub.__signature__ = inner_sig.replace(parameters=params)
122+
func = partial(stub, *func.args, **func.keywords)
123+
eat_self = False
110124
elif not isinstance(func, FunctionTypes):
111125
# If we really want to model an instance of the passed type,
112126
# __call__ should be looked up, not __init__.
@@ -155,6 +169,13 @@ def _callable(obj):
155169
return True
156170
if isinstance(obj, (staticmethod, classmethod, MethodType)):
157171
return _callable(obj.__func__)
172+
if isinstance(obj, partialmethod):
173+
# partialmethod objects have no __call__ of their own, they are non-data
174+
# descriptors that only become callable once resolved via the descriptor
175+
# protocol (instance /class attribute access). Judge callability from the
176+
# wrapped function instead, so autospeccing a partialmethod attribute doesn't
177+
# produce an uncallable mock.
178+
return _callable(obj.func)
158179
if getattr(obj, '__call__', None) is not None:
159180
return True
160181
return False
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix :func:`unittest.mock.create_autospec` (and ``mock.patch(...,
2+
autospec=True)``) producing a completely uncallable
3+
:class:`~unittest.mock.NonCallableMagicMock` when the autospecced attribute
4+
is a :class:`functools.partialmethod`. The mock is now callable and enforces
5+
the partial method's effective signature (``self`` and the partialmethod's
6+
own pre-bound arguments excluded).

0 commit comments

Comments
 (0)