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.
This commit is contained in:
Serhiy Storchaka 2020-04-11 10:59:24 +03:00 committed by GitHub
parent cd8295ff75
commit 4b222c9491
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 29 additions and 48 deletions

View File

@ -1241,11 +1241,6 @@ def _importer(target):
return thing return thing
def _is_started(patcher):
# XXXX horrible
return hasattr(patcher, 'is_local')
class _patch(object): class _patch(object):
attribute_name = None attribute_name = None
@ -1316,14 +1311,9 @@ class _patch(object):
@contextlib.contextmanager @contextlib.contextmanager
def decoration_helper(self, patched, args, keywargs): def decoration_helper(self, patched, args, keywargs):
extra_args = [] extra_args = []
entered_patchers = [] with contextlib.ExitStack() as exit_stack:
patching = None
exc_info = tuple()
try:
for patching in patched.patchings: for patching in patched.patchings:
arg = patching.__enter__() arg = exit_stack.enter_context(patching)
entered_patchers.append(patching)
if patching.attribute_name is not None: if patching.attribute_name is not None:
keywargs.update(arg) keywargs.update(arg)
elif patching.new is DEFAULT: elif patching.new is DEFAULT:
@ -1331,19 +1321,6 @@ class _patch(object):
args += tuple(extra_args) args += tuple(extra_args)
yield (args, keywargs) yield (args, keywargs)
except:
if (patching not in entered_patchers and
_is_started(patching)):
# the patcher may have been started, but an exception
# raised whilst entering one of its additional_patchers
entered_patchers.append(patching)
# Pass the exception to __exit__
exc_info = sys.exc_info()
# re-raise the exception
raise
finally:
for patching in reversed(entered_patchers):
patching.__exit__(*exc_info)
def decorate_callable(self, func): def decorate_callable(self, func):
@ -1520,25 +1497,26 @@ class _patch(object):
self.temp_original = original self.temp_original = original
self.is_local = local self.is_local = local
self._exit_stack = contextlib.ExitStack()
try:
setattr(self.target, self.attribute, new_attr) setattr(self.target, self.attribute, new_attr)
if self.attribute_name is not None: if self.attribute_name is not None:
extra_args = {} extra_args = {}
if self.new is DEFAULT: if self.new is DEFAULT:
extra_args[self.attribute_name] = new extra_args[self.attribute_name] = new
for patching in self.additional_patchers: for patching in self.additional_patchers:
arg = patching.__enter__() arg = self._exit_stack.enter_context(patching)
if patching.new is DEFAULT: if patching.new is DEFAULT:
extra_args.update(arg) extra_args.update(arg)
return extra_args return extra_args
return new return new
except:
if not self.__exit__(*sys.exc_info()):
raise
def __exit__(self, *exc_info): def __exit__(self, *exc_info):
"""Undo the patch.""" """Undo the patch."""
if not _is_started(self):
return
if self.is_local and self.temp_original is not DEFAULT: if self.is_local and self.temp_original is not DEFAULT:
setattr(self.target, self.attribute, self.temp_original) setattr(self.target, self.attribute, self.temp_original)
else: else:
@ -1553,9 +1531,9 @@ class _patch(object):
del self.temp_original del self.temp_original
del self.is_local del self.is_local
del self.target del self.target
for patcher in reversed(self.additional_patchers): exit_stack = self._exit_stack
if _is_started(patcher): del self._exit_stack
patcher.__exit__(*exc_info) return exit_stack.__exit__(*exc_info)
def start(self): def start(self):
@ -1571,9 +1549,9 @@ class _patch(object):
self._active_patches.remove(self) self._active_patches.remove(self)
except ValueError: except ValueError:
# If the patch hasn't been started this will fail # If the patch hasn't been started this will fail
pass return None
return self.__exit__() return self.__exit__(None, None, None)
@ -1873,9 +1851,9 @@ class _patch_dict(object):
_patch._active_patches.remove(self) _patch._active_patches.remove(self)
except ValueError: except ValueError:
# If the patch hasn't been started this will fail # If the patch hasn't been started this will fail
pass return None
return self.__exit__() return self.__exit__(None, None, None)
def _clear_dict(in_dict): def _clear_dict(in_dict):

View File

@ -774,7 +774,7 @@ class PatchTest(unittest.TestCase):
d = {'foo': 'bar'} d = {'foo': 'bar'}
original = d.copy() original = d.copy()
patcher = patch.dict(d, [('spam', 'eggs')], clear=True) patcher = patch.dict(d, [('spam', 'eggs')], clear=True)
self.assertEqual(patcher.stop(), False) self.assertFalse(patcher.stop())
self.assertEqual(d, original) self.assertEqual(d, original)

View File

@ -0,0 +1,3 @@
Fixed reverting multiple patches in unittest.mock. Patcher's ``__exit__()``
is now never called if its ``__enter__()`` is failed. Returning true from
``__exit__()`` silences now the exception.