Skip to content

Commit 951ed33

Browse files
miss-islingtonRed4RuZeroIntensitypicnixz
authored
[3.12] gh-104745: Limit starting a patcher more than once without stopping it (GH-126649) (#126773)
gh-104745: Limit starting a patcher more than once without stopping it (GH-126649) Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --------- (cherry picked from commit 1e40c5b) Co-authored-by: Red4Ru <[email protected]> Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 361dda5 commit 951ed33

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

Lib/test/test_unittest/testmock/testpatch.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -745,6 +745,54 @@ def test_stop_idempotent(self):
745745
self.assertIsNone(patcher.stop())
746746

747747

748+
def test_exit_idempotent(self):
749+
patcher = patch(foo_name, 'bar', 3)
750+
with patcher:
751+
patcher.stop()
752+
753+
754+
def test_second_start_failure(self):
755+
patcher = patch(foo_name, 'bar', 3)
756+
patcher.start()
757+
try:
758+
self.assertRaises(RuntimeError, patcher.start)
759+
finally:
760+
patcher.stop()
761+
762+
763+
def test_second_enter_failure(self):
764+
patcher = patch(foo_name, 'bar', 3)
765+
with patcher:
766+
self.assertRaises(RuntimeError, patcher.start)
767+
768+
769+
def test_second_start_after_stop(self):
770+
patcher = patch(foo_name, 'bar', 3)
771+
patcher.start()
772+
patcher.stop()
773+
patcher.start()
774+
patcher.stop()
775+
776+
777+
def test_property_setters(self):
778+
mock_object = Mock()
779+
mock_bar = mock_object.bar
780+
patcher = patch.object(mock_object, 'bar', 'x')
781+
with patcher:
782+
self.assertEqual(patcher.is_local, False)
783+
self.assertIs(patcher.target, mock_object)
784+
self.assertEqual(patcher.temp_original, mock_bar)
785+
patcher.is_local = True
786+
patcher.target = mock_bar
787+
patcher.temp_original = mock_object
788+
self.assertEqual(patcher.is_local, True)
789+
self.assertIs(patcher.target, mock_bar)
790+
self.assertEqual(patcher.temp_original, mock_object)
791+
# if changes are left intact, they may lead to disruption as shown below (it might be what someone needs though)
792+
self.assertEqual(mock_bar.bar, mock_object)
793+
self.assertEqual(mock_object.bar, 'x')
794+
795+
748796
def test_patchobject_start_stop(self):
749797
original = something
750798
patcher = patch.object(PTModule, 'something', 'foo')
@@ -1098,7 +1146,7 @@ def test_new_callable_patch(self):
10981146

10991147
self.assertIsNot(m1, m2)
11001148
for mock in m1, m2:
1101-
self.assertNotCallable(m1)
1149+
self.assertNotCallable(mock)
11021150

11031151

11041152
def test_new_callable_patch_object(self):
@@ -1111,7 +1159,7 @@ def test_new_callable_patch_object(self):
11111159

11121160
self.assertIsNot(m1, m2)
11131161
for mock in m1, m2:
1114-
self.assertNotCallable(m1)
1162+
self.assertNotCallable(mock)
11151163

11161164

11171165
def test_new_callable_keyword_arguments(self):

Lib/unittest/mock.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,7 @@ def __init__(
13291329
self.autospec = autospec
13301330
self.kwargs = kwargs
13311331
self.additional_patchers = []
1332+
self.is_started = False
13321333

13331334

13341335
def copy(self):
@@ -1441,6 +1442,9 @@ def get_original(self):
14411442

14421443
def __enter__(self):
14431444
"""Perform the patch."""
1445+
if self.is_started:
1446+
raise RuntimeError("Patch is already started")
1447+
14441448
new, spec, spec_set = self.new, self.spec, self.spec_set
14451449
autospec, kwargs = self.autospec, self.kwargs
14461450
new_callable = self.new_callable
@@ -1572,6 +1576,7 @@ def __enter__(self):
15721576
self.temp_original = original
15731577
self.is_local = local
15741578
self._exit_stack = contextlib.ExitStack()
1579+
self.is_started = True
15751580
try:
15761581
setattr(self.target, self.attribute, new_attr)
15771582
if self.attribute_name is not None:
@@ -1591,6 +1596,9 @@ def __enter__(self):
15911596

15921597
def __exit__(self, *exc_info):
15931598
"""Undo the patch."""
1599+
if not self.is_started:
1600+
return
1601+
15941602
if self.is_local and self.temp_original is not DEFAULT:
15951603
setattr(self.target, self.attribute, self.temp_original)
15961604
else:
@@ -1607,6 +1615,7 @@ def __exit__(self, *exc_info):
16071615
del self.target
16081616
exit_stack = self._exit_stack
16091617
del self._exit_stack
1618+
self.is_started = False
16101619
return exit_stack.__exit__(*exc_info)
16111620

16121621

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Limit starting a patcher (from :func:`unittest.mock.patch` or
2+
:func:`unittest.mock.patch.object`) more than
3+
once without stopping it

0 commit comments

Comments
 (0)