Skip to content

Commit dfd379a

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 dfd379a

4 files changed

Lines changed: 86 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: 31 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:
@@ -1060,6 +1075,14 @@ def static_method(a, b=10, *, c): pass
10601075
self.assertRaises(TypeError, method, 1)
10611076
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
10621077

1078+
with patch.object(Foo, 'static_method', autospec=True) as method:
1079+
foo = Foo()
1080+
foo.static_method(1, 2, c=3)
1081+
method.assert_called_once_with(1, 2, c=3)
1082+
self.assertRaises(TypeError, foo.static_method)
1083+
self.assertRaises(TypeError, foo.static_method, 1)
1084+
self.assertRaises(TypeError, foo.static_method, 1, 2, 3, c=4)
1085+
10631086

10641087
def test_autospec_classmethod_signature(self):
10651088
# Patched methods which are decorated with @classmethod should have the same signature
@@ -1075,6 +1098,14 @@ def class_method(cls, a, b=10, *, c): pass
10751098
self.assertRaises(TypeError, method, 1)
10761099
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
10771100

1101+
with patch.object(Foo, 'class_method', autospec=True) as method:
1102+
foo = Foo()
1103+
foo.class_method(1, 2, c=3)
1104+
method.assert_called_once_with(1, 2, c=3)
1105+
self.assertRaises(TypeError, foo.class_method)
1106+
self.assertRaises(TypeError, foo.class_method, 1)
1107+
self.assertRaises(TypeError, foo.class_method, 1, 2, 3, c=4)
1108+
10781109

10791110
def test_autospec_with_new(self):
10801111
patcher = patch('%s.function' % __name__, new=3, autospec=True)

Lib/unittest/mock.py

Lines changed: 50 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)
@@ -1508,6 +1521,7 @@ def __enter__(self):
15081521
raise TypeError("Can't provide explicit spec_set *and* spec or autospec")
15091522

15101523
original, local = self.get_original()
1524+
_is_classmethod = _is_staticmethod = False
15111525

15121526
if new is DEFAULT and autospec is None:
15131527
inherit = False
@@ -1593,7 +1607,10 @@ def __enter__(self):
15931607
raise TypeError("Can't use 'autospec' with create=True")
15941608
spec_set = bool(spec_set)
15951609
if autospec is True:
1596-
autospec = original
1610+
if isinstance(self.target, type):
1611+
autospec = getattr(self.target, self.attribute, original)
1612+
else:
1613+
autospec = original
15971614

15981615
if _is_instance_mock(self.target):
15991616
raise InvalidSpecError(
@@ -1607,14 +1624,34 @@ def __enter__(self):
16071624
f'{target_name!r} as it has already been mocked out. '
16081625
f'[target={self.target!r}, attr={autospec!r}]')
16091626

1627+
# For regular methods on classes, self is passed by the descriptor
1628+
# protocol but should not be recorded in mock call args.
1629+
_eat_self = _must_skip(
1630+
self.target, self.attribute, isinstance(self.target, type)
1631+
)
1632+
1633+
_is_classmethod = isinstance(original, classmethod)
1634+
_is_staticmethod = isinstance(original, staticmethod)
1635+
if _is_classmethod:
1636+
autospec = original.__func__
1637+
_eat_self = True
1638+
16101639
new = create_autospec(autospec, spec_set=spec_set,
1611-
_name=self.attribute, **kwargs)
1640+
_name=self.attribute, _eat_self=_eat_self,
1641+
**kwargs)
16121642
elif kwargs:
16131643
# can't set keyword args when we aren't creating the mock
16141644
# XXXX If new is a Mock we could call new.configure_mock(**kwargs)
16151645
raise TypeError("Can't pass kwargs to a mock we aren't creating")
16161646

16171647
new_attr = new
1648+
if isinstance(new_attr, FunctionTypes):
1649+
if _is_classmethod:
1650+
_check_signature(original.__func__, new.mock, skipfirst=True)
1651+
new_attr = classmethod(new)
1652+
new = new.mock
1653+
elif _is_staticmethod:
1654+
new_attr = staticmethod(new)
16181655

16191656
self.temp_original = original
16201657
self.is_local = local
@@ -2746,7 +2783,7 @@ def call_list(self):
27462783

27472784

27482785
def create_autospec(spec, spec_set=False, instance=False, _parent=None,
2749-
_name=None, *, unsafe=False, **kwargs):
2786+
_name=None, *, unsafe=False, _eat_self=False, **kwargs):
27502787
"""Create a mock object using another object as a spec. Attributes on the
27512788
mock will use the corresponding attribute on the `spec` object as their
27522789
spec.
@@ -2823,17 +2860,17 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
28232860
Klass = NonCallableMagicMock
28242861

28252862
mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name,
2826-
name=_name, **_kwargs)
2863+
name=_name, _eat_self=_eat_self or None, **_kwargs)
28272864
if is_dataclass_spec:
28282865
mock._mock_extend_spec_methods(dataclass_spec_list)
28292866

28302867
if isinstance(spec, FunctionTypes):
28312868
# should only happen at the top level because we don't
28322869
# recurse for functions
28332870
if is_async_func:
2834-
mock = _set_async_signature(mock, spec)
2871+
mock = _set_async_signature(mock, spec, skipfirst=_eat_self)
28352872
else:
2836-
mock = _set_signature(mock, spec)
2873+
mock = _set_signature(mock, spec, skipfirst=_eat_self)
28372874
else:
28382875
_check_signature(spec, mock, is_type, instance)
28392876

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)