Skip to content

Commit 1e40c5b

Browse files
Red4RuZeroIntensitypicnixz
authored
gh-104745: Limit starting a patcher more than once without stopping it (#126649)
Previously, this would cause an `AttributeError` if the patch stopped more than once after this, and would also disrupt the original patched object. --------- Co-authored-by: Peter Bierma <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]>
1 parent 2e39d77 commit 1e40c5b

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
@@ -1360,6 +1360,7 @@ def __init__(
13601360
self.autospec = autospec
13611361
self.kwargs = kwargs
13621362
self.additional_patchers = []
1363+
self.is_started = False
13631364

13641365

13651366
def copy(self):
@@ -1472,6 +1473,9 @@ def get_original(self):
14721473

14731474
def __enter__(self):
14741475
"""Perform the patch."""
1476+
if self.is_started:
1477+
raise RuntimeError("Patch is already started")
1478+
14751479
new, spec, spec_set = self.new, self.spec, self.spec_set
14761480
autospec, kwargs = self.autospec, self.kwargs
14771481
new_callable = self.new_callable
@@ -1603,6 +1607,7 @@ def __enter__(self):
16031607
self.temp_original = original
16041608
self.is_local = local
16051609
self._exit_stack = contextlib.ExitStack()
1610+
self.is_started = True
16061611
try:
16071612
setattr(self.target, self.attribute, new_attr)
16081613
if self.attribute_name is not None:
@@ -1622,6 +1627,9 @@ def __enter__(self):
16221627

16231628
def __exit__(self, *exc_info):
16241629
"""Undo the patch."""
1630+
if not self.is_started:
1631+
return
1632+
16251633
if self.is_local and self.temp_original is not DEFAULT:
16261634
setattr(self.target, self.attribute, self.temp_original)
16271635
else:
@@ -1638,6 +1646,7 @@ def __exit__(self, *exc_info):
16381646
del self.target
16391647
exit_stack = self._exit_stack
16401648
del self._exit_stack
1649+
self.is_started = False
16411650
return exit_stack.__exit__(*exc_info)
16421651

16431652

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)