From 3538ccd7992282a1b81e3560282a4e03b9e667d2 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Wed, 22 May 2024 10:22:51 -0400 Subject: [PATCH] Better context aware `RemoteActorError.pformat()` Such that when displaying with `.__str__()` we do not show the type header (style) since normally python's raising machinery already prints the type path like `'tractor._exceptions.RemoteActorError:'`, so doing it 2x is a bit ugly ;p In support, - include `.relay_uid` in `RemoteActorError.extra_body_fields`. - offer a `with_type_header: bool` to `.pformat()` and only put the opening type path and closing `')>'` tail line when `True`. - add `.is_inception() -> bool:` for an easy way to determine if the error is multi-hop relayed. - only repr the `'|_relay_uid='` field when an error is an inception. - tweak the invalid-payload case in `_mk_msg_type_err()` to explicitly state in the `message` how the `any_pld` value does not match the `MsgDec.pld_spec` by decoding the invalid `.pld` with an any-dec. - allow `_mk_msg_type_err(**mte_kwargs)` passthrough. - pass `boxed_type=cls` inside `MsgTypeError.from_decode()`. --- tractor/_exceptions.py | 101 +++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/tractor/_exceptions.py b/tractor/_exceptions.py index 8367506..179b49a 100644 --- a/tractor/_exceptions.py +++ b/tractor/_exceptions.py @@ -187,6 +187,9 @@ class RemoteActorError(Exception): ] extra_body_fields: list[str] = [ 'cid', + # NOTE: we only show this on relayed errors (aka + # "inceptions"). + 'relay_uid', 'boxed_type', ] @@ -273,7 +276,7 @@ class RemoteActorError(Exception): @property def ipc_msg(self) -> Struct: ''' - Re-render the underlying `._ipc_msg: Msg` as + Re-render the underlying `._ipc_msg: MsgType` as a `pretty_struct.Struct` for introspection such that the returned value is a read-only copy of the original. @@ -344,7 +347,7 @@ class RemoteActorError(Exception): return str(bt.__name__) @property - def boxed_type(self) -> str: + def boxed_type(self) -> Type[BaseException]: ''' Error type boxed by last actor IPC hop. @@ -409,7 +412,14 @@ class RemoteActorError(Exception): end_char: str = '\n', ) -> str: _repr: str = '' + for key in fields: + if ( + key == 'relay_uid' + and not self.is_inception() + ): + continue + val: Any|None = ( getattr(self, key, None) or @@ -427,6 +437,7 @@ class RemoteActorError(Exception): if val: _repr += f'{key}={val_str}{end_char}' + return _repr def reprol(self) -> str: @@ -455,15 +466,45 @@ class RemoteActorError(Exception): _repr ) - def pformat(self) -> str: + def is_inception(self) -> bool: + ''' + Predicate which determines if the shuttled error type + is the same as the container error type; IOW is this + an "error within and error" which points to some original + source error that was relayed through multiple + actor hops. + + Ex. a relayed remote error will generally be some form of + `RemoteActorError[RemoteActorError]` with a `.src_type` which + is not of that same type. + + ''' + # if a single hop boxed error it was not relayed + # more then one hop directly from the src actor. + if ( + self.boxed_type + is + self.src_type + ): + return False + + return True + + def pformat( + self, + with_type_header: bool = True, + ) -> str: ''' Nicely formatted boxed error meta data + traceback, OR just the normal message from `.args` (for eg. as you'd want shown by a locally raised `ContextCancelled`). ''' - tb_str: str = self.tb_str - if tb_str: + header: str = '' + if with_type_header: + header: str = f'<{type(self).__name__}(\n' + + if tb_str := self.tb_str: fields: str = self._mk_fields_str( _body_fields + @@ -481,19 +522,35 @@ class RemoteActorError(Exception): # |___ .. tb_body_indent=1, ) + if not with_type_header: + body = '\n' + body else: body: str = textwrap.indent( self._message, prefix=' ', ) + '\n' + + if with_type_header: + tail: str = ')>' + else: + tail = '' + return ( - f'<{type(self).__name__}(\n' + header + + f'{body}' - ')>' + + + tail ) __repr__ = pformat - __str__ = pformat + + # NOTE: apparently we need this so that + # the full fields show in debugger tests? + # |_ i guess `pexepect` relies on `str`-casing + # of output? + def __str__(self) -> str: + return self.pformat(with_type_header=False) def unwrap( self, @@ -682,6 +739,7 @@ class MsgTypeError( ) -> MsgTypeError: return cls( message=message, + boxed_type=cls, # NOTE: original "vanilla decode" of the msg-bytes # is placed inside a value readable from @@ -949,10 +1007,11 @@ def _raise_from_unexpected_msg( if isinstance(msg, Error): # match msg: # case Error(): - raise unpack_error( + exc: RemoteActorError = unpack_error( msg, ctx.chan, - ) from src_err + ) + raise exc from src_err # `MsgStream` termination msg. # TODO: does it make more sense to pack @@ -966,10 +1025,11 @@ def _raise_from_unexpected_msg( or isinstance(msg, Stop) ): - log.debug( + message: str = ( f'Context[{cid}] stream was stopped by remote side\n' f'cid: {cid}\n' ) + log.debug(message) # TODO: if the a local task is already blocking on # a `Context.result()` and thus a `.receive()` on the @@ -983,6 +1043,8 @@ def _raise_from_unexpected_msg( f'Context stream ended due to msg:\n\n' f'{pformat(msg)}\n' ) + eoc.add_note(message) + # XXX: important to set so that a new `.receive()` # call (likely by another task using a broadcast receiver) # doesn't accidentally pull the `return` message @@ -1007,6 +1069,7 @@ def _raise_from_unexpected_msg( ' BUT received a non-error msg:\n\n' f'{struct_format(msg)}' ) from src_err + # ^-TODO-^ maybe `MsgDialogError` is better? _raise_from_no_key_in_msg = _raise_from_unexpected_msg @@ -1023,6 +1086,8 @@ def _mk_msg_type_err( src_type_error: TypeError|None = None, is_invalid_payload: bool = False, + **mte_kwargs, + ) -> MsgTypeError: ''' Compose a `MsgTypeError` from an input runtime context. @@ -1081,12 +1146,20 @@ def _mk_msg_type_err( else: if is_invalid_payload: msg_type: str = type(msg) + any_pld: Any = msgpack.decode(msg.pld) message: str = ( f'invalid `{msg_type.__qualname__}` payload\n\n' - f'<{type(msg).__qualname__}(\n' - f' |_pld: {codec.pld_spec_str} = {msg.pld!r}' - f')>\n' + f'value: `{any_pld!r}` does not match type-spec: ' #\n' + f'`{type(msg).__qualname__}.pld: {codec.pld_spec_str}`' + # f'<{type(msg).__qualname__}(\n' + # f' |_pld: {codec.pld_spec_str}\n'# != {any_pld!r}\n' + # f')>\n\n' ) + # TODO: should we just decode the msg to a dict despite + # only the payload being wrong? + # -[ ] maybe the better design is to break this construct + # logic into a separate explicit helper raiser-func? + msg_dict: dict = {} else: # decode the msg-bytes using the std msgpack