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 (
|
||||
asynccontextmanager as acm,
|
||||
)
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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
|
||||
async def maybe_raise_from_masking_exc(
|
||||
tn: trio.Nursery,
|
||||
unmask_from: BaseException|None = trio.Cancelled
|
||||
tn: trio.Nursery|None = None,
|
||||
unmask_from: (
|
||||
BaseException|
|
||||
tuple[BaseException]
|
||||
) = (trio.Cancelled,),
|
||||
|
||||
# TODO, maybe offer a collection?
|
||||
# unmask_from: set[BaseException] = {
|
||||
# trio.Cancelled,
|
||||
# },
|
||||
):
|
||||
if not unmask_from:
|
||||
yield
|
||||
return
|
||||
raise_unmasked: bool = True,
|
||||
extra_note: str = (
|
||||
'This can occurr when,\n'
|
||||
' - a `trio.Nursery` scope embeds a `finally:`-block '
|
||||
'which executes a checkpoint!'
|
||||
#
|
||||
# ^TODO? other cases?
|
||||
),
|
||||
|
||||
try:
|
||||
yield
|
||||
except* unmask_from as be_eg:
|
||||
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`).
|
||||
|
||||
# TODO, if we offer `unmask_from: set`
|
||||
# for masker_exc_type in unmask_from:
|
||||
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`.
|
||||
|
||||
matches, rest = be_eg.split(unmask_from)
|
||||
if not matches:
|
||||
raise
|
||||
-------------
|
||||
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?
|
||||
|
||||
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(
|
||||
'''
|
||||
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
|
||||
except* unmask_from as _maybe_eg:
|
||||
maybe_eg = _maybe_eg
|
||||
matches: ExceptionGroup
|
||||
matches, _ = maybe_eg.split(
|
||||
unmask_from
|
||||
)
|
||||
if not matches:
|
||||
raise
|
||||
|
||||
matching: list[BaseException] = matches.exceptions
|
||||
else:
|
||||
try: # handle non-egs
|
||||
yield boxed_maybe_exc
|
||||
return
|
||||
except unmask_from as _maybe_exc:
|
||||
maybe_exc = _maybe_exc
|
||||
matching: list[BaseException] = [
|
||||
maybe_exc
|
||||
]
|
||||
|
||||
# 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'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}'
|
||||
f'{extra_note}\n'
|
||||
)
|
||||
raise exc_ctx from exc_match
|
||||
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
|
||||
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