is also included in the exception group.
The same special case is made for
:exc:`KeyboardInterrupt` and :exc:`SystemExit` as in the previous paragraph.
+There is an additional special case made only for the body of the
+``async with``: if it raises :exc:`GeneratorExit` and none of the
+other tasks raise exceptions that would be reported, then the
+:exc:`GeneratorExit` is reraised.
Task groups are careful not to mix up the internal cancellation used to
"wake up" their :meth:`~object.__aexit__` with cancellation requests
Improved handling of simultaneous internal and external cancellations
and correct preservation of cancellation counts.
+.. versionchanged:: 3.15
+
+ Addition of the special case for :exc:`GeneratorExit`.
+
Sleeping
========
self._parent_task.uncancel()
self._parent_task.cancel()
try:
- raise BaseExceptionGroup(
- 'unhandled errors in a TaskGroup',
- self._errors,
- ) from None
+ # If the *only* error is a GeneratorExit from the body
+ # of the group, then instead of raising an
+ # ExceptionGroup we raise GeneratorExit. This ensures
+ # that async generators that use TaskGroup properly
+ # swallow the exception on `aclose()` while ensuring
+ # that no exceptions from subtasks are swallowed.
+ if (
+ et is not None
+ and issubclass(et, GeneratorExit)
+ and len(self._errors) == 1
+ ):
+ raise exc
+ else:
+ raise BaseExceptionGroup(
+ 'unhandled errors in a TaskGroup',
+ self._errors,
+ ) from None
finally:
exc = None
self.assertEqual(await race(fn_1, fn_2, fn_3), 1)
self.assertListEqual(record, ["1 started", "2 started", "3 started", "1 finished"])
+ async def test_taskgroup_generator_exit_01(self):
+ # GeneratorExit in a TaskGroup should be fine
+ async def gen():
+ yield 1
+
+ async def fn():
+ async with asyncio.TaskGroup() as tg:
+ async for n in gen():
+ yield n
+
+ g = fn()
+ await g.asend(None)
+ await g.aclose()
+
+ async def test_taskgroup_generator_exit_02(self):
+ # A lone GeneratorExit in a task should still give an ExceptionGroup
+ async def t():
+ raise GeneratorExit
+
+ async def fn():
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(t())
+
+ with self.assertRaises(BaseExceptionGroup) as cm:
+ await fn()
+ self.assertEqual(get_error_types(cm.exception), {GeneratorExit})
+
+ async def test_taskgroup_generator_exit_03(self):
+ # A GeneratorExit in one task and an error in another should
+ # still give an ExceptionGroup
+ async def t1():
+ raise GeneratorExit
+
+ async def t2():
+ raise AssertionError('t2 failed')
+
+ async def fn():
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(t1())
+ tg.create_task(t2())
+
+ with self.assertRaises(BaseExceptionGroup) as cm:
+ await fn()
+
+ self.assertEqual(get_error_types(cm.exception), {GeneratorExit, AssertionError})
+
+ async def test_taskgroup_generator_exit_04(self):
+ event = asyncio.Event()
+ async def t():
+ event.set()
+ raise AssertionError('t failed')
+
+ async def fn():
+ async with asyncio.TaskGroup() as tg:
+ tg.create_task(t())
+ yield 1
+
+ g = fn()
+ await g.asend(None)
+ await event.wait() # wait for t() to run
+
+ with self.assertRaises(BaseExceptionGroup) as cm:
+ await g.aclose()
+
+ self.assertEqual(get_error_types(cm.exception), {GeneratorExit, AssertionError})
+
class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase):
loop_factory = asyncio.EventLoop