[Python-checkins] gh-95882: fix regression in the traceback of exceptions propagated from inside a contextlib context manager (GH-95883)

miss-islington webhook-mailer at python.org
Tue Jan 3 11:18:52 EST 2023


https://github.com/python/cpython/commit/861cdefc1bccd419bd268a9f60b34bffbaff9ea2
commit: 861cdefc1bccd419bd268a9f60b34bffbaff9ea2
branch: 3.11
author: Miss Islington (bot) <31488909+miss-islington at users.noreply.github.com>
committer: miss-islington <31488909+miss-islington at users.noreply.github.com>
date: 2023-01-03T08:18:45-08:00
summary:

gh-95882: fix regression in the traceback of exceptions propagated from inside a contextlib context manager (GH-95883)

(cherry picked from commit b3722ca058f6a6d6505cf2ea9ffabaf7fb6b6e19)

Co-authored-by: Thomas Grainger <tagrain at gmail.com>

files:
A Misc/NEWS.d/next/Library/2022-08-11-10-02-19.gh-issue-95882.FsUr72.rst
M Lib/contextlib.py
M Lib/test/test_contextlib.py
M Lib/test/test_contextlib_async.py
M Misc/ACKS

diff --git a/Lib/contextlib.py b/Lib/contextlib.py
index 625bb33b12d5..58e9a498878d 100644
--- a/Lib/contextlib.py
+++ b/Lib/contextlib.py
@@ -173,7 +173,7 @@ def __exit__(self, typ, value, traceback):
                     isinstance(value, StopIteration)
                     and exc.__cause__ is value
                 ):
-                    exc.__traceback__ = traceback
+                    value.__traceback__ = traceback
                     return False
                 raise
             except BaseException as exc:
@@ -228,6 +228,7 @@ async def __aexit__(self, typ, value, traceback):
             except RuntimeError as exc:
                 # Don't re-raise the passed in exception. (issue27122)
                 if exc is value:
+                    exc.__traceback__ = traceback
                     return False
                 # Avoid suppressing if a Stop(Async)Iteration exception
                 # was passed to athrow() and later wrapped into a RuntimeError
@@ -239,6 +240,7 @@ async def __aexit__(self, typ, value, traceback):
                     isinstance(value, (StopIteration, StopAsyncIteration))
                     and exc.__cause__ is value
                 ):
+                    value.__traceback__ = traceback
                     return False
                 raise
             except BaseException as exc:
@@ -250,6 +252,7 @@ async def __aexit__(self, typ, value, traceback):
                 # and the __exit__() protocol.
                 if exc is not value:
                     raise
+                exc.__traceback__ = traceback
                 return False
             raise RuntimeError("generator didn't stop after athrow()")
 
diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py
index 31f5c74572b6..ec06785b5667 100644
--- a/Lib/test/test_contextlib.py
+++ b/Lib/test/test_contextlib.py
@@ -104,15 +104,39 @@ def f():
         self.assertEqual(frames[0].line, '1/0')
 
         # Repeat with RuntimeError (which goes through a different code path)
+        class RuntimeErrorSubclass(RuntimeError):
+            pass
+
         try:
             with f():
-                raise NotImplementedError(42)
-        except NotImplementedError as e:
+                raise RuntimeErrorSubclass(42)
+        except RuntimeErrorSubclass as e:
             frames = traceback.extract_tb(e.__traceback__)
 
         self.assertEqual(len(frames), 1)
         self.assertEqual(frames[0].name, 'test_contextmanager_traceback')
-        self.assertEqual(frames[0].line, 'raise NotImplementedError(42)')
+        self.assertEqual(frames[0].line, 'raise RuntimeErrorSubclass(42)')
+
+        class StopIterationSubclass(StopIteration):
+            pass
+
+        for stop_exc in (
+            StopIteration('spam'),
+            StopIterationSubclass('spam'),
+        ):
+            with self.subTest(type=type(stop_exc)):
+                try:
+                    with f():
+                        raise stop_exc
+                except type(stop_exc) as e:
+                    self.assertIs(e, stop_exc)
+                    frames = traceback.extract_tb(e.__traceback__)
+                else:
+                    self.fail(f'{stop_exc} was suppressed')
+
+                self.assertEqual(len(frames), 1)
+                self.assertEqual(frames[0].name, 'test_contextmanager_traceback')
+                self.assertEqual(frames[0].line, 'raise stop_exc')
 
     def test_contextmanager_no_reraise(self):
         @contextmanager
