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
parent
9be457fcf3
commit
d74dbab1be
|
@ -86,58 +86,113 @@ def test_stashed_child_nursery(use_start_soon):
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
# @pytest.mark.parametrize(
|
|
||||||
# 'open_tn_outside_acm',
|
|
||||||
# [True, False]
|
|
||||||
# # ids='aio_err_triggered={}'.format
|
|
||||||
# )
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'canc_from_finally',
|
('unmask_from_canc', 'canc_from_finally'),
|
||||||
[True, False]
|
[
|
||||||
# ids='aio_err_triggered={}'.format
|
(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(
|
def test_acm_embedded_nursery_propagates_enter_err(
|
||||||
canc_from_finally: bool,
|
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()
|
import tractor
|
||||||
# await trio.lowlevel.checkpoint()
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def wraps_tn_that_always_cancels(
|
async def maybe_raise_from_masking_exc(
|
||||||
# maybe_tn: trio.Nursery|None = None
|
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:
|
try:
|
||||||
yield tn
|
yield tn
|
||||||
finally:
|
finally:
|
||||||
if canc_from_finally:
|
if canc_from_finally:
|
||||||
# await canc_then_checkpoint(tn)
|
|
||||||
tn.cancel_scope.cancel()
|
tn.cancel_scope.cancel()
|
||||||
await trio.lowlevel.checkpoint()
|
await trio.lowlevel.checkpoint()
|
||||||
|
|
||||||
async def _main():
|
async def _main():
|
||||||
# open_nursery = (
|
with tractor.devx.open_crash_handler() as bxerr:
|
||||||
# trio.open_nursery if open_tn_outside_acm
|
assert not bxerr.value
|
||||||
# else nullcontext
|
|
||||||
# )
|
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
# open_nursery() as tn,
|
wraps_tn_that_always_cancels() 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 not tn.cancel_scope.cancel_called
|
||||||
assert 0
|
assert 0
|
||||||
|
|
||||||
|
assert (
|
||||||
|
(err := bxerr.value)
|
||||||
|
and
|
||||||
|
type(err) is AssertionError
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(ExceptionGroup) as excinfo:
|
with pytest.raises(ExceptionGroup) as excinfo:
|
||||||
trio.run(_main)
|
trio.run(_main)
|
||||||
|
|
||||||
eg = excinfo.value
|
eg: ExceptionGroup = excinfo.value
|
||||||
assert_eg, rest_eg = eg.split(AssertionError)
|
assert_eg, rest_eg = eg.split(AssertionError)
|
||||||
|
|
||||||
assert len(assert_eg.exceptions) == 1
|
assert len(assert_eg.exceptions) == 1
|
||||||
|
|
Loading…
Reference in New Issue