From 86346c27e874200a530d4151fbcfc535dc774d20 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 15 Jun 2025 18:03:37 -0400 Subject: [PATCH] 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. --- tractor/trionics/_taskc.py | 179 ++++++++++++++++++++++++++++++------- 1 file changed, 145 insertions(+), 34 deletions(-) diff --git a/tractor/trionics/_taskc.py b/tractor/trionics/_taskc.py index 124ce8c6..4ccd4ebe 100644 --- a/tractor/trionics/_taskc.py +++ b/tractor/trionics/_taskc.py @@ -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