Extend `._taskc.maybe_raise_from_masking_exc()`
To handle captured non-egs (when the now optional `tn` isn't provided) as well as yield up a `BoxedMaybeException` which contains any detected and un-masked `exc_ctx` as its `.value`. Also add some additional tooling, - a `raise_unmasked: bool` toggle for when the caller just wants to report the masked exc and not raise-it-in-place of the masker. - `extra_note: str` which by default is tuned to the default `unmask_from = (trio.Cancelled,)` but which can be used to deliver custom exception msg content. - `always_warn_on: tuple[BaseException]` which will always emit a warning log of what would have been the raised-in-place-of `ctx_exc`'s msg for special cases where you want to report a masking case that might not be otherwise noticed by the runtime (cough like a `Cancelled` masking another `Cancelled) but which you'd still like to warn the caller about. - factor out the masked-`ext_ctx` predicate logic into a `find_masked_excs()` and also use it for non-eg cases. Still maybe todo? - rewrapping multiple masked sub-excs in an eg back into an eg? left in #TODOs and a pause-point where applicable.moar_eg_smoothing
parent
0687f1aaa6
commit
86346c27e8
|
@ -22,50 +22,161 @@ from __future__ import annotations
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
)
|
)
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
# from trio import TaskStatus
|
from tractor.log import get_logger
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from tractor.devx.debug import BoxedMaybeException
|
||||||
|
|
||||||
|
|
||||||
|
def find_masked_excs(
|
||||||
|
maybe_masker: BaseException,
|
||||||
|
unmask_from: set[BaseException],
|
||||||
|
) -> BaseException|None:
|
||||||
|
''''
|
||||||
|
Deliver any `maybe_masker.__context__` provided
|
||||||
|
it a declared masking exc-type entry in `unmask_from`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if (
|
||||||
|
type(maybe_masker) in unmask_from
|
||||||
|
and
|
||||||
|
(exc_ctx := maybe_masker.__context__)
|
||||||
|
|
||||||
|
# TODO? what about any cases where
|
||||||
|
# they could be the same type but not same instance?
|
||||||
|
# |_i.e. a cancel masking a cancel ??
|
||||||
|
# or (
|
||||||
|
# exc_ctx is not maybe_masker
|
||||||
|
# )
|
||||||
|
):
|
||||||
|
return exc_ctx
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def maybe_raise_from_masking_exc(
|
async def maybe_raise_from_masking_exc(
|
||||||
tn: trio.Nursery,
|
tn: trio.Nursery|None = None,
|
||||||
unmask_from: BaseException|None = trio.Cancelled
|
unmask_from: (
|
||||||
|
BaseException|
|
||||||
|
tuple[BaseException]
|
||||||
|
) = (trio.Cancelled,),
|
||||||
|
|
||||||
# TODO, maybe offer a collection?
|
raise_unmasked: bool = True,
|
||||||
# unmask_from: set[BaseException] = {
|
extra_note: str = (
|
||||||
# trio.Cancelled,
|
'This can occurr when,\n'
|
||||||
# },
|
' - a `trio.Nursery` scope embeds a `finally:`-block '
|
||||||
):
|
'which executes a checkpoint!'
|
||||||
if not unmask_from:
|
#
|
||||||
yield
|
# ^TODO? other cases?
|
||||||
|
),
|
||||||
|
|
||||||
|
always_warn_on: tuple[BaseException] = (
|
||||||
|
trio.Cancelled,
|
||||||
|
),
|
||||||
|
# ^XXX, special case(s) where we warn-log bc likely
|
||||||
|
# there will be no operational diff since the exc
|
||||||
|
# is always expected to be consumed.
|
||||||
|
) -> BoxedMaybeException:
|
||||||
|
'''
|
||||||
|
Maybe un-mask and re-raise exception(s) suppressed by a known
|
||||||
|
error-used-as-signal type (cough namely `trio.Cancelled`).
|
||||||
|
|
||||||
|
Though this unmasker targets cancelleds, it can be used more
|
||||||
|
generally to capture and unwrap masked excs detected as
|
||||||
|
`.__context__` values which were suppressed by any error type
|
||||||
|
passed in `unmask_from`.
|
||||||
|
|
||||||
|
-------------
|
||||||
|
STILL-TODO ??
|
||||||
|
-------------
|
||||||
|
-[ ] support for egs which have multiple masked entries in
|
||||||
|
`maybe_eg.exceptions`, in which case we should unmask the
|
||||||
|
individual sub-excs but maintain the eg-parent's form right?
|
||||||
|
|
||||||
|
'''
|
||||||
|
from tractor.devx.debug import (
|
||||||
|
BoxedMaybeException,
|
||||||
|
pause,
|
||||||
|
)
|
||||||
|
boxed_maybe_exc = BoxedMaybeException(
|
||||||
|
raise_on_exit=raise_unmasked,
|
||||||
|
)
|
||||||
|
matching: list[BaseException]|None = None
|
||||||
|
maybe_eg: ExceptionGroup|None
|
||||||
|
maybe_eg: ExceptionGroup|None
|
||||||
|
|
||||||
|
if tn:
|
||||||
|
try: # handle egs
|
||||||
|
yield boxed_maybe_exc
|
||||||
return
|
return
|
||||||
|
except* unmask_from as _maybe_eg:
|
||||||
try:
|
maybe_eg = _maybe_eg
|
||||||
yield
|
matches: ExceptionGroup
|
||||||
except* unmask_from as be_eg:
|
matches, _ = maybe_eg.split(
|
||||||
|
unmask_from
|
||||||
# TODO, if we offer `unmask_from: set`
|
)
|
||||||
# for masker_exc_type in unmask_from:
|
|
||||||
|
|
||||||
matches, rest = be_eg.split(unmask_from)
|
|
||||||
if not matches:
|
if not matches:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
for exc_match in be_eg.exceptions:
|
matching: list[BaseException] = matches.exceptions
|
||||||
if (
|
else:
|
||||||
(exc_ctx := exc_match.__context__)
|
try: # handle non-egs
|
||||||
and
|
yield boxed_maybe_exc
|
||||||
type(exc_ctx) not in {
|
return
|
||||||
# trio.Cancelled, # always by default?
|
except unmask_from as _maybe_exc:
|
||||||
unmask_from,
|
maybe_exc = _maybe_exc
|
||||||
}
|
matching: list[BaseException] = [
|
||||||
):
|
maybe_exc
|
||||||
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}'
|
# XXX, only unmask-ed for debuggin!
|
||||||
|
# TODO, remove eventually..
|
||||||
|
except BaseException as _berr:
|
||||||
|
berr = _berr
|
||||||
|
await pause(shield=True)
|
||||||
|
raise berr
|
||||||
|
|
||||||
|
if matching is None:
|
||||||
|
raise
|
||||||
|
|
||||||
|
masked: list[tuple[BaseException, BaseException]] = []
|
||||||
|
for exc_match in matching:
|
||||||
|
|
||||||
|
if exc_ctx := find_masked_excs(
|
||||||
|
maybe_masker=exc_match,
|
||||||
|
unmask_from={unmask_from},
|
||||||
|
):
|
||||||
|
masked.append((exc_ctx, exc_match))
|
||||||
|
boxed_maybe_exc.value = exc_match
|
||||||
|
note: str = (
|
||||||
|
f'\n'
|
||||||
|
f'^^WARNING^^ the above {exc_ctx!r} was masked by a {unmask_from!r}\n'
|
||||||
)
|
)
|
||||||
|
if extra_note:
|
||||||
|
note += (
|
||||||
|
f'\n'
|
||||||
|
f'{extra_note}\n'
|
||||||
|
)
|
||||||
|
exc_ctx.add_note(note)
|
||||||
|
|
||||||
|
if type(exc_match) in always_warn_on:
|
||||||
|
log.warning(note)
|
||||||
|
|
||||||
|
# await tractor.pause(shield=True)
|
||||||
|
if raise_unmasked:
|
||||||
|
|
||||||
|
if len(masked) < 2:
|
||||||
raise exc_ctx from exc_match
|
raise exc_ctx from exc_match
|
||||||
|
else:
|
||||||
|
# ?TODO, see above but, possibly unmasking sub-exc
|
||||||
|
# entries if there are > 1
|
||||||
|
await pause(shield=True)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
Loading…
Reference in New Issue