From 70f77aa9c7d5a27b3eb0ddf3a2369dc34f340a05 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 13 Sep 2021 00:28:45 +0300 Subject: [PATCH 1/7] bpo-45156: fixes inifite loop on `mock.seal()` --- Lib/unittest/mock.py | 13 ++-- Lib/unittest/test/testmock/testsealable.py | 63 +++++++++++++++++++ .../2021-09-13-00-28-17.bpo-45156.8oomV3.rst | 2 + 3 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 8193efc7cf1257..3c185ca1f51da5 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 = "." + 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..eccd1917edac56 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -171,6 +171,69 @@ 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_attr_autospec(self): + # https://bugs.python.org/issue45156 + class Foo: + foo = 0 + + for spec_set in (True, False): + with self.subTest(spec_set=spec_set): + foo = mock.create_autospec(Foo, spec_set=spec_set) + mock.seal(foo) + + self.assertIsInstance(foo.foo, mock.NonCallableMagicMock) + with self.assertRaises(TypeError): + foo.foo() + with self.assertRaises(AttributeError): + foo.bar = 1 + with self.assertRaises(AttributeError): + foo.missing_attr + with self.assertRaises(AttributeError): + foo.missing_method() + + def test_seal_with_method_autospec(self): + # https://bugs.python.org/issue45156 + class Foo: + def foo(self): + return 1 + + for spec_set in (True, False): + with self.subTest(spec_set=spec_set): + foo = mock.create_autospec(Foo, spec_set=spec_set) + foo.foo.return_value = 0 + mock.seal(foo) + + self.assertIsInstance(foo.foo, mock.MagicMock) + self.assertEqual(foo.foo(), 0) + with self.assertRaises(AttributeError): + foo.bar = 1 + with self.assertRaises(AttributeError): + foo.missing_attr + with self.assertRaises(AttributeError): + foo.missing_method() + + def test_seal_with_nested_class_autospec(self): + # https://bugs.python.org/issue45156 + class Foo: + class Baz: + def baz(self): + return 1 + + for spec_set in (True, False): + with self.subTest(spec_set=spec_set): + foo = mock.create_autospec(Foo, spec_set=spec_set) + foo.Baz.baz.return_value = 0 + mock.seal(foo) + + self.assertIsInstance(foo.Baz.baz, mock.MagicMock) + self.assertEqual(foo.Baz.baz(), 0) + with self.assertRaises(AttributeError): + foo.bar = 1 + with self.assertRaises(AttributeError): + foo.Baz.bar = 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..d499d1ddf7d6cf --- /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 ``unittest.seal()`` of mocks created by +``mock.create_autospec()``. From 8d7824bc7bc0118eaea68169bb5b3eceae017023 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Mon, 13 Sep 2021 02:07:04 +0300 Subject: [PATCH 2/7] Simplifies test case --- Lib/unittest/test/testmock/testsealable.py | 72 +++++++++++----------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/Lib/unittest/test/testmock/testsealable.py b/Lib/unittest/test/testmock/testsealable.py index eccd1917edac56..11784c3678918f 100644 --- a/Lib/unittest/test/testmock/testsealable.py +++ b/Lib/unittest/test/testmock/testsealable.py @@ -171,66 +171,64 @@ 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_attr_autospec(self): + 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.missing_attr - with self.assertRaises(AttributeError): - foo.missing_method() + foo.bar2() - def test_seal_with_method_autospec(self): - # https://bugs.python.org/issue45156 - class Foo: - def foo(self): - return 1 - - for spec_set in (True, False): - with self.subTest(spec_set=spec_set): - foo = mock.create_autospec(Foo, spec_set=spec_set) - foo.foo.return_value = 0 - mock.seal(foo) + foo.bar2.return_value = 'bar2' + self.assertEqual(foo.bar2(), 'bar2') - self.assertIsInstance(foo.foo, mock.MagicMock) - self.assertEqual(foo.foo(), 0) - with self.assertRaises(AttributeError): - foo.bar = 1 with self.assertRaises(AttributeError): foo.missing_attr + with self.assertRaises(AttributeError): + foo.missing_attr = 1 with self.assertRaises(AttributeError): foo.missing_method() - - def test_seal_with_nested_class_autospec(self): - # https://bugs.python.org/issue45156 - class Foo: - class Baz: - def baz(self): - return 1 - - for spec_set in (True, False): - with self.subTest(spec_set=spec_set): - foo = mock.create_autospec(Foo, spec_set=spec_set) - foo.Baz.baz.return_value = 0 - mock.seal(foo) - - self.assertIsInstance(foo.Baz.baz, mock.MagicMock) - self.assertEqual(foo.Baz.baz(), 0) + with self.assertRaises(TypeError): + foo.Baz.baz() with self.assertRaises(AttributeError): - foo.bar = 1 + foo.Baz.missing_attr with self.assertRaises(AttributeError): - foo.Baz.bar = 1 + foo.Baz.missing_attr = 1 with self.assertRaises(AttributeError): foo.Baz.missing_method() From 3c87eeac92edd2fcf1ec3240beaf01296a94dc77 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 13 Sep 2021 16:36:04 +0300 Subject: [PATCH 3/7] Update Lib/unittest/mock.py Co-authored-by: Dong-hee Na --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 3c185ca1f51da5..d74d5d61e962fd 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1006,7 +1006,7 @@ def _get_child_mock(self, /, **kw): return AsyncMock(**kw) if self._mock_sealed: - attribute = "." + kw["name"] if "name" in kw else "()" + attribute = f".{kw['name']} if "name" in kw else "()" mock_name = self._extract_mock_name() + attribute raise AttributeError(mock_name) From 1b70cd44fdd711dcf63746029bbc55a2aa2b661c Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 13 Sep 2021 16:36:10 +0300 Subject: [PATCH 4/7] Update Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst Co-authored-by: Dong-hee Na --- Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index d499d1ddf7d6cf..98c97c943650dc 100644 --- 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 @@ -1,2 +1,2 @@ Fixes infinite loop on ``unittest.seal()`` of mocks created by -``mock.create_autospec()``. +:func:`~unittest. create_autospec `. From 8238aec4acd824a49016adb00c31de08c9a34efe Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 13 Sep 2021 16:36:14 +0300 Subject: [PATCH 5/7] Update Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst Co-authored-by: Dong-hee Na --- Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 98c97c943650dc..feddb13e95e7c5 100644 --- 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 @@ -1,2 +1,2 @@ -Fixes infinite loop on ``unittest.seal()`` of mocks created by +Fixes infinite loop on :func:`unittest.mock.seal` of mocks created by :func:`~unittest. create_autospec `. From 9fb0b33469784cbe57324cedf00d0d46881d4e54 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 13 Sep 2021 16:44:34 +0300 Subject: [PATCH 6/7] Fixes typo --- Lib/unittest/mock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index d74d5d61e962fd..9f99a5aa5bcdcb 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -1006,7 +1006,7 @@ def _get_child_mock(self, /, **kw): return AsyncMock(**kw) if self._mock_sealed: - attribute = f".{kw['name']} if "name" in kw else "()" + attribute = f".{kw['name']}" if "name" in kw else "()" mock_name = self._extract_mock_name() + attribute raise AttributeError(mock_name) From b15624826b4ccf31a2804c21131137826d7a3913 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Mon, 13 Sep 2021 17:03:20 +0300 Subject: [PATCH 7/7] Fixes typo --- Misc/NEWS.d/next/Tests/2021-09-13-00-28-17.bpo-45156.8oomV3.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index feddb13e95e7c5..b2094b5765331c 100644 --- 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 @@ -1,2 +1,2 @@ Fixes infinite loop on :func:`unittest.mock.seal` of mocks created by -:func:`~unittest. create_autospec `. +:func:`~unittest.create_autospec`.