[Python-checkins] bpo-45156: Fixes inifite loop on unittest.mock.seal() (GH-28300) (GH-28326)

vstinner webhook-mailer at python.org
Tue Sep 14 07:00:21 EDT 2021


https://github.com/python/cpython/commit/fd74d2680ef96c0140bc02cf94d1cf1f2ef814c2
commit: fd74d2680ef96c0140bc02cf94d1cf1f2ef814c2
branch: 3.10
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: vstinner <vstinner at python.org>
date: 2021-09-14T13:00:16+02:00
summary:

bpo-45156: Fixes inifite loop on unittest.mock.seal() (GH-28300) (GH-28326)

Fixes infinite loop on unittest.mock.seal() of mocks created by
unittest.create_autospec().

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

Co-authored-by: Nikita Sobolev <mail at sobolevn.me>

files:
A Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst
M Lib/unittest/mock.py
M Lib/unittest/test/testmock/testsealable.py

diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py
index c6067151de14f..6226bd4bc0c19 100644
--- a/Lib/unittest/mock.py
+++ b/Lib/unittest/mock.py
@@ -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
@@ -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)
 
 
@@ -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)
 
diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py
index 59f52338d411e..11784c3678918 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 0000000000000..b2094b5765331
--- /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`.



More information about the Python-checkins mailing list