Skip to content

Commit bb34608

Browse files
gh-76273: Fix autospec mocking of instance and class methods
Currently, when patching instance / class methods with autospec, their self / cls arguments are not consumed, causing call asserts to fail (they expect an instance / class reference as the first argument). Example: from unittest import mock class Something(object): def foo(self, a, b, c, d): pass with mock.patch.object(Something, 'foo', autospec=True): s = Something() s.foo() Fix this by skipping the first argument when presented with a method. Based on #4476 Signed-off-by: Stephen Finucane <stephen@that.guru> Co-authored-by: Claudiu Belu <cbelu@cloudbasesolutions.com>
1 parent 2e64e36 commit bb34608

4 files changed

Lines changed: 55 additions & 16 deletions

File tree

Lib/test/test_unittest/testmock/testmock.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,9 +2196,9 @@ def test_attach_mock_patch_autospec_signature(self):
21962196
manager.attach_mock(mocked, 'attach_meth')
21972197
obj = Something()
21982198
obj.meth(1, 2, 3, d=4)
2199-
manager.assert_has_calls([call.attach_meth(mock.ANY, 1, 2, 3, d=4)])
2200-
obj.meth.assert_has_calls([call(mock.ANY, 1, 2, 3, d=4)])
2201-
mocked.assert_has_calls([call(mock.ANY, 1, 2, 3, d=4)])
2199+
manager.assert_has_calls([call.attach_meth(1, 2, 3, d=4)])
2200+
obj.meth.assert_has_calls([call(1, 2, 3, d=4)])
2201+
mocked.assert_has_calls([call(1, 2, 3, d=4)])
22022202

22032203
with mock.patch(f'{__name__}.something', autospec=True) as mocked:
22042204
manager = Mock()

Lib/test/test_unittest/testmock/testpatch.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,21 @@ def test_autospec_classmethod(self):
10461046
method.assert_called_once_with()
10471047

10481048

1049+
def test_autospec_method_signature(self):
1050+
# Patched methods should have the same signature
1051+
# https://github.com/python/cpython/issues/76273
1052+
class Foo:
1053+
def method(self, a, b=10, *, c): pass
1054+
1055+
with patch.object(Foo, 'method', autospec=True) as mock_method:
1056+
foo = Foo()
1057+
foo.method(1, 2, c=3)
1058+
mock_method.assert_called_once_with(1, 2, c=3)
1059+
self.assertRaises(TypeError, foo.method)
1060+
self.assertRaises(TypeError, foo.method, 1)
1061+
self.assertRaises(TypeError, foo.method, 1, 2, 3, c=4)
1062+
1063+
10491064
def test_autospec_staticmethod_signature(self):
10501065
# Patched methods which are decorated with @staticmethod should have the same signature
10511066
class Foo:

Lib/unittest/mock.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,12 @@ def _instance_callable(obj):
182182
return False
183183

184184

185-
def _set_signature(mock, original, instance=False):
185+
def _set_signature(mock, original, instance=False, skipfirst=False):
186186
# creates a function with signature (*args, **kwargs) that delegates to a
187187
# mock. It still does signature checking by calling a lambda with the same
188188
# signature as the original.
189189

