Skip to content

Commit

Permalink
[3.13] pythongh-104745: Limit starting a patcher more than once witho…
Browse files Browse the repository at this point in the history
…ut stopping it (pythonGH-126649) (python#126772)

pythongh-104745: Limit starting a patcher more than once without stopping it (pythonGH-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]>
  • Loading branch information
4 people authored Nov 13, 2024
1 parent b03ba54 commit e8dbe7e
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 2 deletions.
52 changes: 50 additions & 2 deletions Lib/test/test_unittest/testmock/testpatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,54 @@ def test_stop_idempotent(self):
self.assertIsNone(patcher.stop())


def test_exit_idempotent(self):
patcher = patch(foo_name, 'bar', 3)
with patcher:
patcher.stop()


def test_second_start_failure(self):
patcher = patch(foo_name, 'bar', 3)
patcher.start()
try:
self.assertRaises(RuntimeError, patcher.start)
finally:
patcher.stop()


def test_second_enter_failure(self):
patcher = patch(foo_name, 'bar', 3)
with patcher:
self.assertRaises(RuntimeError, patcher.start)


def test_second_start_after_stop(self):
patcher = patch(foo_name, 'bar', 3)
patcher.start()
patcher.stop()
patcher.start()
patcher.stop()


def test_property_setters(self):
mock_object = Mock()
mock_bar = mock_object.bar
patcher = patch.object(mock_object, 'bar', 'x')
with patcher:
self.assertEqual(patcher.is_local, False)
self.assertIs(patcher.target, mock_object)
self.assertEqual(patcher.temp_original, mock_bar)
patcher.is_local = True
patcher.target = mock_bar
patcher.temp_original = mock_object
self.assertEqual(patcher.is_local, True)
self.assertIs(patcher.target, mock_bar)
self.assertEqual(patcher.temp_original, mock_object)
# if changes are left intact, they may lead to disruption as shown below (it might be what someone needs though)
self.assertEqual(mock_bar.bar, mock_object)
self.assertEqual(mock_object.bar, 'x')


def test_patchobject_start_stop(self):
original = something
patcher = patch.object(PTModule, 'something', 'foo')
Expand Down Expand Up @@ -1098,7 +1146,7 @@ def test_new_callable_patch(self):

self.assertIsNot(m1, m2)
for mock in m1, m2:
self.assertNotCallable(m1)
self.assertNotCallable(mock)


def test_new_callable_patch_object(self):
Expand All @@ -1111,7 +1159,7 @@ def test_new_callable_patch_object(self):

self.assertIsNot(m1, m2)
for mock in m1, m2:
self.assertNotCallable(m1)
self.assertNotCallable(mock)


def test_new_callable_keyword_arguments(self):
Expand Down
9 changes: 9 additions & 0 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,7 @@ def __init__(
self.autospec = autospec
self.kwargs = kwargs
self.additional_patchers = []
self.is_started = False


def copy(self):
Expand Down Expand Up @@ -1469,6 +1470,9 @@ def get_original(self):

def __enter__(self):
"""Perform the patch."""
if self.is_started:
raise RuntimeError("Patch is already started")

new, spec, spec_set = self.new, self.spec, self.spec_set
autospec, kwargs = self.autospec, self.kwargs
new_callable = self.new_callable
Expand Down Expand Up @@ -1600,6 +1604,7 @@ def __enter__(self):
self.temp_original = original
self.is_local = local
self._exit_stack = contextlib.ExitStack()
self.is_started = True
try:
setattr(self.target, self.attribute, new_attr)
if self.attribute_name is not None:
Expand All @@ -1619,6 +1624,9 @@ def __enter__(self):

def __exit__(self, *exc_info):
"""Undo the patch."""
if not self.is_started:
return

if self.is_local and self.temp_original is not DEFAULT:
setattr(self.target, self.attribute, self.temp_original)
else:
Expand All @@ -1635,6 +1643,7 @@ def __exit__(self, *exc_info):
del self.target
exit_stack = self._exit_stack
del self._exit_stack
self.is_started = False
return exit_stack.__exit__(*exc_info)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Limit starting a patcher (from :func:`unittest.mock.patch` or
:func:`unittest.mock.patch.object`) more than
once without stopping it

0 comments on commit e8dbe7e

Please sign in to comment.