diff --git a/tests/test_trioisms.py b/tests/test_trioisms.py index be29965..fad99f1 100644 --- a/tests/test_trioisms.py +++ b/tests/test_trioisms.py @@ -86,58 +86,113 @@ def test_stashed_child_nursery(use_start_soon): trio.run(main) -# @pytest.mark.parametrize( -# 'open_tn_outside_acm', -# [True, False] -# # ids='aio_err_triggered={}'.format -# ) @pytest.mark.parametrize( - 'canc_from_finally', - [True, False] - # ids='aio_err_triggered={}'.format + ('unmask_from_canc', 'canc_from_finally'), + [ + (True, False), + (True, True), + pytest.param(False, True, + marks=pytest.mark.xfail(reason="never raises!") + ), + ], + # TODO, ask ronny how to impl this .. XD + # ids='unmask_from_canc={0}, canc_from_finally={1}',#.format, ) def test_acm_embedded_nursery_propagates_enter_err( canc_from_finally: bool, - # open_tn_outside_acm: bool, + unmask_from_canc: bool, ): - # from tractor.trionics import maybe_open_nursery + ''' + Demo how a masking `trio.Cancelled` could be handled by unmasking from the + `.__context__` field when a user (by accident) re-raises from a `finally:`. - # async def canc_then_checkpoint(tn): - # tn.cancel_scope.cancel() - # await trio.lowlevel.checkpoint() + ''' + import tractor @acm - async def wraps_tn_that_always_cancels( - # maybe_tn: trio.Nursery|None = None + async def maybe_raise_from_masking_exc( + tn: trio.Nursery, + unmask_from: BaseException|None = trio.Cancelled + + # TODO, maybe offer a collection? + # unmask_from: set[BaseException] = { + # trio.Cancelled, + # }, ): - # async with maybe_open_nursery(maybe_tn) as tn: - async with trio.open_nursery() as tn: + if not unmask_from: + yield + return + + try: + yield + except* unmask_from as be_eg: + + # TODO, if we offer `unmask_from: set` + # for masker_exc_type in unmask_from: + + matches, rest = be_eg.split(unmask_from) + if not matches: + raise + + for exc_match in be_eg.exceptions: + if ( + (exc_ctx := exc_match.__context__) + and + type(exc_ctx) not in { + # trio.Cancelled, # always by default? + unmask_from, + } + ): + exc_ctx.add_note( + f'\n' + f'WARNING: the above error was masked by a {unmask_from!r} !?!\n' + f'Are you always cancelling? Say from a `finally:` ?\n\n' + + f'{tn!r}' + ) + raise exc_ctx from exc_match + + + @acm + async def wraps_tn_that_always_cancels(): + async with ( + trio.open_nursery() as tn, + maybe_raise_from_masking_exc( + tn=tn, + unmask_from=( + trio.Cancelled + if unmask_from_canc + else None + ), + ) + ): try: yield tn finally: if canc_from_finally: - # await canc_then_checkpoint(tn) tn.cancel_scope.cancel() await trio.lowlevel.checkpoint() async def _main(): - # open_nursery = ( - # trio.open_nursery if open_tn_outside_acm - # else nullcontext - # ) + with tractor.devx.open_crash_handler() as bxerr: + assert not bxerr.value - async with ( - # open_nursery() as tn, - # wraps_tn_that_always_cancels(maybe_tn=tn) as tn - wraps_tn_that_always_cancels() as tn - ): - assert not tn.cancel_scope.cancel_called - assert 0 + async with ( + wraps_tn_that_always_cancels() as tn, + ): + assert not tn.cancel_scope.cancel_called + assert 0 + + assert ( + (err := bxerr.value) + and + type(err) is AssertionError + ) with pytest.raises(ExceptionGroup) as excinfo: trio.run(_main) - eg = excinfo.value + eg: ExceptionGroup = excinfo.value assert_eg, rest_eg = eg.split(AssertionError) assert len(assert_eg.exceptions) == 1