Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Lib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -239,6 +240,7 @@ async def __aexit__(self, typ, value, traceback):
isinstance(value, (StopIteration, StopAsyncIteration))
and exc.__cause__ is value
):
exc.__traceback__ = traceback
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs a test for this case

Copy link
Contributor Author

@graingert graingert Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iritkatriel it doesn't look like this line does anything. If I delete it I get the same behavior, same with L176:

cpython/Lib/contextlib.py

Lines 166 to 177 in adf2bf3

# Avoid suppressing if a StopIteration exception
# was passed to throw() and later wrapped into a RuntimeError
# (see PEP 479 for sync generators; async generators also
# have this behavior). But do this only if the exception wrapped
# by the RuntimeError is actually Stop(Async)Iteration (see
# issue29692).
if (
isinstance(value, StopIteration)
and exc.__cause__ is value
):
exc.__traceback__ = traceback
return False

return False
raise
except BaseException as exc:
Expand All @@ -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()")

Expand Down
32 changes: 29 additions & 3 deletions Lib/test/test_contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,15 +104,41 @@ 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), 2)
self.assertEqual(frames[0].name, 'f')
self.assertEqual(frames[0].line, 'yield')
Copy link
Contributor Author

@graingert graingert Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@iritkatriel do you expect to see this yield here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does 3.10 do?

Copy link
Contributor Author

@graingert graingert Aug 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the yield is new, incorrect, behavior in 3.11:

import contextlib
import logging

logger = logging.getLogger("__name__")

@contextlib.contextmanager
def f():
    yield


try:
    with f():
        raise StopIteration
except StopIteration:
    logger.exception("stop iteration")



print("================ overwriting StopIteration via globals!==============")


class StopIteration(Exception):
    pass


try:
    with f():
        raise StopIteration
except StopIteration:
    logger.exception("stop iteration")
 graingert@conscientious  testing311  ~/projects/trio   master ± python3.10 demo.py
stop iteration
Traceback (most recent call last):
  File "/home/graingert/projects/trio/demo.py", line 13, in <module>
    raise StopIteration
StopIteration
================ overwriting StopIteration via globals!==============
stop iteration
Traceback (most recent call last):
  File "/home/graingert/projects/trio/demo.py", line 28, in <module>
    raise StopIteration
StopIteration

Notice the extra yield in the first run before overwriting StopIteration via globals happens:

 graingert@conscientious  testing311  ~/projects/trio   master ± python3.11 demo.py 
stop iteration
Traceback (most recent call last):
  File "/home/graingert/projects/trio/demo.py", line 8, in f
    yield
  File "/home/graingert/projects/trio/demo.py", line 13, in <module>
    raise StopIteration
StopIteration
================ overwriting StopIteration via globals!==============
stop iteration
Traceback (most recent call last):
  File "/home/graingert/projects/trio/demo.py", line 28, in <module>
    raise StopIteration
StopIteration

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed it!

self.assertEqual(frames[1].name, 'test_contextmanager_traceback')
self.assertEqual(frames[1].line, 'raise stop_exc')

def test_contextmanager_no_reraise(self):
@contextmanager
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_contextlib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import functools
from test import support
import unittest
import traceback

from test.test_contextlib import TestBaseExitStack

Expand Down Expand Up @@ -125,6 +126,64 @@ 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), 2)
self.assertEqual(frames[0].name, 'f')
self.assertEqual(frames[0].line, 'yield')
self.assertEqual(frames[1].name, 'test_contextmanager_traceback')
self.assertEqual(frames[1].line, 'raise stop_exc')

@_async_test
async def test_contextmanager_no_reraise(self):
@asynccontextmanager
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a 3.11 regression in :func:`~contextlib.asynccontextmanager`, which caused it to propagate exceptions with incorrect tracebacks.