1919from ... import _core
2020from ..._threads import to_thread_run_sync
2121from ..._timeouts import fail_after , sleep
22- from ...testing import Sequencer , assert_checkpoints , wait_all_tasks_blocked
22+ from ...testing import (
23+ Matcher ,
24+ RaisesGroup ,
25+ Sequencer ,
26+ assert_checkpoints ,
27+ wait_all_tasks_blocked ,
28+ )
2329from .._run import DEADLINE_HEAP_MIN_PRUNE_THRESHOLD
2430from .tutil import (
2531 check_sequence_matches ,
@@ -192,13 +198,8 @@ async def main() -> NoReturn:
192198 nursery .start_soon (crasher )
193199 raise KeyError
194200
195- with pytest . raises ( ExceptionGroup ) as excinfo :
201+ with RaisesGroup ( ValueError , KeyError ) :
196202 _core .run (main )
197- print (excinfo .value )
198- assert {type (exc ) for exc in excinfo .value .exceptions } == {
199- ValueError ,
200- KeyError ,
201- }
202203
203204
204205def test_two_child_crashes () -> None :
@@ -210,12 +211,8 @@ async def main() -> None:
210211 nursery .start_soon (crasher , KeyError )
211212 nursery .start_soon (crasher , ValueError )
212213
213- with pytest . raises ( ExceptionGroup ) as excinfo :
214+ with RaisesGroup ( ValueError , KeyError ) :
214215 _core .run (main )
215- assert {type (exc ) for exc in excinfo .value .exceptions } == {
216- ValueError ,
217- KeyError ,
218- }
219216
220217
221218async def test_child_crash_wakes_parent () -> None :
@@ -429,16 +426,18 @@ async def test_cancel_scope_exceptiongroup_filtering() -> None:
429426 async def crasher () -> NoReturn :
430427 raise KeyError
431428
432- # check that the inner except is properly executed.
433- # alternative would be to have a `except BaseException` and an `else`
434- exception_group_caught_inner = False
435-
436429 # This is outside the outer scope, so all the Cancelled
437430 # exceptions should have been absorbed, leaving just a regular
438431 # KeyError from crasher()
439432 with pytest .raises (KeyError ): # noqa: PT012
440433 with _core .CancelScope () as outer :
441- try :
434+ # Since the outer scope became cancelled before the
435+ # nursery block exited, all cancellations inside the
436+ # nursery block continue propagating to reach the
437+ # outer scope.
438+ with RaisesGroup (
439+ _core .Cancelled , _core .Cancelled , _core .Cancelled , KeyError
440+ ) as excinfo :
442441 async with _core .open_nursery () as nursery :
443442 # Two children that get cancelled by the nursery scope
444443 nursery .start_soon (sleep_forever ) # t1
@@ -452,22 +451,9 @@ async def crasher() -> NoReturn:
452451 # And one that raises a different error
453452 nursery .start_soon (crasher ) # t4
454453 # and then our __aexit__ also receives an outer Cancelled
455- except BaseExceptionGroup as multi_exc :
456- exception_group_caught_inner = True
457- # Since the outer scope became cancelled before the
458- # nursery block exited, all cancellations inside the
459- # nursery block continue propagating to reach the
460- # outer scope.
461- # the noqa is for "Found assertion on exception `multi_exc` in `except` block"
462- assert len (multi_exc .exceptions ) == 4 # noqa: PT017
463- summary : dict [type , int ] = {}
464- for exc in multi_exc .exceptions :
465- summary .setdefault (type (exc ), 0 )
466- summary [type (exc )] += 1
467- assert summary == {_core .Cancelled : 3 , KeyError : 1 }
468- raise
469-
470- assert exception_group_caught_inner
454+ # reraise the exception caught by RaisesGroup for the
455+ # CancelScope to handle
456+ raise excinfo .value
471457
472458
473459async def test_precancelled_task () -> None :
@@ -788,14 +774,22 @@ async def task2() -> None:
788774 RuntimeError , match = "which had already been exited"
789775 ) as exc_info :
790776 await nursery_mgr .__aexit__ (* sys .exc_info ())
791- assert type (exc_info .value .__context__ ) is ExceptionGroup
792- assert len (exc_info .value .__context__ .exceptions ) == 3
793- cancelled_in_context = False
794- for exc in exc_info .value .__context__ .exceptions :
795- assert isinstance (exc , RuntimeError )
796- assert "closed before the task exited" in str (exc )
797- cancelled_in_context |= isinstance (exc .__context__ , _core .Cancelled )
798- assert cancelled_in_context # for the sleep_forever
777+
778+ def no_context (exc : RuntimeError ) -> bool :
779+ return exc .__context__ is None
780+
781+ msg = "closed before the task exited"
782+ group = RaisesGroup (
783+ Matcher (RuntimeError , match = msg , check = no_context ),
784+ Matcher (RuntimeError , match = msg , check = no_context ),
785+ # sleep_forever
786+ Matcher (
787+ RuntimeError ,
788+ match = msg ,
789+ check = lambda x : isinstance (x .__context__ , _core .Cancelled ),
790+ ),
791+ )
792+ assert group .matches (exc_info .value .__context__ )
799793
800794 # Trying to exit a cancel scope from an unrelated task raises an error
801795 # without affecting any state
@@ -949,11 +943,7 @@ async def main() -> None:
949943 with pytest .raises (_core .TrioInternalError ) as excinfo :
950944 _core .run (main )
951945
952- me = excinfo .value .__cause__
953- assert isinstance (me , ExceptionGroup )
954- assert len (me .exceptions ) == 2
955- for exc in me .exceptions :
956- assert isinstance (exc , (KeyError , ValueError ))
946+ assert RaisesGroup (KeyError , ValueError ).matches (excinfo .value .__cause__ )
957947
958948
959949def test_system_task_crash_plus_Cancelled () -> None :
@@ -1210,12 +1200,11 @@ async def test_nursery_exception_chaining_doesnt_make_context_loops() -> None:
12101200 async def crasher () -> NoReturn :
12111201 raise KeyError
12121202
1213- with pytest .raises (ExceptionGroup ) as excinfo : # noqa: PT012
1203+ # the ExceptionGroup should not have the KeyError or ValueError as context
1204+ with RaisesGroup (ValueError , KeyError , check = lambda x : x .__context__ is None ):
12141205 async with _core .open_nursery () as nursery :
12151206 nursery .start_soon (crasher )
12161207 raise ValueError
1217- # the ExceptionGroup should not have the KeyError or ValueError as context
1218- assert excinfo .value .__context__ is None
12191208
12201209
12211210def test_TrioToken_identity () -> None :
@@ -1980,11 +1969,10 @@ async def test_nursery_stop_iteration() -> None:
19801969 async def fail () -> NoReturn :
19811970 raise ValueError
19821971
1983- with pytest . raises ( ExceptionGroup ) as excinfo : # noqa: PT012
1972+ with RaisesGroup ( StopIteration , ValueError ):
19841973 async with _core .open_nursery () as nursery :
19851974 nursery .start_soon (fail )
19861975 raise StopIteration
1987- assert tuple (map (type , excinfo .value .exceptions )) == (StopIteration , ValueError )
19881976
19891977
19901978async def test_nursery_stop_async_iteration () -> None :
@@ -2033,7 +2021,18 @@ async def test_traceback_frame_removal() -> None:
20332021 async def my_child_task () -> NoReturn :
20342022 raise KeyError ()
20352023
2036- with pytest .raises (ExceptionGroup ) as excinfo : # noqa: PT012
2024+ def check_traceback (exc : KeyError ) -> bool :
2025+ # The top frame in the exception traceback should be inside the child
2026+ # task, not trio/contextvars internals. And there's only one frame
2027+ # inside the child task, so this will also detect if our frame-removal
2028+ # is too eager.
2029+ tb = exc .__traceback__
2030+ assert tb is not None
2031+ return tb .tb_frame .f_code is my_child_task .__code__
2032+
2033+ expected_exception = Matcher (KeyError , check = check_traceback )
2034+
2035+ with RaisesGroup (expected_exception , expected_exception ):
20372036 # Trick: For now cancel/nursery scopes still leave a bunch of tb gunk
20382037 # behind. But if there's an ExceptionGroup, they leave it on the group,
20392038 # which lets us get a clean look at the KeyError itself. Someday I
@@ -2042,15 +2041,6 @@ async def my_child_task() -> NoReturn:
20422041 async with _core .open_nursery () as nursery :
20432042 nursery .start_soon (my_child_task )
20442043 nursery .start_soon (my_child_task )
2045- first_exc = excinfo .value .exceptions [0 ]
2046- assert isinstance (first_exc , KeyError )
2047- # The top frame in the exception traceback should be inside the child
2048- # task, not trio/contextvars internals. And there's only one frame
2049- # inside the child task, so this will also detect if our frame-removal
2050- # is too eager.
2051- tb = first_exc .__traceback__
2052- assert tb is not None
2053- assert tb .tb_frame .f_code is my_child_task .__code__
20542044
20552045
20562046def test_contextvar_support () -> None :
@@ -2529,15 +2519,12 @@ async def main() -> NoReturn:
25292519 async with _core .open_nursery ():
25302520 raise Exception ("foo" )
25312521
2532- with pytest .raises (
2533- ExceptionGroup , match = "^Exceptions from Trio nursery \\ (1 sub-exception\\ )$"
2534- ) as exc :
2522+ with RaisesGroup (
2523+ Matcher (Exception , match = "^foo$" ),
2524+ match = "^Exceptions from Trio nursery \\ (1 sub-exception\\ )$" ,
2525+ ):
25352526 _core .run (main , strict_exception_groups = True )
25362527
2537- assert len (exc .value .exceptions ) == 1
2538- assert type (exc .value .exceptions [0 ]) is Exception
2539- assert exc .value .exceptions [0 ].args == ("foo" ,)
2540-
25412528
25422529def test_run_strict_exception_groups_nursery_override () -> None :
25432530 """
@@ -2555,14 +2542,10 @@ async def main() -> NoReturn:
25552542
25562543async def test_nursery_strict_exception_groups () -> None :
25572544 """Test that strict exception groups can be enabled on a per-nursery basis."""
2558- with pytest . raises ( ExceptionGroup ) as exc :
2545+ with RaisesGroup ( Matcher ( Exception , match = "^foo$" )) :
25592546 async with _core .open_nursery (strict_exception_groups = True ):
25602547 raise Exception ("foo" )
25612548
2562- assert len (exc .value .exceptions ) == 1
2563- assert type (exc .value .exceptions [0 ]) is Exception
2564- assert exc .value .exceptions [0 ].args == ("foo" ,)
2565-
25662549
25672550async def test_nursery_loose_exception_groups () -> None :
25682551 """Test that loose exception groups can be enabled on a per-nursery basis."""
@@ -2573,20 +2556,18 @@ async def raise_error() -> NoReturn:
25732556 with pytest .raises (RuntimeError , match = "^test error$" ):
25742557 async with _core .open_nursery (strict_exception_groups = False ) as nursery :
25752558 nursery .start_soon (raise_error )
2576-
2577- with pytest .raises ( # noqa: PT012 # multiple statements
2578- ExceptionGroup , match = "^Exceptions from Trio nursery \\ (2 sub-exceptions\\ )$"
2579- ) as exc :
2559+ m = Matcher (RuntimeError , match = "^test error$" )
2560+
2561+ with RaisesGroup (
2562+ m ,
2563+ m ,
2564+ match = "Exceptions from Trio nursery \\ (2 sub-exceptions\\ )" ,
2565+ check = lambda x : x .__notes__ == [_core ._run .NONSTRICT_EXCEPTIONGROUP_NOTE ],
2566+ ):
25802567 async with _core .open_nursery (strict_exception_groups = False ) as nursery :
25812568 nursery .start_soon (raise_error )
25822569 nursery .start_soon (raise_error )
25832570
2584- assert exc .value .__notes__ == [_core ._run .NONSTRICT_EXCEPTIONGROUP_NOTE ]
2585- assert len (exc .value .exceptions ) == 2
2586- for subexc in exc .value .exceptions :
2587- assert type (subexc ) is RuntimeError
2588- assert subexc .args == ("test error" ,)
2589-
25902571
25912572async def test_nursery_collapse_strict () -> None :
25922573 """
@@ -2597,7 +2578,7 @@ async def test_nursery_collapse_strict() -> None:
25972578 async def raise_error () -> NoReturn :
25982579 raise RuntimeError ("test error" )
25992580
2600- with pytest . raises ( ExceptionGroup ) as exc : # noqa: PT012
2581+ with RaisesGroup ( RuntimeError , RaisesGroup ( RuntimeError )):
26012582 async with _core .open_nursery () as nursery :
26022583 nursery .start_soon (sleep_forever )
26032584 nursery .start_soon (raise_error )
@@ -2606,13 +2587,6 @@ async def raise_error() -> NoReturn:
26062587 nursery2 .start_soon (raise_error )
26072588 nursery .cancel_scope .cancel ()
26082589
2609- exceptions = exc .value .exceptions
2610- assert len (exceptions ) == 2
2611- assert isinstance (exceptions [0 ], RuntimeError )
2612- assert isinstance (exceptions [1 ], ExceptionGroup )
2613- assert len (exceptions [1 ].exceptions ) == 1
2614- assert isinstance (exceptions [1 ].exceptions [0 ], RuntimeError )
2615-
26162590
26172591async def test_nursery_collapse_loose () -> None :
26182592 """
@@ -2623,7 +2597,7 @@ async def test_nursery_collapse_loose() -> None:
26232597 async def raise_error () -> NoReturn :
26242598 raise RuntimeError ("test error" )
26252599
2626- with pytest . raises ( ExceptionGroup ) as exc : # noqa: PT012
2600+ with RaisesGroup ( RuntimeError , RuntimeError ):
26272601 async with _core .open_nursery () as nursery :
26282602 nursery .start_soon (sleep_forever )
26292603 nursery .start_soon (raise_error )
@@ -2632,19 +2606,14 @@ async def raise_error() -> NoReturn:
26322606 nursery2 .start_soon (raise_error )
26332607 nursery .cancel_scope .cancel ()
26342608
2635- exceptions = exc .value .exceptions
2636- assert len (exceptions ) == 2
2637- assert isinstance (exceptions [0 ], RuntimeError )
2638- assert isinstance (exceptions [1 ], RuntimeError )
2639-
26402609
26412610async def test_cancel_scope_no_cancellederror () -> None :
26422611 """
26432612 Test that when a cancel scope encounters an exception group that does NOT contain
26442613 a Cancelled exception, it will NOT set the ``cancelled_caught`` flag.
26452614 """
26462615
2647- with pytest . raises ( ExceptionGroup ): # noqa: PT012
2616+ with RaisesGroup ( RuntimeError , RuntimeError , match = "test" ):
26482617 with _core .CancelScope () as scope :
26492618 scope .cancel ()
26502619 raise ExceptionGroup ("test" , [RuntimeError (), RuntimeError ()])
0 commit comments