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=<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()`.
runtime_to_msgspec
Tyler Goodlet 2024-05-22 10:22:51 -04:00
parent b22f7dcae0
commit 3538ccd799
1 changed files with 87 additions and 14 deletions

View File

@ -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