diff options
author | Matthew Suozzo <msuozzo@google.com> | 2021-04-09 23:45:50 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-09 20:45:50 -0700 |
commit | dccdc500f9b5dab0a20407ae0178d393796a8828 (patch) | |
tree | a48fd0cbd5857345cd084e47f93675e56a857323 /Lib/unittest | |
parent | ba1db571987c65672d9c06789e9852313ed2412a (diff) | |
download | cpython-git-dccdc500f9b5dab0a20407ae0178d393796a8828.tar.gz |
bpo-43478: Restrict use of Mock objects as specs (GH-25326)
* Restrict using Mock objects as specs as this is always a test bug where the resulting mock is misleadingly useless.
* Skip a broken test that exposes a bug elsewhere in mock (noted in the original issue).
Diffstat (limited to 'Lib/unittest')
-rw-r--r-- | Lib/unittest/mock.py | 42 | ||||
-rw-r--r-- | Lib/unittest/test/testmock/testasync.py | 4 | ||||
-rw-r--r-- | Lib/unittest/test/testmock/testmock.py | 26 |
3 files changed, 64 insertions, 8 deletions
diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 720f682efb..c6067151de 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -36,6 +36,10 @@ from unittest.util import safe_repr from functools import wraps, partial +class InvalidSpecError(Exception): + """Indicates that an invalid value was used as a mock spec.""" + + _builtins = {name for name in dir(builtins) if not name.startswith('_')} FILTER_DIR = True @@ -653,10 +657,17 @@ class NonCallableMock(Base): self._mock_children[name] = result elif isinstance(result, _SpecState): - result = create_autospec( - result.spec, result.spec_set, result.instance, - result.parent, result.name - ) + try: + result = create_autospec( + result.spec, result.spec_set, result.instance, + result.parent, result.name + ) + except InvalidSpecError: + target_name = self.__dict__['_mock_name'] or self + raise InvalidSpecError( + f'Cannot autospec attr {name!r} from target ' + f'{target_name!r} as it has already been mocked out. ' + f'[target={self!r}, attr={result.spec!r}]') self._mock_children[name] = result return result @@ -1273,6 +1284,14 @@ class _patch(object): ) if not unsafe: _check_spec_arg_typos(kwargs) + if _is_instance_mock(spec): + raise InvalidSpecError( + f'Cannot spec attr {attribute!r} as the spec ' + f'has already been mocked out. [spec={spec!r}]') + if _is_instance_mock(spec_set): + raise InvalidSpecError( + f'Cannot spec attr {attribute!r} as the spec_set ' + f'target has already been mocked out. [spec_set={spec_set!r}]') self.getter = getter self.attribute = attribute @@ -1500,6 +1519,18 @@ class _patch(object): if autospec is True: autospec = original + if _is_instance_mock(self.target): + raise InvalidSpecError( + f'Cannot autospec attr {self.attribute!r} as the patch ' + f'target has already been mocked out. ' + f'[target={self.target!r}, attr={autospec!r}]') + if _is_instance_mock(autospec): + target_name = getattr(self.target, '__name__', self.target) + raise InvalidSpecError( + f'Cannot autospec attr {self.attribute!r} from target ' + f'{target_name!r} as it has already been mocked out. ' + f'[target={self.target!r}, attr={autospec!r}]') + new = create_autospec(autospec, spec_set=spec_set, _name=self.attribute, **kwargs) elif kwargs: @@ -2613,6 +2644,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None, spec = type(spec) is_type = isinstance(spec, type) + if _is_instance_mock(spec): + raise InvalidSpecError(f'Cannot autospec a Mock object. ' + f'[object={spec!r}]') is_async_func = _is_async_func(spec) _kwargs = {'spec': spec} if spec_set: diff --git a/Lib/unittest/test/testmock/testasync.py b/Lib/unittest/test/testmock/testasync.py index 690ca4f55f..e1866a3492 100644 --- a/Lib/unittest/test/testmock/testasync.py +++ b/Lib/unittest/test/testmock/testasync.py @@ -199,9 +199,9 @@ class AsyncAutospecTest(unittest.TestCase): with self.assertRaises(RuntimeError): create_autospec(async_func, instance=True) + @unittest.skip('Broken test from https://bugs.python.org/issue37251') def test_create_autospec_awaitable_class(self): - awaitable_mock = create_autospec(spec=AwaitableClass()) - self.assertIsInstance(create_autospec(awaitable_mock), AsyncMock) + self.assertIsInstance(create_autospec(AwaitableClass), AsyncMock) def test_create_autospec(self): spec = create_autospec(async_func_args) diff --git a/Lib/unittest/test/testmock/testmock.py b/Lib/unittest/test/testmock/testmock.py index e38f41e1d2..fdba543b53 100644 --- a/Lib/unittest/test/testmock/testmock.py +++ b/Lib/unittest/test/testmock/testmock.py @@ -11,7 +11,7 @@ from unittest.mock import ( call, DEFAULT, patch, sentinel, MagicMock, Mock, NonCallableMock, NonCallableMagicMock, AsyncMock, _Call, _CallList, - create_autospec + create_autospec, InvalidSpecError ) @@ -205,6 +205,28 @@ class MockTest(unittest.TestCase): self.assertRaisesRegex(ValueError, 'Bazinga!', mock) + def test_autospec_mock(self): + class A(object): + class B(object): + C = None + + with mock.patch.object(A, 'B'): + with self.assertRaisesRegex(InvalidSpecError, + "Cannot autospec attr 'B' from target <MagicMock spec='A'"): + create_autospec(A).B + with self.assertRaisesRegex(InvalidSpecError, + "Cannot autospec attr 'B' from target 'A'"): + mock.patch.object(A, 'B', autospec=True).start() + with self.assertRaisesRegex(InvalidSpecError, + "Cannot autospec attr 'C' as the patch target "): + mock.patch.object(A.B, 'C', autospec=True).start() + with self.assertRaisesRegex(InvalidSpecError, + "Cannot spec attr 'B' as the spec "): + mock.patch.object(A, 'B', spec=A.B).start() + with self.assertRaisesRegex(InvalidSpecError, + "Cannot spec attr 'B' as the spec_set "): + mock.patch.object(A, 'B', spec_set=A.B).start() + def test_reset_mock(self): parent = Mock() spec = ["something"] @@ -2177,7 +2199,7 @@ class MockTest(unittest.TestCase): self.obj_with_bool_func = unittest.mock.MagicMock() obj = Something() - with unittest.mock.patch.object(obj, 'obj_with_bool_func', autospec=True): pass + with unittest.mock.patch.object(obj, 'obj_with_bool_func', spec=object): pass self.assertEqual(obj.obj_with_bool_func.__bool__.call_count, 0) |