Skip to content

Commit 13257d9

Browse files
bpo-45156: Fixes inifite loop on unittest.mock.seal() (GH-28300)
Fixes infinite loop on unittest.mock.seal() of mocks created by unittest.create_autospec(). Co-authored-by: Dong-hee Na <[email protected]> (cherry picked from commit 7f60c9e) Co-authored-by: Nikita Sobolev <[email protected]>
1 parent 9cd8fb8 commit 13257d9

File tree

3 files changed

+70
-6
lines changed

3 files changed

+70
-6
lines changed

Lib/unittest/mock.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,11 @@ def _get_child_mock(self, /, **kw):
992992
if _new_name in self.__dict__['_spec_asyncs']:
993993
return AsyncMock(**kw)
994994

995+
if self._mock_sealed:
996+
attribute = f".{kw['name']}" if "name" in kw else "()"
997+
mock_name = self._extract_mock_name() + attribute
998+
raise AttributeError(mock_name)
999+
9951000
_type = type(self)
9961001
if issubclass(_type, MagicMock) and _new_name in _async_method_magics:
9971002
# Any asynchronous magic becomes an AsyncMock
@@ -1010,12 +1015,6 @@ def _get_child_mock(self, /, **kw):
10101015
klass = Mock
10111016
else:
10121017
klass = _type.__mro__[1]
1013-
1014-
if self._mock_sealed:
1015-
attribute = "." + kw["name"] if "name" in kw else "()"
1016-
mock_name = self._extract_mock_name() + attribute
1017-
raise AttributeError(mock_name)
1018-
10191018
return klass(**kw)
10201019

10211020

@@ -2869,6 +2868,8 @@ def seal(mock):
28692868
continue
28702869
if not isinstance(m, NonCallableMock):
28712870
continue
2871+
if isinstance(m._mock_children.get(attr), _SpecState):
2872+
continue
28722873
if m._mock_new_parent is mock:
28732874
seal(m)
28742875

Lib/unittest/test/testmock/testsealable.py

+61
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,67 @@ def test_call_chain_is_maintained(self):
171171
m.test1().test2.test3().test4()
172172
self.assertIn("mock.test1().test2.test3().test4", str(cm.exception))
173173

174+
def test_seal_with_autospec(self):
175+
# https://bugs.python.org/issue45156
176+
class Foo:
177+
foo = 0
178+
def bar1(self):
179+
return 1
180+
def bar2(self):
181+
return 2
182+
183+
class Baz:
184+
baz = 3
185+
def ban(self):
186+
return 4
187+
188+
for spec_set in (True, False):
189+
with self.subTest(spec_set=spec_set):
190+
foo = mock.create_autospec(Foo, spec_set=spec_set)
191+
foo.bar1.return_value = 'a'
192+
foo.Baz.ban.return_value = 'b'
193+
194+
mock.seal(foo)
195+
196+
self.assertIsInstance(foo.foo, mock.NonCallableMagicMock)
197+
self.assertIsInstance(foo.bar1, mock.MagicMock)
198+
self.assertIsInstance(foo.bar2, mock.MagicMock)
199+
self.assertIsInstance(foo.Baz, mock.MagicMock)
200+
self.assertIsInstance(foo.Baz.baz, mock.NonCallableMagicMock)
201+
self.assertIsInstance(foo.Baz.ban, mock.MagicMock)
202+
203+
self.assertEqual(foo.bar1(), 'a')
204+
foo.bar1.return_value = 'new_a'
205+
self.assertEqual(foo.bar1(), 'new_a')
206+
self.assertEqual(foo.Baz.ban(), 'b')
207+
foo.Baz.ban.return_value = 'new_b'
208+
self.assertEqual(foo.Baz.ban(), 'new_b')
209+
210+
with self.assertRaises(TypeError):
211+
foo.foo()
212+
with self.assertRaises(AttributeError):
213+
foo.bar = 1
214+
with self.assertRaises(AttributeError):
215+
foo.bar2()
216+
217+
foo.bar2.return_value = 'bar2'
218+
self.assertEqual(foo.bar2(), 'bar2')
219+
220+
with self.assertRaises(AttributeError):
221+
foo.missing_attr
222+
with self.assertRaises(AttributeError):
223+
foo.missing_attr = 1
224+
with self.assertRaises(AttributeError):
225+
foo.missing_method()
226+
with self.assertRaises(TypeError):
227+
foo.Baz.baz()
228+
with self.assertRaises(AttributeError):
229+
foo.Baz.missing_attr
230+
with self.assertRaises(AttributeError):
231+
foo.Baz.missing_attr = 1
232+
with self.assertRaises(AttributeError):
233+
foo.Baz.missing_method()
234+
174235

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

0 commit comments

Comments
 (0)