19
19
from ... import _core
20
20
from ..._threads import to_thread_run_sync
21
21
from ..._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
+ )
23
29
from .._run import DEADLINE_HEAP_MIN_PRUNE_THRESHOLD
24
30
from .tutil import (
25
31
check_sequence_matches ,
@@ -192,13 +198,8 @@ async def main() -> NoReturn:
192
198
nursery .start_soon (crasher )
193
199
raise KeyError
194
200
195
- with pytest . raises ( ExceptionGroup ) as excinfo :
201
+ with RaisesGroup ( ValueError , KeyError ) :
196
202
_core .run (main )
197
- print (excinfo .value )
198
- assert {type (exc ) for exc in excinfo .value .exceptions } == {
199
- ValueError ,
200
- KeyError ,
201
- }
202
203
203
204
204
205
def test_two_child_crashes () -> None :
@@ -210,12 +211,8 @@ async def main() -> None:
210
211
nursery .start_soon (crasher , KeyError )
211
212
nursery .start_soon (crasher , ValueError )
212
213
213
- with pytest . raises ( ExceptionGroup ) as excinfo :
214
+ with RaisesGroup ( ValueError , KeyError ) :
214
215
_core .run (main )
215
- assert {type (exc ) for exc in excinfo .value .exceptions } == {
216
- ValueError ,
217
- KeyError ,
218
- }
219
216
220
217
221
218
async def test_child_crash_wakes_parent () -> None :
@@ -429,16 +426,18 @@ async def test_cancel_scope_exceptiongroup_filtering() -> None:
429
426
async def crasher () -> NoReturn :
430
427
raise KeyError
431
428
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
-
436
429
# This is outside the outer scope, so all the Cancelled
437
430
# exceptions should have been absorbed, leaving just a regular
438
431
# KeyError from crasher()
439
432
with pytest .raises (KeyError ): # noqa: PT012
440
433
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 :
442
441
async with _core .open_nursery () as nursery :
443
442
# Two children that get cancelled by the nursery scope
444
443
nursery .start_soon (sleep_forever ) # t1
@@ -452,22 +451,9 @@ async def crasher() -> NoReturn:
452
451
# And one that raises a different error
453
452
nursery .start_soon (crasher ) # t4
454
453
# 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
471
457
472
458
473
459
async def test_precancelled_task () -> None :
@@ -788,14 +774,22 @@ async def task2() -> None:
788
774
RuntimeError , match = "which had already been exited"
789
775
) as exc_info :
790
776
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__ )
799
793
800
794
# Trying to exit a cancel scope from an unrelated task raises an error
801
795
# without affecting any state
@@ -949,11 +943,7 @@ async def main() -> None:
949
943
with pytest .raises (_core .TrioInternalError ) as excinfo :
950
944
_core .run (main )
951
945
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__ )
957
947
958
948
959
949
def test_system_task_crash_plus_Cancelled () -> None :
@@ -1210,12 +1200,11 @@ async def test_nursery_exception_chaining_doesnt_make_context_loops() -> None:
1210
1200
async def crasher () -> NoReturn :
1211
1201
raise KeyError
1212
1202
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 ):
1214
1205
async with _core .open_nursery () as nursery :
1215
1206
nursery .start_soon (crasher )
1216
1207
raise ValueError
1217
- # the ExceptionGroup should not have the KeyError or ValueError as context
1218
- assert excinfo .value .__context__ is None
1219
1208
1220
1209
1221
1210
def test_TrioToken_identity () -> None :
@@ -1980,11 +1969,10 @@ async def test_nursery_stop_iteration() -> None:
1980
1969
async def fail () -> NoReturn :
1981
1970
raise ValueError
1982
1971
1983
- with pytest . raises ( ExceptionGroup ) as excinfo : # noqa: PT012
1972
+ with RaisesGroup ( StopIteration , ValueError ):
1984
1973
async with _core .open_nursery () as nursery :
1985
1974
nursery .start_soon (fail )
1986
1975
raise StopIteration
1987
- assert tuple (map (type , excinfo .value .exceptions )) == (StopIteration , ValueError )
1988
1976
1989
1977
1990
1978
async def test_nursery_stop_async_iteration () -> None :
@@ -2033,7 +2021,18 @@ async def test_traceback_frame_removal() -> None:
2033
2021
async def my_child_task () -> NoReturn :
2034
2022
raise KeyError ()
2035
2023
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 ):
2037
2036
# Trick: For now cancel/nursery scopes still leave a bunch of tb gunk
2038
2037
# behind. But if there's an ExceptionGroup, they leave it on the group,
2039
2038
# which lets us get a clean look at the KeyError itself. Someday I
@@ -2042,15 +2041,6 @@ async def my_child_task() -> NoReturn:
2042
2041
async with _core .open_nursery () as nursery :
2043
2042
nursery .start_soon (my_child_task )
2044
2043
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__
2054
2044
2055
2045
2056
2046
def test_contextvar_support () -> None :
@@ -2529,15 +2519,12 @@ async def main() -> NoReturn:
2529
2519
async with _core .open_nursery ():
2530
2520
raise Exception ("foo" )
2531
2521
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
+ ):
2535
2526
_core .run (main , strict_exception_groups = True )
2536
2527
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
-
2541
2528
2542
2529
def test_run_strict_exception_groups_nursery_override () -> None :
2543
2530
"""
@@ -2555,14 +2542,10 @@ async def main() -> NoReturn:
2555
2542
2556
2543
async def test_nursery_strict_exception_groups () -> None :
2557
2544
"""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$" )) :
2559
2546
async with _core .open_nursery (strict_exception_groups = True ):
2560
2547
raise Exception ("foo" )
2561
2548
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
-
2566
2549
2567
2550
async def test_nursery_loose_exception_groups () -> None :
2568
2551
"""Test that loose exception groups can be enabled on a per-nursery basis."""
@@ -2573,20 +2556,18 @@ async def raise_error() -> NoReturn:
2573
2556
with pytest .raises (RuntimeError , match = "^test error$" ):
2574
2557
async with _core .open_nursery (strict_exception_groups = False ) as nursery :
2575
2558
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
+ ):
2580
2567
async with _core .open_nursery (strict_exception_groups = False ) as nursery :
2581
2568
nursery .start_soon (raise_error )
2582
2569
nursery .start_soon (raise_error )
2583
2570
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
-
2590
2571
2591
2572
async def test_nursery_collapse_strict () -> None :
2592
2573
"""
@@ -2597,7 +2578,7 @@ async def test_nursery_collapse_strict() -> None:
2597
2578
async def raise_error () -> NoReturn :
2598
2579
raise RuntimeError ("test error" )
2599
2580
2600
- with pytest . raises ( ExceptionGroup ) as exc : # noqa: PT012
2581
+ with RaisesGroup ( RuntimeError , RaisesGroup ( RuntimeError )):
2601
2582
async with _core .open_nursery () as nursery :
2602
2583
nursery .start_soon (sleep_forever )
2603
2584
nursery .start_soon (raise_error )
@@ -2606,13 +2587,6 @@ async def raise_error() -> NoReturn:
2606
2587
nursery2 .start_soon (raise_error )
2607
2588
nursery .cancel_scope .cancel ()
2608
2589
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
-
2616
2590
2617
2591
async def test_nursery_collapse_loose () -> None :
2618
2592
"""
@@ -2623,7 +2597,7 @@ async def test_nursery_collapse_loose() -> None:
2623
2597
async def raise_error () -> NoReturn :
2624
2598
raise RuntimeError ("test error" )
2625
2599
2626
- with pytest . raises ( ExceptionGroup ) as exc : # noqa: PT012
2600
+ with RaisesGroup ( RuntimeError , RuntimeError ):
2627
2601
async with _core .open_nursery () as nursery :
2628
2602
nursery .start_soon (sleep_forever )
2629
2603
nursery .start_soon (raise_error )
@@ -2632,19 +2606,14 @@ async def raise_error() -> NoReturn:
2632
2606
nursery2 .start_soon (raise_error )
2633
2607
nursery .cancel_scope .cancel ()
2634
2608
2635
- exceptions = exc .value .exceptions
2636
- assert len (exceptions ) == 2
2637
- assert isinstance (exceptions [0 ], RuntimeError )
2638
- assert isinstance (exceptions [1 ], RuntimeError )
2639
-
2640
2609
2641
2610
async def test_cancel_scope_no_cancellederror () -> None :
2642
2611
"""
2643
2612
Test that when a cancel scope encounters an exception group that does NOT contain
2644
2613
a Cancelled exception, it will NOT set the ``cancelled_caught`` flag.
2645
2614
"""
2646
2615
2647
- with pytest . raises ( ExceptionGroup ): # noqa: PT012
2616
+ with RaisesGroup ( RuntimeError , RuntimeError , match = "test" ):
2648
2617
with _core .CancelScope () as scope :
2649
2618
scope .cancel ()
2650
2619
raise ExceptionGroup ("test" , [RuntimeError (), RuntimeError ()])
0 commit comments