Skip to content

Commit 4b222c9

Browse files
bpo-40126: Fix reverting multiple patches in unittest.mock. (GH-19351)
Patcher's __exit__() is now never called if its __enter__() is failed. Returning true from __exit__() silences now the exception.
1 parent cd8295f commit 4b222c9

File tree

3 files changed

+30
-49
lines changed

3 files changed

+30
-49
lines changed

Lib/unittest/mock.py

Lines changed: 26 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,11 +1241,6 @@ def _importer(target):
12411241
return thing
12421242

12431243

1244-
def _is_started(patcher):
1245-
# XXXX horrible
1246-
return hasattr(patcher, 'is_local')
1247-
1248-
12491244
class _patch(object):
12501245

12511246
attribute_name = None
@@ -1316,34 +1311,16 @@ def decorate_class(self, klass):
13161311
@contextlib.contextmanager
13171312
def decoration_helper(self, patched, args, keywargs):
13181313
extra_args = []
1319-
entered_patchers = []
1320-
patching = None
1321-
1322-
exc_info = tuple()
1323-
try:
1314+
with contextlib.ExitStack() as exit_stack:
13241315
for patching in patched.patchings:
1325-
arg = patching.__enter__()
1326-
entered_patchers.append(patching)
1316+
arg = exit_stack.enter_context(patching)
13271317
if patching.attribute_name is not None:
13281318
keywargs.update(arg)
13291319
elif patching.new is DEFAULT:
13301320
extra_args.append(arg)
13311321

13321322
args += tuple(extra_args)
13331323
yield (args, keywargs)
1334-
except:
1335-
if (patching not in entered_patchers and
1336-
_is_started(patching)):
1337-
# the patcher may have been started, but an exception
1338-
# raised whilst entering one of its additional_patchers
1339-
entered_patchers.append(patching)
1340-
# Pass the exception to __exit__
1341-
exc_info = sys.exc_info()
1342-
# re-raise the exception
1343-
raise
1344-
finally:
1345-
for patching in reversed(entered_patchers):
1346-
patching.__exit__(*exc_info)
13471324

13481325

13491326
def decorate_callable(self, func):
@@ -1520,25 +1497,26 @@ def __enter__(self):
15201497

15211498
self.temp_original = original
15221499
self.is_local = local
1523-
setattr(self.target, self.attribute, new_attr)
1524-
if self.attribute_name is not None:
1525-
extra_args = {}
1526-
if self.new is DEFAULT:
1527-
extra_args[self.attribute_name] = new
1528-
for patching in self.additional_patchers:
1529-
arg = patching.__enter__()
1530-
if patching.new is DEFAULT:
1531-
extra_args.update(arg)
1532-
return extra_args
1533-
1534-
return new
1535-
1500+
self._exit_stack = contextlib.ExitStack()
1501+
try:
1502+
setattr(self.target, self.attribute, new_attr)
1503+
if self.attribute_name is not None:
1504+
extra_args = {}
1505+
if self.new is DEFAULT:
1506+
extra_args[self.attribute_name] = new
1507+
for patching in self.additional_patchers:
1508+
arg = self._exit_stack.enter_context(patching)
1509+
if patching.new is DEFAULT:
1510+
extra_args.update(arg)
1511+
return extra_args
1512+
1513+
return new
1514+
except:
1515+
if not self.__exit__(*sys.exc_info()):
1516+
raise
15361517

15371518
def __exit__(self, *exc_info):
15381519
"""Undo the patch."""
1539-
if not _is_started(self):
1540-
return
1541-
15421520
if self.is_local and self.temp_original is not DEFAULT:
15431521
setattr(self.target, self.attribute, self.temp_original)
15441522
else:
@@ -1553,9 +1531,9 @@ def __exit__(self, *exc_info):
15531531
del self.temp_original
15541532
del self.is_local
15551533
del self.target
1556-
for patcher in reversed(self.additional_patchers):
1557-
if _is_started(patcher):
1558-
patcher.__exit__(*exc_info)
1534+
exit_stack = self._exit_stack
1535+
del self._exit_stack
1536+
return exit_stack.__exit__(*exc_info)
15591537

15601538

15611539
def start(self):
@@ -1571,9 +1549,9 @@ def stop(self):
15711549
self._active_patches.remove(self)
15721550
except ValueError:
15731551
# If the patch hasn't been started this will fail
1574-
pass
1552+
return None
15751553

1576-
return self.__exit__()
1554+
return self.__exit__(None, None, None)
15771555

15781556

15791557

@@ -1873,9 +1851,9 @@ def stop(self):
18731851
_patch._active_patches.remove(self)
18741852
except ValueError:
18751853
# If the patch hasn't been started this will fail
1876-
pass
1854+
return None
18771855

1878-
return self.__exit__()
1856+
return self.__exit__(None, None, None)
18791857

18801858

18811859
def _clear_dict(in_dict):

Lib/unittest/test/testmock/testpatch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ def test_patch_dict_stop_without_start(self):
774774
d = {'foo': 'bar'}
775775
original = d.copy()
776776
patcher = patch.dict(d, [('spam', 'eggs')], clear=True)
777-
self.assertEqual(patcher.stop(), False)
777+
self.assertFalse(patcher.stop())
778778
self.assertEqual(d, original)
779779

780780

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed reverting multiple patches in unittest.mock. Patcher's ``__exit__()``
2+
is now never called if its ``__enter__()`` is failed. Returning true from
3+
``__exit__()`` silences now the exception.

0 commit comments

Comments
 (0)