Impl a proto "unmasker" `@acm` alongside our test

Such that the suite verifies the wip `maybe_raise_from_masking_exc()`
will raise from a `trio.Cancelled.__context__` since I can't think of
any reason a `Cancelled` should ever be raised in-place of
a non-`Cancelled` XD

Not sure what should be raised instead (or maybe just a `log.warning()`
emitted?) but this starts a draft for refinement at the least. Use the
new `@pytest.mark.parametrize` explicit tuple-of-params form with an
`pytest.param + `.mark.xfail()` for the default behaviour case.
hilevel_serman
Tyler Goodlet 2025-01-10 17:29:11 -05:00
parent 9be457fcf3
commit d74dbab1be
1 changed files with 85 additions and 30 deletions

View File

@ -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,
# },
):
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
),
)
):
# async with maybe_open_nursery(maybe_tn) as tn:
async with trio.open_nursery() as tn:
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
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