Skip to content

Commit

Permalink
[3.8] bpo-40126: Fix reverting multiple patches in unittest.mock. (py…
Browse files Browse the repository at this point in the history
…thonGH-19351)

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 <storchaka@gmail.com>
  • Loading branch information
serhiy-storchaka committed Apr 12, 2020
1 parent 2714c90 commit 2584468
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 46 deletions.
70 changes: 24 additions & 46 deletions Lib/unittest/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,11 +1229,6 @@ def _importer(target):
return thing


def _is_started(patcher):
# XXXX horrible
return hasattr(patcher, 'is_local')


class _patch(object):

attribute_name = None
Expand Down Expand Up @@ -1304,34 +1299,16 @@ def decorate_class(self, klass):
@contextlib.contextmanager
def decoration_helper(self, patched, args, keywargs):
extra_args = []
entered_patchers = []
patching = None

exc_info = tuple()
try:
with contextlib.ExitStack() as exit_stack:
for patching in patched.patchings:
arg = patching.__enter__()
entered_patchers.append(patching)
arg = exit_stack.enter_context(patching)
if patching.attribute_name is not None:
keywargs.update(arg)
elif patching.new is DEFAULT:
extra_args.append(arg)

args += tuple(extra_args)
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):
Expand Down Expand Up @@ -1508,25 +1485,26 @@ def __enter__(self):

self.temp_original = original
self.is_local = local
setattr(self.target, self.attribute, new_attr)
if self.attribute_name is not None:
extra_args = {}
if self.new is DEFAULT:
extra_args[self.attribute_name] = new
for patching in self.additional_patchers:
arg = patching.__enter__()
if patching.new is DEFAULT:
extra_args.update(arg)
return extra_args

return new

self._exit_stack = contextlib.ExitStack()
try:
setattr(self.target, self.attribute, new_attr)
if self.attribute_name is not None:
extra_args = {}
if self.new is DEFAULT:
extra_args[self.attribute_name] = new
for patching in self.additional_patchers:
arg = self._exit_stack.enter_context(patching)
if patching.new is DEFAULT:
extra_args.update(arg)
return extra_args

return new
except:
if not self.__exit__(*sys.exc_info()):
raise

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

if self.is_local and self.temp_original is not DEFAULT:
setattr(self.target, self.attribute, self.temp_original)
else:
Expand All @@ -1541,9 +1519,9 @@ def __exit__(self, *exc_info):
del self.temp_original
del self.is_local
del self.target
for patcher in reversed(self.additional_patchers):
if _is_started(patcher):
patcher.__exit__(*exc_info)
exit_stack = self._exit_stack
del self._exit_stack
return exit_stack.__exit__(*exc_info)


def start(self):
Expand All @@ -1559,9 +1537,9 @@ def stop(self):
self._active_patches.remove(self)
except ValueError:
# If the patch hasn't been started this will fail
pass
return None

return self.__exit__()
return self.__exit__(None, None, None)



Expand Down
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 2584468

Please sign in to comment.