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