From: Serhiy Storchaka Date: Sun, 12 Apr 2020 11:54:03 +0000 (+0300) Subject: [3.7] bpo-40126: Fix reverting multiple patches in unittest.mock. (GH-19351) (GH... X-Git-Tag: v3.7.8rc1~98 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4057e8f9b56789223a1e691d7601003aceb84ad1;p=thirdparty%2FPython%2Fcpython.git [3.7] bpo-40126: Fix reverting multiple patches in unittest.mock. (GH-19351) (GH-19484) Patcher's __exit__() is now never called if its __enter__() is failed. Returning true from __exit__() silences now the exception. (cherry picked from commit 4b222c9491d1700e9bdd98e6889b8d0ea1c7321e) Co-authored-by: Serhiy Storchaka --- diff --git a/Lib/unittest/mock.py b/Lib/unittest/mock.py index 87e8735e958d..43fb00feac46 100644 --- a/Lib/unittest/mock.py +++ b/Lib/unittest/mock.py @@ -30,6 +30,7 @@ import inspect import pprint import sys import builtins +import contextlib from types import ModuleType, MethodType from functools import wraps, partial @@ -1243,13 +1244,9 @@ class _patch(object): @wraps(func) def patched(*args, **keywargs): extra_args = [] - entered_patchers = [] - - 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: @@ -1257,19 +1254,6 @@ class _patch(object): args += tuple(extra_args) return func(*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) patched.patchings = [self] return patched @@ -1411,19 +1395,23 @@ class _patch(object): 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.""" @@ -1444,9 +1432,9 @@ class _patch(object): 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): @@ -1464,7 +1452,7 @@ class _patch(object): # If the patch hasn't been started this will fail pass - return self.__exit__() + return self.__exit__(None, None, None) diff --git a/Misc/NEWS.d/next/Library/2020-04-04-00-47-40.bpo-40126.Y-bTNP.rst b/Misc/NEWS.d/next/Library/2020-04-04-00-47-40.bpo-40126.Y-bTNP.rst new file mode 100644 index 000000000000..8f725cfba86e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-04-04-00-47-40.bpo-40126.Y-bTNP.rst @@ -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.