diff --git a/tractor/trionics/__init__.py b/tractor/trionics/__init__.py index afd1f434..2e91aa30 100644 --- a/tractor/trionics/__init__.py +++ b/tractor/trionics/__init__.py @@ -31,7 +31,7 @@ from ._broadcast import ( ) from ._beg import ( collapse_eg as collapse_eg, - maybe_collapse_eg as maybe_collapse_eg, + get_collapsed_eg as get_collapsed_eg, is_multi_cancelled as is_multi_cancelled, ) from ._taskc import ( diff --git a/tractor/trionics/_beg.py b/tractor/trionics/_beg.py index 4204df19..4c25050e 100644 --- a/tractor/trionics/_beg.py +++ b/tractor/trionics/_beg.py @@ -15,8 +15,9 @@ # along with this program. If not, see . ''' -`BaseExceptionGroup` related utils and helpers pertaining to -first-class-`trio` from a historical perspective B) +`BaseExceptionGroup` utils and helpers pertaining to +first-class-`trio` from a "historical" perspective, like "loose +exception group" task-nurseries. ''' from contextlib import ( @@ -24,28 +25,83 @@ from contextlib import ( ) from typing import ( Literal, + Type, ) import trio +# from trio._core._concat_tb import ( +# concat_tb, +# ) -def maybe_collapse_eg( +# XXX NOTE +# taken verbatim from `trio._core._run` except, +# - remove the NONSTRICT_EXCEPTIONGROUP_NOTE deprecation-note +# guard-check; we know we want an explicit collapse. +# - mask out tb rewriting in collapse case, i don't think it really +# matters? +# +def collapse_exception_group( + excgroup: BaseExceptionGroup[BaseException], +) -> BaseException: + """Recursively collapse any single-exception groups into that single contained + exception. + + """ + exceptions = list(excgroup.exceptions) + modified = False + for i, exc in enumerate(exceptions): + if isinstance(exc, BaseExceptionGroup): + new_exc = collapse_exception_group(exc) + if new_exc is not exc: + modified = True + exceptions[i] = new_exc + + if ( + len(exceptions) == 1 + and isinstance(excgroup, BaseExceptionGroup) + + # XXX trio's loose-setting condition.. + # and NONSTRICT_EXCEPTIONGROUP_NOTE in getattr(excgroup, "__notes__", ()) + ): + # exceptions[0].__traceback__ = concat_tb( + # excgroup.__traceback__, + # exceptions[0].__traceback__, + # ) + return exceptions[0] + elif modified: + return excgroup.derive(exceptions) + else: + return excgroup + + +def get_collapsed_eg( beg: BaseExceptionGroup, -) -> BaseException|bool: + + bp: bool = False, +) -> BaseException|None: ''' - If the input beg can collapse to a single non-eg sub-exception, - return it instead. + If the input beg can collapse to a single sub-exception which is + itself **not** an eg, return it. ''' - if len(excs := beg.exceptions) == 1: - return excs[0] + maybe_exc = collapse_exception_group(beg) + if maybe_exc is beg: + return None + + return maybe_exc - return False @acm async def collapse_eg( hide_tb: bool = True, + + ignore: set[Type[BaseException]] = { + # trio.Cancelled, + }, + add_notes: bool = True, + bp: bool = False, ): ''' If `BaseExceptionGroup` raised in the body scope is @@ -57,16 +113,64 @@ async def collapse_eg( __tracebackhide__: bool = hide_tb try: yield - except* BaseException as beg: + except BaseExceptionGroup as _beg: + beg = _beg + + # TODO, remove this rant.. + # + # except* BaseException as beg: + # ^XXX WOW.. good job cpython. ^ + # like, never ever EVER use this!! XD + # + # turns out rasing from an `except*`-block has the opposite + # behaviour of normal `except` and further can *never* be used to + # get the equiv of, + # `trio.open_nursery(strict_exception_groups=False)` + # + # ------ docs ------ + # https://docs.python.org/3/reference/compound_stmts.html#except-star + # + # > Any remaining exceptions that were not handled by any + # > except* clause are re-raised at the end, along with all + # > exceptions that were raised from within the except* + # > clauses. If this list contains more than one exception to + # > reraise, they are combined into an exception group. + if bp: + from tractor.devx import pause + await pause(shield=True) + if ( - exc := maybe_collapse_eg(beg) + (exc := get_collapsed_eg(beg)) + and + type(exc) not in ignore ): - if cause := exc.__cause__: - raise exc from cause + + # TODO? report number of nested groups it was collapsed + # *from*? + if add_notes: + from_group_note: str = ( + '( ^^^ this exc was collapsed from a group ^^^ )\n' + ) + if ( + from_group_note + not in + getattr(exc, "__notes__", ()) + ): + exc.add_note(from_group_note) raise exc - raise beg + # ?TODO? not needed right? + # if cause := exc.__cause__: + # raise exc# from cause + # else: + # # raise exc from beg + # # suppress "during handling of " + # # output in tb/console. + # raise exc from None + + # keep original + raise # beg def is_multi_cancelled(