Skip to content
Open
12 changes: 12 additions & 0 deletions Lib/asyncio/taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@ async def _aexit(self, et, exc):
assert not self._tasks

if self._base_error is not None:
# self._base_error (SystemExit or KeyboardInterrupt) is about
# to propagate out of this method, which discards any other
# collected task errors silently. Report them instead of
# losing them. See gh-135736.
for suppressed_exc in self._errors:
self._loop.call_exception_handler({
'message': 'TaskGroup task exception was not '
'propagated because the TaskGroup body '
'is being closed with a BaseException',
'exception': suppressed_exc,
'task_group': self,
})
try:
raise self._base_error
finally:
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_asyncio/test_taskgroups.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,39 @@ async def runner():
get_error_types(cm.exception), {MyBaseExc, ZeroDivisionError}
)

async def test_taskgroup_20b(self):
# Same setup as test_taskgroup_20 (a KeyboardInterrupt from the
# "async with" body itself, alongside a sibling task's exception):
# raising self._base_error out of _aexit() discards self._errors
# silently. The sibling's exception can't be raised alongside
# the KeyboardInterrupt (only one exception can propagate), but
# it must be reported via the loop's exception handler instead
# of being discarded silently. See gh-135736.
async def crash_soon():
await asyncio.sleep(0.1)
1 / 0

async def nested():
try:
await asyncio.sleep(10)
finally:
raise KeyboardInterrupt

async def runner():
async with taskgroups.TaskGroup() as g:
g.create_task(crash_soon())
await nested()

contexts = []
loop = asyncio.get_running_loop()
loop.set_exception_handler(lambda loop, context: contexts.append(context))

with self.assertRaises(KeyboardInterrupt):
await runner()

self.assertEqual(len(contexts), 1)
self.assertIsInstance(contexts[0]['exception'], ZeroDivisionError)

async def _test_taskgroup_21(self):
# This test doesn't work as asyncio, currently, doesn't
# correctly propagate KeyboardInterrupt (or SystemExit) --
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix :class:`asyncio.TaskGroup` silently discarding errors from sibling
tasks whenever the ``async with`` block exits with a :exc:`SystemExit` or
:exc:`KeyboardInterrupt`. These errors are now reported via
:meth:`loop.call_exception_handler() <asyncio.loop.call_exception_handler>`
instead of being lost.
Loading