Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Lib/test/test_unittest/testmock/testmock.py
Original file line number Diff line number Diff line change
Expand Up @@ -2196,9 +2196,9 @@ def test_attach_mock_patch_autospec_signature(self):
manager.attach_mock(mocked, 'attach_meth')
obj = Something()
obj.meth(1, 2, 3, d=4)
manager.assert_has_calls([call.attach_meth(mock.ANY, 1, 2, 3, d=4)])
obj.meth.assert_has_calls([call(mock.ANY, 1, 2, 3, d=4)])
mocked.assert_has_calls([call(mock.ANY, 1, 2, 3, d=4)])
manager.assert_has_calls([call.attach_meth(1, 2, 3, d=4)])
obj.meth.assert_has_calls([call(1, 2, 3, d=4)])
mocked.assert_has_calls([call(1, 2, 3, d=4)])

with mock.patch(f'{__name__}.something', autospec=True) as mocked:
manager = Mock()
Expand Down
31 changes: 31 additions & 0 deletions Lib/test/test_unittest/testmock/testpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,21 @@ def test_autospec_classmethod(self):
method.assert_called_once_with()


def test_autospec_method_signature(self):
# Patched methods should have the same signature
# https://github.com/python/cpython/issues/76273
class Foo:
def method(self, a, b=10, *, c): pass

with patch.object(Foo, 'method', autospec=True) as mock_method:
foo = Foo()
foo.method(1, 2, c=3)
mock_method.assert_called_once_with(1, 2, c=3)
self.assertRaises(TypeError, foo.method)
self.assertRaises(TypeError, foo.method, 1)
self.assertRaises(TypeError, foo.method, 1, 2, 3, c=4)


def test_autospec_staticmethod_signature(self):
# Patched methods which are decorated with @staticmethod should have the same signature
class Foo:
Expand All @@ -1060,6 +1075,14 @@ def static_method(a, b=10, *, c): pass
self.assertRaises(TypeError, method, 1)
self.assertRaises(TypeError, method, 1, 2, 3, c=4)

with patch.object(Foo, 'static_method', autospec=True) as method:
foo = Foo()
foo.static_method(1, 2, c=3)
method.assert_called_once_with(1, 2, c=3)
self.assertRaises(TypeError, foo.static_method)
self.assertRaises(TypeError, foo.static_method, 1)
self.assertRaises(TypeError, foo.static_method, 1, 2, 3, c=4)


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

with patch.object(Foo, 'class_method', autospec=True) as method:
foo = Foo()
foo.class_method(1, 2, c=3)
method.assert_called_once_with(1, 2, c=3)
self.assertRaises(TypeError, foo.class_method)
self.assertRaises(TypeError, foo.class_method, 1)
self.assertRaises(TypeError, foo.class_method, 1, 2, 3, c=4)


def test_autospec_with_new(self):
patcher = patch('%s.function' % __name__, new=3, autospec=True)
Expand Down
63 changes: 50 additions & 13 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,12 @@ def _instance_callable(obj):
return False


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

skipfirst = isinstance(original, type)
skipfirst = skipfirst or isinstance(original, type)
result = _get_signature_object(original, instance, skipfirst)
if result is None:
return mock
Expand All @@ -200,28 +200,41 @@ def checksig(*args, **kwargs):
if not name.isidentifier():
name = 'funcopy'
context = {'_checksig_': checksig, 'mock': mock}
src = """def %s(*args, **kwargs):
if skipfirst:
src = """def %s(_mock_self, /, *args, **kwargs):
_checksig_(*args, **kwargs)
return mock(*args, **kwargs)""" % name
else:
src = """def %s(*args, **kwargs):
_checksig_(*args, **kwargs)
return mock(*args, **kwargs)""" % name
exec (src, context)
funcopy = context[name]
_setup_func(funcopy, mock, sig)
return funcopy

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

skipfirst = isinstance(original, type)
func, sig = _get_signature_object(original, instance, skipfirst)
skipfirst = skipfirst or isinstance(original, type)
result = _get_signature_object(original, instance, skipfirst)
if result is None:
return mock
func, sig = result
def checksig(*args, **kwargs):
sig.bind(*args, **kwargs)
_copy_func_details(func, checksig)

name = original.__name__
context = {'_checksig_': checksig, 'mock': mock}
src = """async def %s(*args, **kwargs):
if skipfirst:
src = """async def %s(_mock_self, /, *args, **kwargs):
_checksig_(*args, **kwargs)
return await mock(*args, **kwargs)""" % name
else:
src = """async def %s(*args, **kwargs):
_checksig_(*args, **kwargs)
return await mock(*args, **kwargs)""" % name
exec (src, context)
Expand Down Expand Up @@ -1508,6 +1521,7 @@ def __enter__(self):
raise TypeError("Can't provide explicit spec_set *and* spec or autospec")

original, local = self.get_original()
_is_classmethod = _is_staticmethod = False

if new is DEFAULT and autospec is None:
inherit = False
Expand Down Expand Up @@ -1593,7 +1607,10 @@ def __enter__(self):
raise TypeError("Can't use 'autospec' with create=True")
spec_set = bool(spec_set)
if autospec is True:
autospec = original
if isinstance(self.target, type):
autospec = getattr(self.target, self.attribute, original)
else:
autospec = original

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

# For regular methods on classes, self is passed by the descriptor
# protocol but should not be recorded in mock call args.
_eat_self = _must_skip(
self.target, self.attribute, isinstance(self.target, type)
)

_is_classmethod = isinstance(original, classmethod)
_is_staticmethod = isinstance(original, staticmethod)
if _is_classmethod:
autospec = original.__func__
_eat_self = True

new = create_autospec(autospec, spec_set=spec_set,
_name=self.attribute, **kwargs)
_name=self.attribute, _eat_self=_eat_self,
**kwargs)
elif kwargs:
# can't set keyword args when we aren't creating the mock
# XXXX If new is a Mock we could call new.configure_mock(**kwargs)
raise TypeError("Can't pass kwargs to a mock we aren't creating")

new_attr = new
if isinstance(new_attr, FunctionTypes):
if _is_classmethod:
_check_signature(original.__func__, new.mock, skipfirst=True)
new_attr = classmethod(new)
new = new.mock
elif _is_staticmethod:
new_attr = staticmethod(new)

self.temp_original = original
self.is_local = local
Expand Down Expand Up @@ -2746,7 +2783,7 @@ def call_list(self):


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

mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name,
name=_name, **_kwargs)
name=_name, _eat_self=_eat_self or None, **_kwargs)
if is_dataclass_spec:
mock._mock_extend_spec_methods(dataclass_spec_list)

if isinstance(spec, FunctionTypes):
# should only happen at the top level because we don't
# recurse for functions
if is_async_func:
mock = _set_async_signature(mock, spec)
mock = _set_async_signature(mock, spec, skipfirst=_eat_self)
else:
mock = _set_signature(mock, spec)
mock = _set_signature(mock, spec, skipfirst=_eat_self)
else:
_check_signature(spec, mock, is_type, instance)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Calling ``unittest.mock.patch`` with ``autospec`` on an instance or class method
will now correctly consume the ``self`` / ``cls`` argument.
Loading