]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.8] bpo-40126: Fix reverting multiple patches in unittest.mock. (GH-19351) (GH...
authorSerhiy Storchaka <storchaka@gmail.com>
Sun, 12 Apr 2020 11:53:46 +0000 (14:53 +0300)
committerGitHub <noreply@github.com>
Sun, 12 Apr 2020 11:53:46 +0000 (14:53 +0300)
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 <storchaka@gmail.com>
Lib/unittest/mock.py
Misc/NEWS.d/next/Library/2020-04-04-00-47-40.bpo-40126.Y-bTNP.rst [new file with mode: 0644]

index a8f74a95791c1f881c98d026406887231f443b14..3629cf61098f63b7d8a22793325dd30c70d0c969 100644 (file)
@@ -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
@@ -1304,14 +1299,9 @@ class _patch(object):
     @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:
@@ -1319,19 +1309,6 @@ class _patch(object):
 
             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):
@@ -1508,25 +1485,26 @@ 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."""
-        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:
@@ -1541,9 +1519,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):
@@ -1559,9 +1537,9 @@ class _patch(object):
             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)
 
 
 
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 (file)
index 0000000..8f725cf
--- /dev/null
@@ -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.