forked from goodboy/tractor
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
parent
b22f7dcae0
commit
3538ccd799
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue