Skip to content

Commit 079655c

Browse files
tomasr8cjw296
authored andcommitted
gh-102978: Fix mock.patch function signatures for class and staticmethod decorators (#103228)
Fixes unittest.mock.patch not enforcing function signatures for methods decorated with @classmethod or @staticmethod when patch is called with autospec=True. Backports: 59e0de4903c02e72b329e505fddf1ad9794928bc Signed-off-by: Chris Withers <chris@simplistix.co.uk>
1 parent 1c7bfef commit 079655c

File tree

4 files changed

+57
-0
lines changed

4 files changed

+57
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixes :func:`unittest.mock.patch` not enforcing function signatures for methods
2+
decorated with ``@classmethod`` or ``@staticmethod`` when patch is called with
3+
``autospec=True``.

mock/mock.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ def _get_signature_object(func, as_instance, eat_self):
101101
func = func.__init__
102102
# Skip the `self` argument in __init__
103103
eat_self = True
104+
elif isinstance(func, (classmethod, staticmethod)):
105+
if isinstance(func, classmethod):
106+
# Skip the `cls` argument of a class method
107+
eat_self = True
108+
# Use the original decorated method to extract the correct function signature
109+
func = func.__func__
104110
elif not isinstance(func, FunctionTypes):
105111
# If we really want to model an instance of the passed type,
106112
# __call__ should be looked up, not __init__.

mock/tests/testhelpers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,24 @@ def __getattr__(self, attribute):
961961
self.assertFalse(hasattr(autospec, '__name__'))
962962

963963

964+
def test_autospec_signature_staticmethod(self):
965+
class Foo:
966+
@staticmethod
967+
def static_method(a, b=10, *, c): pass
968+
969+
mock = create_autospec(Foo.__dict__['static_method'])
970+
self.assertEqual(inspect.signature(Foo.static_method), inspect.signature(mock))
971+
972+
973+
def test_autospec_signature_classmethod(self):
974+
class Foo:
975+
@classmethod
976+
def class_method(cls, a, b=10, *, c): pass
977+
978+
mock = create_autospec(Foo.__dict__['class_method'])
979+
self.assertEqual(inspect.signature(Foo.class_method), inspect.signature(mock))
980+
981+
964982
def test_spec_inspect_signature(self):
965983

966984
def myfunc(x, y): pass

mock/tests/testpatch.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,36 @@ def test_autospec_classmethod(self):
996996
method.assert_called_once_with()
997997

998998

999+
def test_autospec_staticmethod_signature(self):
1000+
# Patched methods which are decorated with @staticmethod should have the same signature
1001+
class Foo:
1002+
@staticmethod
1003+
def static_method(a, b=10, *, c): pass
1004+
1005+
Foo.static_method(1, 2, c=3)
1006+
1007+
with patch.object(Foo, 'static_method', autospec=True) as method:
1008+
method(1, 2, c=3)
1009+
self.assertRaises(TypeError, method)
1010+
self.assertRaises(TypeError, method, 1)
1011+
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
1012+
1013+
1014+
def test_autospec_classmethod_signature(self):
1015+
# Patched methods which are decorated with @classmethod should have the same signature
1016+
class Foo:
1017+
@classmethod
1018+
def class_method(cls, a, b=10, *, c): pass
1019+
1020+
Foo.class_method(1, 2, c=3)
1021+
1022+
with patch.object(Foo, 'class_method', autospec=True) as method:
1023+
method(1, 2, c=3)
1024+
self.assertRaises(TypeError, method)
1025+
self.assertRaises(TypeError, method, 1)
1026+
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
1027+
1028+
9991029
def test_autospec_with_new(self):
10001030
patcher = patch('%s.function' % __name__, new=3, autospec=True)
10011031
self.assertRaises(TypeError, patcher.start)

0 commit comments

Comments
 (0)