From 59e0de4903c02e72b329e505fddf1ad9794928bc Mon Sep 17 00:00:00 2001 From: Tomas R Date: Thu, 13 Apr 2023 09:37:57 +0200 Subject: [PATCH] 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. --- .../test_unittest/testmock/testhelpers.py | 18 +++++++++++ Lib/test/test_unittest/testmock/testpatch.py | 30 +++++++++++++++++++ Lib/unittest/mock.py | 6 ++++ Misc/ACKS | 1 + ...-04-03-23-44-34.gh-issue-102978.gy9eVk.rst | 3 ++ 5 files changed, 58 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-03-23-44-34.gh-issue-102978.gy9eVk.rst diff --git a/Lib/test/test_unittest/testmock/testhelpers.py b/Lib/test/test_unittest/testmock/testhelpers.py index dc4d004cda8a75..74785a83757a92 100644 --- a/Lib/test/test_unittest/testmock/testhelpers.py +++ b/Lib/test/test_unittest/testmock/testhelpers.py @@ -952,6 +952,24 @@ def __getattr__(self, attribute): 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 myfunc(x, y): pass diff --git a/Lib/test/test_unittest/testmock/testpatch.py b/Lib/test/test_unittest/testmock/testpatch.py index 8ceb5d973e1aaf..833d7da1f31a20 100644 --- a/Lib/test/test_unittest/testmock/testpatch.py +++ b/Lib/test/test_unittest/testmock/testpatch.py @@ -996,6 +996,36 @@ def test_autospec_classmethod(self): 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): patcher = patch('%s.function' % __name__, new=3, autospec=True) self.assertRaises(TypeError, patcher.start) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 0f93cb53c3d5ce..7ca085760650af 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -98,6 +98,12 @@ def _get_signature_object(func, as_instance, eat_self): func = func.__init__ # Skip the `self` argument in __init__ 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): # If we really want to model an instance of the passed type, # __call__ should be looked up, not __init__. diff --git a/Misc/ACKS b/Misc/ACKS index 1e94d33a665e4c..d0ff4e8aeb5c90 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1550,6 +1550,7 @@ Hugo van Rossum Saskia van Rossum Robin Roth Clement Rouault +Tomas Roun Donald Wallace Rouse II Liam Routt Todd Rovito diff --git a/Misc/NEWS.d/next/Library/2023-04-03-23-44-34.gh-issue-102978.gy9eVk.rst b/Misc/NEWS.d/next/Library/2023-04-03-23-44-34.gh-issue-102978.gy9eVk.rst new file mode 100644 index 00000000000000..df63af10a385eb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-03-23-44-34.gh-issue-102978.gy9eVk.rst @@ -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``.