mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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).
This commit is contained in:
parent
ba1db57198
commit
dccdc500f9
4 changed files with 65 additions and 8 deletions
|
@ -36,6 +36,10 @@ from unittest.util import safe_repr
|
||||||
from functools import wraps, partial
|
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('_')}
|
_builtins = {name for name in dir(builtins) if not name.startswith('_')}
|
||||||
|
|
||||||
FILTER_DIR = True
|
FILTER_DIR = True
|
||||||
|
@ -653,10 +657,17 @@ class NonCallableMock(Base):
|
||||||
self._mock_children[name] = result
|
self._mock_children[name] = result
|
||||||
|
|
||||||
elif isinstance(result, _SpecState):
|
elif isinstance(result, _SpecState):
|
||||||
result = create_autospec(
|
try:
|
||||||
result.spec, result.spec_set, result.instance,
|
result = create_autospec(
|
||||||
result.parent, result.name
|
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
|
self._mock_children[name] = result
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -1273,6 +1284,14 @@ class _patch(object):
|
||||||
)
|
)
|
||||||
if not unsafe:
|
if not unsafe:
|
||||||
_check_spec_arg_typos(kwargs)
|
_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.getter = getter
|
||||||
self.attribute = attribute
|
self.attribute = attribute
|
||||||
|
@ -1500,6 +1519,18 @@ class _patch(object):
|
||||||
if autospec is True:
|
if autospec is True:
|
||||||
autospec = original
|
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,
|
new = create_autospec(autospec, spec_set=spec_set,
|
||||||
_name=self.attribute, **kwargs)
|
_name=self.attribute, **kwargs)
|
||||||
elif kwargs:
|
elif kwargs:
|
||||||
|
@ -2613,6 +2644,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
|
||||||
spec = type(spec)
|
spec = type(spec)
|
||||||
|
|
||||||
is_type = isinstance(spec, type)
|
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)
|
is_async_func = _is_async_func(spec)
|
||||||
_kwargs = {'spec': spec}
|
_kwargs = {'spec': spec}
|
||||||
if spec_set:
|
if spec_set:
|
||||||
|
|
|
@ -199,9 +199,9 @@ class AsyncAutospecTest(unittest.TestCase):
|
||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(RuntimeError):
|
||||||
create_autospec(async_func, instance=True)
|
create_autospec(async_func, instance=True)
|
||||||
|
|
||||||
|
@unittest.skip('Broken test from https://bugs.python.org/issue37251')
|
||||||
def test_create_autospec_awaitable_class(self):
|
def test_create_autospec_awaitable_class(self):
|
||||||
awaitable_mock = create_autospec(spec=AwaitableClass())
|
self.assertIsInstance(create_autospec(AwaitableClass), AsyncMock)
|
||||||
self.assertIsInstance(create_autospec(awaitable_mock), AsyncMock)
|
|
||||||
|
|
||||||
def test_create_autospec(self):
|
def test_create_autospec(self):
|
||||||
spec = create_autospec(async_func_args)
|
spec = create_autospec(async_func_args)
|
||||||
|
|
|
@ -11,7 +11,7 @@ from unittest.mock import (
|
||||||
call, DEFAULT, patch, sentinel,
|
call, DEFAULT, patch, sentinel,
|
||||||
MagicMock, Mock, NonCallableMock,
|
MagicMock, Mock, NonCallableMock,
|
||||||
NonCallableMagicMock, AsyncMock, _Call, _CallList,
|
NonCallableMagicMock, AsyncMock, _Call, _CallList,
|
||||||
create_autospec
|
create_autospec, InvalidSpecError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -205,6 +205,28 @@ class MockTest(unittest.TestCase):
|
||||||
self.assertRaisesRegex(ValueError, 'Bazinga!', mock)
|
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):
|
def test_reset_mock(self):
|
||||||
parent = Mock()
|
parent = Mock()
|
||||||
spec = ["something"]
|
spec = ["something"]
|
||||||
|
@ -2177,7 +2199,7 @@ class MockTest(unittest.TestCase):
|
||||||
self.obj_with_bool_func = unittest.mock.MagicMock()
|
self.obj_with_bool_func = unittest.mock.MagicMock()
|
||||||
|
|
||||||
obj = Something()
|
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)
|
self.assertEqual(obj.obj_with_bool_func.__bool__.call_count, 0)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Mocks can no longer be used as the specs for other Mocks. As a result, an already-mocked object cannot have an attribute mocked using `autospec=True` or be the subject of a `create_autospec(...)` call. This can uncover bugs in tests since these Mock-derived Mocks will always pass certain tests (e.g. isinstance) and builtin assert functions (e.g. assert_called_once_with) will unconditionally pass.
|
Loading…
Add table
Add a link
Reference in a new issue