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.
This commit is contained in:
Tomas R 2023-04-13 09:37:57 +02:00 committed by GitHub
parent 19d2639d1e
commit 59e0de4903
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 58 additions and 0 deletions

View file

@ -952,6 +952,24 @@ class SpecSignatureTest(unittest.TestCase):
self.assertFalse(hasattr(autospec, '__name__')) self.assertFalse(hasattr(autospec, '__name__'))
def test_autospec_signature_staticmethod(self):
class Foo:
@staticmethod
def static_method(a, b=10, *, c): pass
mock = create_autospec(Foo.__dict__['static_method'])
self.assertEqual(inspect.signature(Foo.static_method), inspect.signature(mock))
def test_autospec_signature_classmethod(self):
class Foo:
@classmethod
def class_method(cls, a, b=10, *, c): pass
mock = create_autospec(Foo.__dict__['class_method'])
self.assertEqual(inspect.signature(Foo.class_method), inspect.signature(mock))
def test_spec_inspect_signature(self): def test_spec_inspect_signature(self):
def myfunc(x, y): pass def myfunc(x, y): pass

View file

@ -996,6 +996,36 @@ class PatchTest(unittest.TestCase):
method.assert_called_once_with() method.assert_called_once_with()
def test_autospec_staticmethod_signature(self):
# Patched methods which are decorated with @staticmethod should have the same signature
class Foo:
@staticmethod
def static_method(a, b=10, *, c): pass
Foo.static_method(1, 2, c=3)
with patch.object(Foo, 'static_method', autospec=True) as method:
method(1, 2, c=3)
self.assertRaises(TypeError, method)
self.assertRaises(TypeError, method, 1)
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
def test_autospec_classmethod_signature(self):
# Patched methods which are decorated with @classmethod should have the same signature
class Foo:
@classmethod
def class_method(cls, a, b=10, *, c): pass
Foo.class_method(1, 2, c=3)
with patch.object(Foo, 'class_method', autospec=True) as method:
method(1, 2, c=3)
self.assertRaises(TypeError, method)
self.assertRaises(TypeError, method, 1)
self.assertRaises(TypeError, method, 1, 2, 3, c=4)
def test_autospec_with_new(self): def test_autospec_with_new(self):
patcher = patch('%s.function' % __name__, new=3, autospec=True) patcher = patch('%s.function' % __name__, new=3, autospec=True)
self.assertRaises(TypeError, patcher.start) self.assertRaises(TypeError, patcher.start)

View file

@ -98,6 +98,12 @@ def _get_signature_object(func, as_instance, eat_self):
func = func.__init__ func = func.__init__
# Skip the `self` argument in __init__ # Skip the `self` argument in __init__
eat_self = True eat_self = True
elif isinstance(func, (classmethod, staticmethod)):
if isinstance(func, classmethod):
# Skip the `cls` argument of a class method
eat_self = True
# Use the original decorated method to extract the correct function signature
func = func.__func__
elif not isinstance(func, FunctionTypes): elif not isinstance(func, FunctionTypes):
# If we really want to model an instance of the passed type, # If we really want to model an instance of the passed type,
# __call__ should be looked up, not __init__. # __call__ should be looked up, not __init__.

View file

@ -1550,6 +1550,7 @@ Hugo van Rossum
Saskia van Rossum Saskia van Rossum
Robin Roth Robin Roth
Clement Rouault Clement Rouault
Tomas Roun
Donald Wallace Rouse II Donald Wallace Rouse II
Liam Routt Liam Routt
Todd Rovito Todd Rovito

View file

@ -0,0 +1,3 @@
Fixes :func:`unittest.mock.patch` not enforcing function signatures for methods
decorated with ``@classmethod`` or ``@staticmethod`` when patch is called with
``autospec=True``.