From 50fe098e0676d4147a9edf374e9695c364a25bc7 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 9 Oct 2022 13:12:50 -0400 Subject: [PATCH] First pass, swap `MultiError` for `BaseExceptionGroup` --- tractor/__init__.py | 4 ++-- tractor/_exceptions.py | 28 +++++++++++++++++----------- tractor/_portal.py | 1 - tractor/_root.py | 7 ++++++- tractor/_runtime.py | 19 +++++++++++++------ tractor/_spawn.py | 10 +++++++--- tractor/_state.py | 7 ------- tractor/_supervise.py | 24 +++++++++++++++--------- 8 files changed, 60 insertions(+), 40 deletions(-) diff --git a/tractor/__init__.py b/tractor/__init__.py index c4d40f8..a691df6 100644 --- a/tractor/__init__.py +++ b/tractor/__init__.py @@ -18,7 +18,7 @@ tractor: structured concurrent "actors". """ -from trio import MultiError +from exceptiongroup import BaseExceptionGroup from ._clustering import open_actor_cluster from ._ipc import Channel @@ -62,7 +62,7 @@ __all__ = [ 'ContextCancelled', 'ModuleNotExposed', 'MsgStream', - 'MultiError', + 'BaseExceptionGroup', 'Portal', 'ReceiveMsgStream', 'RemoteActorError', diff --git a/tractor/_exceptions.py b/tractor/_exceptions.py index 9ce59e8..89ad3f9 100644 --- a/tractor/_exceptions.py +++ b/tractor/_exceptions.py @@ -27,6 +27,7 @@ import importlib import builtins import traceback +import exceptiongroup as eg import trio @@ -52,9 +53,6 @@ class RemoteActorError(Exception): self.type = suberror_type self.msgdata = msgdata - # TODO: a trio.MultiError.catch like context manager - # for catching underlying remote errors of a particular type - class InternalActorError(RemoteActorError): """Remote internal ``tractor`` error indicating @@ -139,7 +137,12 @@ def unpack_error( suberror_type = trio.Cancelled else: # try to lookup a suitable local error type - for ns in [builtins, _this_mod, trio]: + for ns in [ + builtins, + _this_mod, + eg, + trio, + ]: try: suberror_type = getattr(ns, type_name) break @@ -158,12 +161,15 @@ def unpack_error( def is_multi_cancelled(exc: BaseException) -> bool: - """Predicate to determine if a ``trio.MultiError`` contains only - ``trio.Cancelled`` sub-exceptions (and is likely the result of + ''' + Predicate to determine if a possible ``eg.BaseExceptionGroup`` contains + only ``trio.Cancelled`` sub-exceptions (and is likely the result of cancelling a collection of subtasks. - """ - return not trio.MultiError.filter( - lambda exc: exc if not isinstance(exc, trio.Cancelled) else None, - exc, - ) + ''' + if isinstance(exc, eg.BaseExceptionGroup): + return exc.subgroup( + lambda exc: isinstance(exc, trio.Cancelled) + ) is not None + + return False diff --git a/tractor/_portal.py b/tractor/_portal.py index de2da45..18b73ac 100644 --- a/tractor/_portal.py +++ b/tractor/_portal.py @@ -460,7 +460,6 @@ class Portal: # sure it's worth being pedantic: # Exception, # trio.Cancelled, - # trio.MultiError, # KeyboardInterrupt, ) as err: diff --git a/tractor/_root.py b/tractor/_root.py index 0e5b2aa..16c4bb8 100644 --- a/tractor/_root.py +++ b/tractor/_root.py @@ -29,6 +29,8 @@ from typing import ( import typing import warnings + +from exceptiongroup import BaseExceptionGroup import trio from ._runtime import Actor, Arbiter, async_main @@ -205,7 +207,10 @@ async def open_root_actor( try: yield actor - except (Exception, trio.MultiError) as err: + except ( + Exception, + BaseExceptionGroup, + ) as err: entered = await _debug._maybe_enter_pm(err) diff --git a/tractor/_runtime.py b/tractor/_runtime.py index 2ece834..5a3a693 100644 --- a/tractor/_runtime.py +++ b/tractor/_runtime.py @@ -37,9 +37,10 @@ import os from contextlib import ExitStack import warnings +from async_generator import aclosing +from exceptiongroup import BaseExceptionGroup import trio # type: ignore from trio_typing import TaskStatus -from async_generator import aclosing from ._ipc import Channel from ._streaming import Context @@ -194,7 +195,7 @@ async def _invoke( res = await coro await chan.send({'return': res, 'cid': cid}) - except trio.MultiError: + except BaseExceptionGroup: # if a context error was set then likely # thei multierror was raised due to that if ctx._error is not None: @@ -266,7 +267,7 @@ async def _invoke( except ( Exception, - trio.MultiError + BaseExceptionGroup, ) as err: if not is_multi_cancelled(err): @@ -349,7 +350,7 @@ def _get_mod_abspath(module): async def try_ship_error_to_parent( channel: Channel, - err: Union[Exception, trio.MultiError], + err: Union[Exception, BaseExceptionGroup], ) -> None: with trio.CancelScope(shield=True): @@ -1549,7 +1550,10 @@ async def process_messages( partial(_invoke, actor, cid, chan, func, kwargs), name=funcname, ) - except (RuntimeError, trio.MultiError): + except ( + RuntimeError, + BaseExceptionGroup, + ): # avoid reporting a benign race condition # during actor runtime teardown. nursery_cancelled_before_task = True @@ -1594,7 +1598,10 @@ async def process_messages( # transport **was** disconnected return True - except (Exception, trio.MultiError) as err: + except ( + Exception, + BaseExceptionGroup, + ) as err: if nursery_cancelled_before_task: sn = actor._service_n assert sn and sn.cancel_scope.cancel_called diff --git a/tractor/_spawn.py b/tractor/_spawn.py index 4a9f118..68d8a41 100644 --- a/tractor/_spawn.py +++ b/tractor/_spawn.py @@ -31,6 +31,7 @@ from typing import ( ) from collections.abc import Awaitable +from exceptiongroup import BaseExceptionGroup import trio from trio_typing import TaskStatus @@ -146,8 +147,11 @@ async def exhaust_portal( # always be established and shutdown using a context manager api final = await portal.result() - except (Exception, trio.MultiError) as err: - # we reraise in the parent task via a ``trio.MultiError`` + except ( + Exception, + BaseExceptionGroup, + ) as err: + # we reraise in the parent task via a ``BaseExceptionGroup`` return err except trio.Cancelled as err: # lol, of course we need this too ;P @@ -175,7 +179,7 @@ async def cancel_on_completion( ''' # if this call errors we store the exception for later # in ``errors`` which will be reraised inside - # a MultiError and we still send out a cancel request + # an exception group and we still send out a cancel request result = await exhaust_portal(portal, actor) if isinstance(result, Exception): errors[actor.uid] = result diff --git a/tractor/_state.py b/tractor/_state.py index d8aa2f2..28fa16e 100644 --- a/tractor/_state.py +++ b/tractor/_state.py @@ -22,7 +22,6 @@ from typing import ( Optional, Any, ) -from collections.abc import Mapping import trio @@ -46,12 +45,6 @@ def current_actor(err_on_no_runtime: bool = True) -> 'Actor': # type: ignore # return _current_actor -_conc_name_getters = { - 'task': trio.lowlevel.current_task, - 'actor': current_actor -} - - def is_main_process() -> bool: """Bool determining if this actor is running in the top-most process. """ diff --git a/tractor/_supervise.py b/tractor/_supervise.py index 4708e1e..65f5e0c 100644 --- a/tractor/_supervise.py +++ b/tractor/_supervise.py @@ -18,6 +18,7 @@ ``trio`` inspired apis and helpers """ +from contextlib import asynccontextmanager as acm from functools import partial import inspect from typing import ( @@ -27,8 +28,8 @@ from typing import ( import typing import warnings +from exceptiongroup import BaseExceptionGroup import trio -from async_generator import asynccontextmanager from ._debug import maybe_wait_for_debugger from ._state import current_actor, is_main_process @@ -294,7 +295,7 @@ class ActorNursery: self._join_procs.set() -@asynccontextmanager +@acm async def _open_and_supervise_one_cancels_all_nursery( actor: Actor, ) -> typing.AsyncGenerator[ActorNursery, None]: @@ -387,13 +388,16 @@ async def _open_and_supervise_one_cancels_all_nursery( # cancel all subactors await anursery.cancel() - except trio.MultiError as merr: + except BaseExceptionGroup as merr: # If we receive additional errors while waiting on # remaining subactors that were cancelled, # aggregate those errors with the original error # that triggered this teardown. if err not in merr.exceptions: - raise trio.MultiError(merr.exceptions + [err]) + raise BaseExceptionGroup( + 'tractor.ActorNursery errored with', + list(merr.exceptions) + [err], + ) else: raise @@ -402,9 +406,8 @@ async def _open_and_supervise_one_cancels_all_nursery( # XXX: do we need a `trio.Cancelled` catch here as well? # this is the catch around the ``.run_in_actor()`` nursery except ( - Exception, - trio.MultiError, + BaseExceptionGroup, trio.Cancelled ) as err: @@ -436,9 +439,12 @@ async def _open_and_supervise_one_cancels_all_nursery( with trio.CancelScope(shield=True): await anursery.cancel() - # use `MultiError` as needed + # use `BaseExceptionGroup` as needed if len(errors) > 1: - raise trio.MultiError(tuple(errors.values())) + raise BaseExceptionGroup( + 'tractor.ActorNursery errored with', + tuple(errors.values()), + ) else: raise list(errors.values())[0] @@ -447,7 +453,7 @@ async def _open_and_supervise_one_cancels_all_nursery( # after nursery exit -@asynccontextmanager +@acm async def open_nursery( **kwargs,