From 72c4a9d20b90ae59e30ee0f4504912207fa72ec9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 25 Jul 2025 09:57:20 -0400 Subject: [PATCH] Rework `collapse_eg()` to NOT use `except*`.. Since it turns out the semantics are basically inverse of normal `except` (particularly for re-raising) which is hard to get right, and bc it's a lot easier to just delegate to what `trio` already has behind the `strict_exception_groups=False` setting, Bp I added a rant here which will get removed shortly likely, but i think going forward recommending against use of `except*` is prudent for anything low level enough in the runtime (like trying to filter begs). Dirty deats, - copy `trio._core._run.collapse_exception_group()` to here with only a slight mod to remove the notes check and tb concatting for the collapse case. - rename `maybe_collapse_eg()` - > `get_collapsed_eg()` and delegate it directly to the former `trio` fn; return `None` when it returns the same beg without collapse. - simplify our own `collapse_eg()` to either raise the collapsed `exc` or original `beg`. --- tractor/trionics/__init__.py | 2 +- tractor/trionics/_beg.py | 132 +++++++++++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 15 deletions(-) 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(