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

(cherry picked from commit b3722ca058)

Co-authored-by: Thomas Grainger <tagrain@gmail.com>
This commit is contained in:
Miss Islington (bot) 2023-01-03 08:18:45 -08:00 committed by GitHub
parent b99ac1dbc0
commit 861cdefc1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 4 deletions

View File

@ -173,7 +173,7 @@ def __exit__(self, typ, value, traceback):
isinstance(value, StopIteration) isinstance(value, StopIteration)
and exc.__cause__ is value and exc.__cause__ is value
): ):
exc.__traceback__ = traceback value.__traceback__ = traceback
return False return False
raise raise
except BaseException as exc: except BaseException as exc:
@ -228,6 +228,7 @@ async def __aexit__(self, typ, value, traceback):
except RuntimeError as exc: except RuntimeError as exc:
# Don't re-raise the passed in exception. (issue27122) # Don't re-raise the passed in exception. (issue27122)
if exc is value: if exc is value:
exc.__traceback__ = traceback
return False return False
# Avoid suppressing if a Stop(Async)Iteration exception # Avoid suppressing if a Stop(Async)Iteration exception
# was passed to athrow() and later wrapped into a RuntimeError # 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)) isinstance(value, (StopIteration, StopAsyncIteration))
and exc.__cause__ is value and exc.__cause__ is value
): ):
value.__traceback__ = traceback
return False return False
raise raise
except BaseException as exc: except BaseException as exc:
@ -250,6 +252,7 @@ async def __aexit__(self, typ, value, traceback):
# and the __exit__() protocol. # and the __exit__() protocol.
if exc is not value: if exc is not value:
raise raise
exc.__traceback__ = traceback
return False return False
raise RuntimeError("generator didn't stop after athrow()") raise RuntimeError("generator didn't stop after athrow()")

View File

@ -104,15 +104,39 @@ def f():
self.assertEqual(frames[0].line, '1/0') self.assertEqual(frames[0].line, '1/0')
# Repeat with RuntimeError (which goes through a different code path) # Repeat with RuntimeError (which goes through a different code path)
class RuntimeErrorSubclass(RuntimeError):
pass
try: try:
with f(): with f():
raise NotImplementedError(42) raise RuntimeErrorSubclass(42)
except NotImplementedError as e: except RuntimeErrorSubclass as e:
frames = traceback.extract_tb(e.__traceback__) frames = traceback.extract_tb(e.__traceback__)
self.assertEqual(len(frames), 1) self.assertEqual(len(frames), 1)
self.assertEqual(frames[0].name, 'test_contextmanager_traceback') 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): def test_contextmanager_no_reraise(self):
@contextmanager @contextmanager

View File

@ -5,6 +5,7 @@
import functools import functools
from test import support from test import support
import unittest import unittest
import traceback
from test.test_contextlib import TestBaseExitStack from test.test_contextlib import TestBaseExitStack
@ -125,6 +126,62 @@ async def woohoo():
raise ZeroDivisionError() raise ZeroDivisionError()
self.assertEqual(state, [1, 42, 999]) 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_test
async def test_contextmanager_no_reraise(self): async def test_contextmanager_no_reraise(self):
@asynccontextmanager @asynccontextmanager

View File

@ -641,6 +641,7 @@ Hans de Graaff
Tim Graham Tim Graham
Kim Gräsman Kim Gräsman
Alex Grönholm Alex Grönholm
Thomas Grainger
Nathaniel Gray Nathaniel Gray
Eddy De Greef Eddy De Greef
Duane Griffin Duane Griffin

View File

@ -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`.