Skip to content

Commit ee249d7

Browse files
[3.8] bpo-40126: Fix reverting multiple patches in unittest.mock. (GH-19351) (GH-19483)
Patcher's __exit__() is now never called if its __enter__() is failed. Returning true from __exit__() silences now the exception. (cherry picked from commit 4b222c9) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 2714c90 commit ee249d7

File tree

2 files changed

+27
-46
lines changed

2 files changed

+27
-46
lines changed

Lib/unittest/mock.py

Lines changed: 24 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1229,11 +1229,6 @@ def _importer(target):
12291229
return thing
12301230

12311231

1232-
def _is_started(patcher):
1233-
# XXXX horrible
1234-
return hasattr(patcher, 'is_local')
1235-
1236-
12371232
class _patch(object):
12381233

12391234
attribute_name = None
@@ -1304,34 +1299,16 @@ def decorate_class(self, klass):
13041299
@contextlib.contextmanager
13051300
def decoration_helper(self, patched, args, keywargs):
13061301
extra_args = []
1307-
entered_patchers = []
1308-
patching = None
1309-
1310-
exc_info = tuple()
1311-
try:
1302+
with contextlib.ExitStack() as exit_stack:
13121303
for patching in patched.patchings:
1313-
arg = patching.__enter__()
1314-
entered_patchers.append(patching)
1304+
arg = exit_stack.enter_context(patching)
13151305
if patching.attribute_name is not None:
13161306
keywargs.update(arg)
13171307
elif patching.new is DEFAULT:
13181308
extra_args.append(arg)
13191309

13201310
args += tuple(extra_args)
13211311
yield (args, keywargs)
1322-
except:
1323-
if (patching not in entered_patchers and
1324-
_is_started(patching)):
1325-
# the patcher may have been started, but an exception
1326-
# raised whilst entering one of its additional_patchers
1327-
entered_patchers.append(patching)
1328-
# Pass the exception to __exit__
1329-
exc_info = sys.exc_info()
1330-
# re-raise the exception
1331-
raise
1332-
finally:
1333-
for patching in reversed(entered_patchers):
1334-
patching.__exit__(*exc_info)
13351312

13361313

13371314
def decorate_callable(self, func):
@@ -1508,25 +1485,26 @@ def __enter__(self):
15081485

15091486
self.temp_original = original
15101487
self.is_local = local
1511-
setattr(self.target, self.attribute, new_attr)
1512-
if self.attribute_name is not None:
1513-
extra_args = {}
1514-
if self.new is DEFAULT:
1515-
extra_args[self.attribute_name] = new
1516-
for patching in self.additional_patchers:
1517-
arg = patching.__enter__()
1518-
if patching.new is DEFAULT:
1519-
extra_args.update(arg)
1520-
return extra_args
1521-
1522-
return new
1523-
1488+
self._exit_stack = contextlib.ExitStack()
1489+
try:
1490+
setattr(self.target, self.attribute, new_attr)
1491+
if self.attribute_name is not None:
1492+
extra_args = {}
1493+
if self.new is DEFAULT:
1494+
extra_args[self.attribute_name] = new
1495+
for patching in self.additional_patchers:
1496+
arg = self._exit_stack.enter_context(patching)
1497+
if patching.new is DEFAULT:
1498+
extra_args.update(arg)
1499+
return extra_args
1500+
1501+
return new
1502+
except:
1503+
if not self.__exit__(*sys.exc_info()):
1504+
raise
15241505

15251506
def __exit__(self, *exc_info):
15261507
"""Undo the patch."""
1527-
if not _is_started(self):
1528-
return
1529-
15301508
if self.is_local and self.temp_original is not DEFAULT:
15311509
setattr(self.target, self.attribute, self.temp_original)
15321510
else:
@@ -1541,9 +1519,9 @@ def __exit__(self, *exc_info):
15411519
del self.temp_original
15421520
del self.is_local
15431521
del self.target
1544-
for patcher in reversed(self.additional_patchers):
1545-
if _is_started(patcher):
1546-
patcher.__exit__(*exc_info)
1522+
exit_stack = self._exit_stack
1523+
del self._exit_stack
1524+
return exit_stack.__exit__(*exc_info)
15471525

15481526

15491527
def start(self):
@@ -1559,9 +1537,9 @@ def stop(self):
15591537
self._active_patches.remove(self)
15601538
except ValueError:
15611539
# If the patch hasn't been started this will fail
1562-
pass
1540+
return None
15631541

1564-
return self.__exit__()
1542+
return self.__exit__(None, None, None)
15651543

15661544

15671545

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)