@@ -608,6 +608,87 @@ async def runner():
608608 get_error_types (cm .exception ), {MyBaseExc , ZeroDivisionError }
609609 )
610610
611+ 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+
611692 async def _test_taskgroup_21 (self ):
612693 # This test doesn't work as asyncio, currently, doesn't
613694 # correctly propagate KeyboardInterrupt (or SystemExit) --
0 commit comments