190-
skipfirst = isinstance(original, type)
190+
skipfirst = skipfirst or isinstance(original, type)
191191
result = _get_signature_object(original, instance, skipfirst)
192192
if result is None:
193193
return mock
@@ -200,28 +200,41 @@ def checksig(*args, **kwargs):
200200
if not name.isidentifier():
201201
name = 'funcopy'
202202
context = {'_checksig_': checksig, 'mock': mock}
203-
src = """def %s(*args, **kwargs):
203+
if skipfirst:
204+
src = """def %s(_mock_self, /, *args, **kwargs):
205+
_checksig_(*args, **kwargs)
206+
return mock(*args, **kwargs)""" % name
207+
else:
208+
src = """def %s(*args, **kwargs):
204209
_checksig_(*args, **kwargs)
205210
return mock(*args, **kwargs)""" % name
206211
exec (src, context)
207212
funcopy = context[name]
208213
_setup_func(funcopy, mock, sig)
209214
return funcopy
210215

211-
def _set_async_signature(mock, original, instance=False, is_async_mock=False):
216+
def _set_async_signature(mock, original, instance=False, is_async_mock=False, skipfirst=False):
212217
# creates an async function with signature (*args, **kwargs) that delegates to a
213218
# mock. It still does signature checking by calling a lambda with the same
214219
# signature as the original.
215220

216-
skipfirst = isinstance(original, type)
217-
func, sig = _get_signature_object(original, instance, skipfirst)
221+
skipfirst = skipfirst or isinstance(original, type)
222+
result = _get_signature_object(original, instance, skipfirst)
223+
if result is None:
224+
return mock
225+
func, sig = result
218226
def checksig(*args, **kwargs):
219227
sig.bind(*args, **kwargs)
220228
_copy_func_details(func, checksig)
221229

222230
name = original.__name__
223231
context = {'_checksig_': checksig, 'mock': mock}
224-
src = """async def %s(*args, **kwargs):
232+
if skipfirst:
233+
src = """async def %s(_mock_self, /, *args, **kwargs):
234+
_checksig_(*args, **kwargs)
235+
return await mock(*args, **kwargs)""" % name
236+
else:
237+
src = """async def %s(*args, **kwargs):
225238
_checksig_(*args, **kwargs)
226239
return await mock(*args, **kwargs)""" % name
227240
exec (src, context)
@@ -1593,7 +1606,10 @@ def __enter__(self):
15931606
raise TypeError("Can't use 'autospec' with create=True")
15941607
spec_set = bool(spec_set)
15951608
if autospec is True:
1596-
autospec = original
1609+
if isinstance(self.target, type):
1610+
autospec = getattr(self.target, self.attribute, original)
1611+
else:
1612+
autospec = original
15971613

15981614
if _is_instance_mock(self.target):
15991615
raise InvalidSpecError(
@@ -1607,8 +1623,14 @@ def __enter__(self):
16071623
f'{target_name!r} as it has already been mocked out. '
16081624
f'[target={self.target!r}, attr={autospec!r}]')
16091625

1626+
# For regular methods on classes, self is passed by the descriptor
1627+
# protocol but should not be recorded in mock call args.
1628+
_eat_self = _must_skip(
1629+
self.target, self.attribute, isinstance(self.target, type)
1630+
)
16101631
new = create_autospec(autospec, spec_set=spec_set,
1611-
_name=self.attribute, **kwargs)
1632+
_name=self.attribute, _eat_self=_eat_self,
1633+
**kwargs)
16121634
elif kwargs:
16131635
# can't set keyword args when we aren't creating the mock
16141636
# XXXX If new is a Mock we could call new.configure_mock(**kwargs)
@@ -2746,7 +2768,7 @@ def call_list(self):
27462768

27472769

27482770
def create_autospec(spec, spec_set=False, instance=False, _parent=None,
2749-
_name=None, *, unsafe=False, **kwargs):
2771+
_name=None, *, unsafe=False, _eat_self=False, **kwargs):
27502772
"""Create a mock object using another object as a spec. Attributes on the
27512773
mock will use the corresponding attribute on the `spec` object as their
27522774
spec.
@@ -2823,17 +2845,17 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28232845
Klass = NonCallableMagicMock
28242846

28252847
mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name,
2826-
name=_name, **_kwargs)
2848+
name=_name, _eat_self=_eat_self or None, **_kwargs)
28272849
if is_dataclass_spec:
28282850
mock._mock_extend_spec_methods(dataclass_spec_list)
28292851

28302852
if isinstance(spec, FunctionTypes):
28312853
# should only happen at the top level because we don't
28322854
# recurse for functions
28332855
if is_async_func:
2834-
mock = _set_async_signature(mock, spec)
2856+
mock = _set_async_signature(mock, spec, skipfirst=_eat_self)
28352857
else:
2836-
mock = _set_signature(mock, spec)
2858+
mock = _set_signature(mock, spec, skipfirst=_eat_self)
28372859
else:
28382860
_check_signature(spec, mock, is_type, instance)
28392861

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Calling ``unittest.mock.patch`` with ``autospec`` on an instance or class method
2+
will now correctly consume the ``self`` / ``cls`` argument.

0 commit comments

Comments
 (0)