Skip to content

Commit ec2d1e2

Browse files
committed
gh-135736: Restrict fix to reporting suppressed sibling errors
Per review feedback from kumaraditya303: an alternate fix for the GeneratorExit-wrapped-in-BaseExceptionGroup bug was merged separately (gh-154649), so drop the now-redundant GeneratorExit handling from _is_base_error()/_base_error here. Keep only the independent part: reporting sibling task errors via loop.call_exception_handler() when they would otherwise be silently discarded because the "async with" body exited with a SystemExit or KeyboardInterrupt.
1 parent dfc7f1d commit ec2d1e2

3 files changed

Lines changed: 16 additions & 99 deletions

File tree

Lib/asyncio/taskgroups.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ async def _aexit(self, et, exc):
8787
self._exiting = True
8888

8989
if (exc is not None and
90-
(self._is_base_error(exc) or isinstance(exc, GeneratorExit)) and
90+
self._is_base_error(exc) and
9191
self._base_error is None):
9292
self._base_error = exc
9393

@@ -140,10 +140,10 @@ async def _aexit(self, et, exc):
140140
assert not self._tasks
141141

142142
if self._base_error is not None:
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.
143+
# self._base_error (SystemExit or KeyboardInterrupt) is about
144+
# to propagate out of this method, which discards any other
145+
# collected task errors silently. Report them instead of
146+
# losing them. See gh-135736.
147147
for suppressed_exc in self._errors:
148148
self._loop.call_exception_handler({
149149
'message': 'TaskGroup task exception was not '

Lib/test/test_asyncio/test_taskgroups.py

Lines changed: 6 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -609,93 +609,13 @@ async def runner():
609609
)
610610

611611
async def test_taskgroup_20b(self):
612-
# See gh-135736: a GeneratorExit raised by the "async with" body
613-
# itself (e.g. propagating out of an enclosing async generator's
614-
# aclose()) must propagate directly, not be wrapped in an
615-
# ExceptionGroup, since callers of GeneratorExit-closing APIs
616-
# only know how to handle GeneratorExit/StopAsyncIteration.
617-
async def nested():
618-
try:
619-
await asyncio.sleep(10)
620-
finally:
621-
raise GeneratorExit
622-
623-
async def runner():
624-
async with taskgroups.TaskGroup() as g:
625-
await nested()
626-
627-
with self.assertRaises(GeneratorExit):
628-
await runner()
629-
630-
async def test_taskgroup_20c(self):
631-
# Same as test_taskgroup_20b, but with a sibling task that also
632-
# fails. The sibling's exception can't be raised (only one
633-
# exception can propagate through GeneratorExit-closing APIs),
634-
# but it must be reported via the loop's exception handler
635-
# instead of silently discarded.
636-
async def crash_soon():
637-
await asyncio.sleep(0.1)
638-
1 / 0
639-
640-
async def nested():
641-
try:
642-
await asyncio.sleep(10)
643-
finally:
644-
raise GeneratorExit
645-
646-
async def runner():
647-
async with taskgroups.TaskGroup() as g:
648-
g.create_task(crash_soon())
649-
await nested()
650-
651-
contexts = []
652-
loop = asyncio.get_running_loop()
653-
loop.set_exception_handler(lambda loop, context: contexts.append(context))
654-
655-
with self.assertRaises(GeneratorExit):
656-
await runner()
657-
658-
self.assertEqual(len(contexts), 1)
659-
self.assertIsInstance(contexts[0]['exception'], ZeroDivisionError)
660-
661-
async def test_taskgroup_20d(self):
662-
# Reproduces the original report in gh-135736: closing an async
663-
# generator whose body awaits inside a TaskGroup must not raise
664-
# an ExceptionGroup out of aclose().
665-
async def genfn():
666-
async with taskgroups.TaskGroup():
667-
yield 1
668-
669-
g = genfn()
670-
await g.asend(None)
671-
await g.aclose()
672-
673-
async def test_taskgroup_20e(self):
674-
# Unlike a GeneratorExit raised by the "async with" body itself
675-
# (test_taskgroup_20b), a GeneratorExit raised by a *task* inside
676-
# the group is not a "close this generator" signal and continues
677-
# to be wrapped in an ExceptionGroup like any other exception.
678-
async def crash_soon():
679-
await asyncio.sleep(0.1)
680-
raise GeneratorExit
681-
682-
async def runner():
683-
async with taskgroups.TaskGroup() as g:
684-
g.create_task(crash_soon())
685-
await asyncio.sleep(10)
686-
687-
with self.assertRaises(BaseExceptionGroup) as cm:
688-
await runner()
689-
690-
self.assertEqual(get_error_types(cm.exception), {GeneratorExit})
691-
692-
async def test_taskgroup_20f(self):
693612
# Same setup as test_taskgroup_20 (a KeyboardInterrupt from the
694-
# "async with" body itself, alongside a sibling task's exception),
695-
# but confirms the gh-135736 fix also applies to non-GeneratorExit
696-
# base errors: the sibling's exception must be reported via the
697-
# loop's exception handler, the same as test_taskgroup_20c, instead
698-
# of being discarded silently.
613+
# "async with" body itself, alongside a sibling task's exception):
614+
# raising self._base_error out of _aexit() discards self._errors
615+
# silently. The sibling's exception can't be raised alongside
616+
# the KeyboardInterrupt (only one exception can propagate), but
617+
# it must be reported via the loop's exception handler instead
618+
# of being discarded silently. See gh-135736.
699619
async def crash_soon():
700620
await asyncio.sleep(0.1)
701621
1 / 0
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
Fix :class:`asyncio.TaskGroup` raising a spurious :exc:`ExceptionGroup` instead
2-
of propagating :exc:`GeneratorExit` directly when the ``async with`` block is
3-
exited via an enclosing async generator's :meth:`~agen.aclose`. Errors from
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>`.
1+
Fix :class:`asyncio.TaskGroup` silently discarding errors from sibling
2+
tasks whenever the ``async with`` block exits with a :exc:`SystemExit` or
3+
:exc:`KeyboardInterrupt`. These errors are now reported via
4+
:meth:`loop.call_exception_handler() <asyncio.loop.call_exception_handler>`
5+
instead of being lost.

0 commit comments

Comments
 (0)