diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8193efc7cf1257..9f99a5aa5bcdcb 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1005,6 +1005,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 @@ -1023,12 +1028,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) @@ -2913,6 +2912,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) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py index 59f52338d411e6..11784c3678918f 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -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() diff --git a/Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst b/Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst new file mode 100644 index 00000000000000..b2094b5765331c --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst @@ -0,0 +1,2 @@ +Fixes infinite loop on :func:`unittest.mock.seal` of mocks created by +:func:`~unittest.create_autospec`.