diff --git a/Lib/test/test_contextlib_async.py b/Lib/test/test_contextlib_async.py
index b64673d2c31e..3d43ed0fcab1 100644
--- a/Lib/test/test_contextlib_async.py
+++ b/Lib/test/test_contextlib_async.py
@@ -5,6 +5,7 @@
 import functools
 from test import support
 import unittest
+import traceback
 
 from test.test_contextlib import TestBaseExitStack
 
@@ -125,6 +126,62 @@ async def woohoo():
                 raise ZeroDivisionError()
         self.assertEqual(state, [1, 42, 999])
 
+    @_async_test
+    async def test_contextmanager_traceback(self):
+        @asynccontextmanager
+        async def f():
+            yield
+
+        try:
+            async with f():
+                1/0
+        except ZeroDivisionError as e:
+            frames = traceback.extract_tb(e.__traceback__)
+
+        self.assertEqual(len(frames), 1)
+        self.assertEqual(frames[0].name, 'test_contextmanager_traceback')
+        self.assertEqual(frames[0].line, '1/0')
+
+        # Repeat with RuntimeError (which goes through a different code path)
+        class RuntimeErrorSubclass(RuntimeError):
+            pass
+
+        try:
+            async with f():
+                raise RuntimeErrorSubclass(42)
+        except RuntimeErrorSubclass as e:
+            frames = traceback.extract_tb(e.__traceback__)
+
+        self.assertEqual(len(frames), 1)
+        self.assertEqual(frames[0].name, 'test_contextmanager_traceback')
+        self.assertEqual(frames[0].line, 'raise RuntimeErrorSubclass(42)')
+
+        class StopIterationSubclass(StopIteration):
+            pass
+
+        class StopAsyncIterationSubclass(StopAsyncIteration):
+            pass
+
+        for stop_exc in (
+            StopIteration('spam'),
+            StopAsyncIteration('ham'),
+            StopIterationSubclass('spam'),
+            StopAsyncIterationSubclass('spam')
+        ):
+            with self.subTest(type=type(stop_exc)):
+                try:
+                    async with f():
+                        raise stop_exc
+                except type(stop_exc) as e:
+                    self.assertIs(e, stop_exc)
+                    frames = traceback.extract_tb(e.__traceback__)
+                else:
+                    self.fail(f'{stop_exc} was suppressed')
+
+                self.assertEqual(len(frames), 1)
+                self.assertEqual(frames[0].name, 'test_contextmanager_traceback')
+                self.assertEqual(frames[0].line, 'raise stop_exc')
+
     @_async_test
     async def test_contextmanager_no_reraise(self):
         @asynccontextmanager
diff --git a/Misc/ACKS b/Misc/ACKS
index 58b29c19400e..dc47e894b764 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -641,6 +641,7 @@ Hans de Graaff
 Tim Graham
 Kim Gräsman
 Alex Grönholm
+Thomas Grainger
 Nathaniel Gray
 Eddy De Greef
 Duane Griffin
diff --git a/Misc/NEWS.d/next/Library/2022-08-11-10-02-19.gh-issue-95882.FsUr72.rst b/Misc/NEWS.d/next/Library/2022-08-11-10-02-19.gh-issue-95882.FsUr72.rst
new file mode 100644
index 000000000000..9cdb237d5c87
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-08-11-10-02-19.gh-issue-95882.FsUr72.rst
@@ -0,0 +1 @@
+Fix a 3.11 regression in :func:`~contextlib.asynccontextmanager`, which caused it to propagate exceptions with incorrect tracebacks and fix a 3.11 regression in  :func:`~contextlib.contextmanager`, which caused it to propagate exceptions with incorrect tracebacks for :exc:`StopIteration`.



More information about the Python-checkins mailing list