Skip to content

Commit fd74d26

Browse files
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 <[email protected]> (cherry picked from commit 7f60c9e) Co-authored-by: Nikita Sobolev <[email protected]>
1 parent a390bb6 commit fd74d26

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
@@ -1004,6 +1004,11 @@ def _get_child_mock(self, /, **kw):
10041004
if _new_name in self.__dict__['_spec_asyncs']:
10051005
return AsyncMock(**kw)
10061006

1007+
if self._mock_sealed:
1008+
attribute = f".{kw['name']}" if "name" in kw else "()"
1009+
mock_name = self._extract_mock_name() + attribute
1010+
raise AttributeError(mock_name)
1011+
10071012
_type = type(self)
10081013
if issubclass(_type, MagicMock) and _new_name in _async_method_magics:
10091014
# Any asynchronous magic becomes an AsyncMock
@@ -1022,12 +1027,6 @@ def _get_child_mock(self, /, **kw):
10221027
klass = Mock
10231028
else:
10241029
klass = _type.__mro__[1]
1025-
1026-
if self._mock_sealed:
1027-
attribute = "." + kw["name"] if "name" in kw else "()"
1028-
mock_name = self._extract_mock_name() + attribute
1029-
raise AttributeError(mock_name)
1030-
10311030
return klass(**kw)
10321031

10331032

@@ -2927,6 +2926,8 @@ def seal(mock):
29272926
continue
29282927
if not isinstance(m, NonCallableMock):
29292928
continue
2929+
if isinstance(m._mock_children.get(attr), _SpecState):
2930+
continue
29302931
if m._mock_new_parent is mock:
29312932
seal(m)
29322933

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)