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:
parent
cd8295ff75
commit
4b222c9491
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue