Skip to content

Commit

Permalink
bpo-45156: Fixes inifite loop on unittest.mock.seal() (pythonGH-28300)
Browse files Browse the repository at this point in the history
Fixes infinite loop on unittest.mock.seal() of mocks created by
unittest.create_autospec().

Co-authored-by: Dong-hee Na <donghee.na92@gmail.com>
(cherry picked from commit 7f60c9e)

Co-authored-by: Nikita Sobolev <mail@sobolevn.me>
  • Loading branch information
sobolevn authored and miss-islington committed Sep 14, 2021
1 parent a390bb6 commit 50abca7
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 6 deletions.
13 changes: 7 additions & 6 deletions Lib/unittest/mock.py
Expand Up @@ -1004,6 +1004,11 @@ def _get_child_mock(self, /, **kw):
if _new_name in self.__dict__['_spec_asyncs']:
return AsyncMock(**kw)

if self._mock_sealed:
attribute = f".{kw['name']}" if "name" in kw else "()"
mock_name = self._extract_mock_name() + attribute
raise AttributeError(mock_name)

_type = type(self)
if issubclass(_type, MagicMock) and _new_name in _async_method_magics:
# Any asynchronous magic becomes an AsyncMock
Expand All @@ -1022,12 +1027,6 @@ def _get_child_mock(self, /, **kw):
klass = Mock
else:
klass = _type.__mro__[1]

if self._mock_sealed:
attribute = "." + kw["name"] if "name" in kw else "()"
mock_name = self._extract_mock_name() + attribute
raise AttributeError(mock_name)

return klass(**kw)


Expand Down Expand Up @@ -2927,6 +2926,8 @@ def seal(mock):
continue
if not isinstance(m, NonCallableMock):
continue
if isinstance(m._mock_children.get(attr), _SpecState):
continue
if m._mock_new_parent is mock:
seal(m)

Expand Down
61 changes: 61 additions & 0 deletions Lib/unittest/test/testmock/testsealable.py
Expand Up @@ -171,6 +171,67 @@ def test_call_chain_is_maintained(self):
m.test1().test2.test3().test4()
self.assertIn("mock.test1().test2.test3().test4", str(cm.exception))

def test_seal_with_autospec(self):
# https://bugs.python.org/issue45156
class Foo:
foo = 0
def bar1(self):
return 1
def bar2(self):
return 2

class Baz:
baz = 3
def ban(self):
return 4

for spec_set in (True, False):
with self.subTest(spec_set=spec_set):
foo = mock.create_autospec(Foo, spec_set=spec_set)
foo.bar1.return_value = 'a'
foo.Baz.ban.return_value = 'b'

mock.seal(foo)

self.assertIsInstance(foo.foo, mock.NonCallableMagicMock)
self.assertIsInstance(foo.bar1, mock.MagicMock)
self.assertIsInstance(foo.bar2, mock.MagicMock)
self.assertIsInstance(foo.Baz, mock.MagicMock)
self.assertIsInstance(foo.Baz.baz, mock.NonCallableMagicMock)
self.assertIsInstance(foo.Baz.ban, mock.MagicMock)

self.assertEqual(foo.bar1(), 'a')
foo.bar1.return_value = 'new_a'
self.assertEqual(foo.bar1(), 'new_a')
self.assertEqual(foo.Baz.ban(), 'b')
foo.Baz.ban.return_value = 'new_b'
self.assertEqual(foo.Baz.ban(), 'new_b')

with self.assertRaises(TypeError):
foo.foo()
with self.assertRaises(AttributeError):
foo.bar = 1
with self.assertRaises(AttributeError):
foo.bar2()

foo.bar2.return_value = 'bar2'
self.assertEqual(foo.bar2(), 'bar2')

with self.assertRaises(AttributeError):
foo.missing_attr
with self.assertRaises(AttributeError):
foo.missing_attr = 1
with self.assertRaises(AttributeError):
foo.missing_method()
with self.assertRaises(TypeError):
foo.Baz.baz()
with self.assertRaises(AttributeError):
foo.Baz.missing_attr
with self.assertRaises(AttributeError):
foo.Baz.missing_attr = 1
with self.assertRaises(AttributeError):
foo.Baz.missing_method()


if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,2 @@
Fixes infinite loop on :func:`unittest.mock.seal` of mocks created by
:func:`~unittest.create_autospec`.

0 comments on commit 50abca7

Please sign in to comment.