Skip to content

Commit f878aeb

Browse files
committed
gh-135736: Report suppressed sibling errors for all base errors, not just GeneratorExit
Per review feedback from kumaraditya303: the exception-handler reporting of suppressed sibling-task errors was previously gated on isinstance(self._base_error, GeneratorExit), but any base error (SystemExit, KeyboardInterrupt, etc.) causes the same silent discarding of collected task errors. Broaden the reporting to run whenever self._base_error is set, and add a regression test covering a non-GeneratorExit base error.
1 parent 63f3675 commit f878aeb

3 files changed

Lines changed: 48 additions & 17 deletions

File tree

Lib/asyncio/taskgroups.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -140,21 +140,18 @@ async def _aexit(self, et, exc):
140140
assert not self._tasks
141141

142142
if self._base_error is not None:
143-
if isinstance(self._base_error, GeneratorExit):
144-
# Unlike SystemExit/KeyboardInterrupt, GeneratorExit can
145-
# only reach here via the "async with" body itself being
146-
# closed (e.g. an enclosing async generator's aclose()),
147-
# not via a child task, so any collected task errors are
148-
# about to be discarded silently. Report them instead of
149-
# losing them. See gh-135736.
150-
for suppressed_exc in self._errors:
151-
self._loop.call_exception_handler({
152-
'message': 'TaskGroup task exception was not '
153-
'propagated because the TaskGroup body '
154-
'is being closed via GeneratorExit',
155-
'exception': suppressed_exc,
156-
'task_group': self,
157-
})
143+
# self._base_error (e.g. SystemExit, KeyboardInterrupt, or
144+
# GeneratorExit) is about to propagate out of this method,
145+
# which discards any other collected task errors silently.
146+
# Report them instead of losing them. See gh-135736.
147+
for suppressed_exc in self._errors:
148+
self._loop.call_exception_handler({
149+
'message': 'TaskGroup task exception was not '
150+
'propagated because the TaskGroup body '
151+
'is being closed with a BaseException',
152+
'exception': suppressed_exc,
153+
'task_group': self,
154+
})
158155
try:
159156
raise self._base_error
160157
finally:

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,37 @@ async def runner():
689689

690690
self.assertEqual(get_error_types(cm.exception), {GeneratorExit})
691691

692+
async def test_taskgroup_20f(self):
693+
# See gh-135736: a *non*-GeneratorExit base error (here MyBaseExc,
694+
# standing in for SystemExit/KeyboardInterrupt) must also report a
695+
# sibling task's suppressed exception via the loop's exception
696+
# handler, the same as the GeneratorExit case in
697+
# test_taskgroup_20c, instead of discarding it silently.
698+
async def crash_soon():
699+
await asyncio.sleep(0.1)
700+
1 / 0
701+
702+
async def nested():
703+
try:
704+
await asyncio.sleep(10)
705+
finally:
706+
raise MyBaseExc
707+
708+
async def runner():
709+
async with taskgroups.TaskGroup() as g:
710+
g.create_task(crash_soon())
711+
await nested()
712+
713+
contexts = []
714+
loop = asyncio.get_running_loop()
715+
loop.set_exception_handler(lambda loop, context: contexts.append(context))
716+
717+
with self.assertRaises(MyBaseExc):
718+
await runner()
719+
720+
self.assertEqual(len(contexts), 1)
721+
self.assertIsInstance(contexts[0]['exception'], ZeroDivisionError)
722+
692723
async def _test_taskgroup_21(self):
693724
# This test doesn't work as asyncio, currently, doesn't
694725
# correctly propagate KeyboardInterrupt (or SystemExit) --
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
Fix :class:`asyncio.TaskGroup` raising a spurious :exc:`ExceptionGroup` instead
22
of propagating :exc:`GeneratorExit` directly when the ``async with`` block is
33
exited via an enclosing async generator's :meth:`~agen.aclose`. Errors from
4-
sibling tasks that would otherwise be silently discarded in this case are now
5-
reported via :meth:`loop.call_exception_handler() <asyncio.loop.call_exception_handler>`.
4+
sibling tasks that would otherwise be silently discarded whenever the
5+
``async with`` block exits with *any* :ref:`base exception <bltin-exceptions>`
6+
(:exc:`GeneratorExit`, :exc:`KeyboardInterrupt`, :exc:`SystemExit`, etc.), not
7+
just :exc:`GeneratorExit`, are now reported via
8+
:meth:`loop.call_exception_handler() <asyncio.loop.call_exception_handler>`.

0 commit comments

Comments
 (0)