Bug report
Checklist
CPython versions tested on:
3.8, 3.9, 3.10, 3.11, 3.12, CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.11.5 (main, Aug 28 2023, 16:29:45) [GCC 13.2.1 20230801]
A clear and concise description of the bug:
if an exception is active inside a coroutine/generator (coro1/agen1) and another exception is .thrown into its await coro2/yield from agen2, then coro2/agen2 loses the original exc_info.
-
a generator example:
import sys
def outer():
try:
raise RuntimeError("boom")
except RuntimeError: # or `__exit__`/`finally`
print(sys.exc_info())
yield from inner()
# note: if you inline `inner()` here, things work fine.
print(sys.exc_info())
def inner():
print(sys.exc_info())
try:
yield
except ValueError:
pass
print(sys.exc_info())
gen = outer()
gen.send(None)
try:
gen.throw(ValueError)
except StopIteration:
pass
output:
(<class 'RuntimeError'>, RuntimeError('boom'), <traceback object at 0x7fdb6b313840>)
(<class 'RuntimeError'>, RuntimeError('boom'), <traceback object at 0x7fdb6b313840>)
(None, None, None)
(<class 'RuntimeError'>, RuntimeError('boom'), <traceback object at 0x7fdb6b313840>)
expected output:
same exc_info all four times.
-
two coroutine examples (using asyncio in particular, with asyncio calling throw):
import asyncio
import sys
from asyncio import CancelledError
from math import inf
async def outer():
try:
raise RuntimeError("boom")
except RuntimeError: # or `__aexit__`/`finally`
print(sys.exc_info())
await clean_up_and_log()
# note: if you inline `clean_up_and_log()` here, things work fine.
print(sys.exc_info())
async def clean_up_and_log():
print(sys.exc_info())
# attempt to do some clean up that we are okay skipping part of if we get cancelled
# during it or if we already have a cancellation request.
#
# (to reproduce, suppose that we get a cancellation request before the optional
# clean up step.)
asyncio.current_task().cancel()
try:
await asyncio.sleep(0)
except CancelledError:
if sys.version_info >= (3, 11):
asyncio.current_task().uncancel()
# after the optional async clean up, log the exception.
print(sys.exc_info())
async def clean_up_and_log(): # this example is >= 3.11 only.
print(sys.exc_info())
# attempt to do some clean up that we want to skip if it takes too long (e.g. due to
# a non-responsive peer) or if we get cancelled during it or if we already have a
# cancellation request.
try:
async with asyncio.timeout(0.1):
await asyncio.sleep(inf)
except TimeoutError:
pass
except CancelledError:
asyncio.current_task().uncancel()
# after the optional async clean up, log the exception.
print(sys.exc_info())
asyncio.run(outer())
output and expected output: same as the generator example.
this problem is a special case of GH-73773 that was not discussed or fixed as part of that issue1.
for comparison, PyPy behaves the way I expect here.
Bug report
Checklist
and am confident this bug has not been reported before
CPython versions tested on:
3.8, 3.9, 3.10, 3.11, 3.12, CPython main branch
Operating systems tested on:
Linux
Output from running 'python -VV' on the command line:
Python 3.11.5 (main, Aug 28 2023, 16:29:45) [GCC 13.2.1 20230801]
A clear and concise description of the bug:
if an exception is active inside a coroutine/generator (
coro1/agen1) and another exception is.thrown into itsawait coro2/yield from agen2, thencoro2/agen2loses the originalexc_info.a generator example:
output:
expected output:
same
exc_infoall four times.two coroutine examples (using asyncio in particular, with asyncio calling
throw):output and expected output: same as the generator example.
this problem is a special case of GH-73773 that was not discussed or fixed as part of that issue1.
for comparison, PyPy behaves the way I expect here.
Footnotes
I am not sure whether it would be preferred that I necrobump the old closed issue or file a new report here. ↩