Skip to content

Commit dccdc50

Browse files
authored
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).
1 parent ba1db57 commit dccdc50

File tree

4 files changed

+65
-8
lines changed

4 files changed

+65
-8
lines changed

Lib/unittest/mock.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
from functools import wraps, partial
3737

3838

39+
class InvalidSpecError(Exception):
40+
"""Indicates that an invalid value was used as a mock spec."""
41+
42+
3943
_builtins = {name for name in dir(builtins) if not name.startswith('_')}
4044

4145
FILTER_DIR = True
@@ -653,10 +657,17 @@ def __getattr__(self, name):
653657
self._mock_children[name] = result
654658

655659
elif isinstance(result, _SpecState):
656-
result = create_autospec(
657-
result.spec, result.spec_set, result.instance,
658-
result.parent, result.name
659-
)
660+
try:
661+
result = create_autospec(
662+
result.spec, result.spec_set, result.instance,
663+
result.parent, result.name
664+
)
665+
except InvalidSpecError:
666+
target_name = self.__dict__['_mock_name'] or self
667+
raise InvalidSpecError(
668+
f'Cannot autospec attr {name!r} from target '
669+
f'{target_name!r} as it has already been mocked out. '
670+
f'[target={self!r}, attr={result.spec!r}]')
660671
self._mock_children[name] = result
661672

662673
return result
@@ -1273,6 +1284,14 @@ def __init__(
12731284
)
12741285
if not unsafe:
12751286
_check_spec_arg_typos(kwargs)
1287+
if _is_instance_mock(spec):
1288+
raise InvalidSpecError(
1289+
f'Cannot spec attr {attribute!r} as the spec '
1290+
f'has already been mocked out. [spec={spec!r}]')
1291+
if _is_instance_mock(spec_set):
1292+
raise InvalidSpecError(
1293+
f'Cannot spec attr {attribute!r} as the spec_set '
1294+
f'target has already been mocked out. [spec_set={spec_set!r}]')
12761295

12771296
self.getter = getter
12781297
self.attribute = attribute
@@ -1500,6 +1519,18 @@ def __enter__(self):
15001519
if autospec is True:
15011520
autospec = original
15021521

1522+
if _is_instance_mock(self.target):
1523+
raise InvalidSpecError(
1524+
f'Cannot autospec attr {self.attribute!r} as the patch '
1525+
f'target has already been mocked out. '
1526+
f'[target={self.target!r}, attr={autospec!r}]')
1527+
if _is_instance_mock(autospec):
1528+
target_name = getattr(self.target, '__name__', self.target)
1529+
raise InvalidSpecError(
1530+
f'Cannot autospec attr {self.attribute!r} from target '
1531+
f'{target_name!r} as it has already been mocked out. '
1532+
f'[target={self.target!r}, attr={autospec!r}]')
1533+
15031534
new = create_autospec(autospec, spec_set=spec_set,
15041535
_name=self.attribute, **kwargs)
15051536
elif kwargs:
@@ -2613,6 +2644,9 @@ def create_autospec(spec, spec_set=False, instance=False, _parent=None,
26132644
spec = type(spec)
26142645

26152646
is_type = isinstance(spec, type)
2647+
if _is_instance_mock(spec):
2648+
raise InvalidSpecError(f'Cannot autospec a Mock object. '
2649+
f'[object={spec!r}]')
26162650
is_async_func = _is_async_func(spec)
26172651
_kwargs = {'spec': spec}
26182652
if spec_set:

Lib/unittest/test/testmock/testasync.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,9 +199,9 @@ def test_create_autospec_instance(self):
199199
with self.assertRaises(RuntimeError):
200200
create_autospec(async_func, instance=True)
201201

202+
@unittest.skip('Broken test from https://bugs.python.org/issue37251')
202203
def test_create_autospec_awaitable_class(self):
203-
awaitable_mock = create_autospec(spec=AwaitableClass())
204-
self.assertIsInstance(create_autospec(awaitable_mock), AsyncMock)
204+
self.assertIsInstance(create_autospec(AwaitableClass), AsyncMock)
205205

206206
def test_create_autospec(self):
207207
spec = create_autospec(async_func_args)

Lib/unittest/test/testmock/testmock.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
call, DEFAULT, patch, sentinel,
1212
MagicMock, Mock, NonCallableMock,
1313
NonCallableMagicMock, AsyncMock, _Call, _CallList,
14-
create_autospec
14+
create_autospec, InvalidSpecError
1515
)
1616

1717

@@ -205,6 +205,28 @@ def f(): pass
205205
self.assertRaisesRegex(ValueError, 'Bazinga!', mock)
206206

207207

208+
def test_autospec_mock(self):
209+
class A(object):
210+
class B(object):
211+
C = None
212+
213+
with mock.patch.object(A, 'B'):
214+
with self.assertRaisesRegex(InvalidSpecError,
215+
"Cannot autospec attr 'B' from target <MagicMock spec='A'"):
216+
create_autospec(A).B
217+
with self.assertRaisesRegex(InvalidSpecError,
218+
"Cannot autospec attr 'B' from target 'A'"):
219+
mock.patch.object(A, 'B', autospec=True).start()
220+
with self.assertRaisesRegex(InvalidSpecError,
221+
"Cannot autospec attr 'C' as the patch target "):
222+
mock.patch.object(A.B, 'C', autospec=True).start()
223+
with self.assertRaisesRegex(InvalidSpecError,
224+
"Cannot spec attr 'B' as the spec "):
225+
mock.patch.object(A, 'B', spec=A.B).start()
226+
with self.assertRaisesRegex(InvalidSpecError,
227+
"Cannot spec attr 'B' as the spec_set "):
228+
mock.patch.object(A, 'B', spec_set=A.B).start()
229+
208230
def test_reset_mock(self):
209231
parent = Mock()
210232
spec = ["something"]
@@ -2177,7 +2199,7 @@ def __init__(self):
21772199
self.obj_with_bool_func = unittest.mock.MagicMock()
21782200

21792201
obj = Something()
2180-
with unittest.mock.patch.object(obj, 'obj_with_bool_func', autospec=True): pass
2202+
with unittest.mock.patch.object(obj, 'obj_with_bool_func', spec=object): pass
21812203

21822204
self.assertEqual(obj.obj_with_bool_func.__bool__.call_count, 0)
21832205

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

0 commit comments

Comments
 (0)