forked from goodboy/tractor
Better RAE `.pformat()`-ing for send-side MTEs
Send-side `MsgTypeError`s actually shouldn't have any "boxed" traceback per say since they're raised in the transmitting actor's local task env and we (normally) don't want the ascii decoration added around the error's `._message: str`, that is not until the exc is `pack_error()`-ed before transit. As such, the presentation of an embedded traceback (and its ascii box) gets bypassed when only a `._message: str` is set (as we now do for pld-spec failures in `_mk_msg_type_err()`). Further this tweaks the `.pformat()` output to include the `._message` part to look like `<RemoteActorError( <._message> ) ..` instead of jamming it implicitly to the end of the embedded `.tb_str` (as was done implicitly by `unpack_error()`) and also adds better handling for the `with_type_header == False` case including forcing that case when we detect that the currently handled exc is the RAE in `.pformat()`. Toss in a lengthier doc-str explaining it all. Surrounding/supporting changes, - better `unpack_error()` message which just briefly reports the remote task's error type. - add public `.message: str` prop. - always set a `._extra_msgdata: dict` since some MTE props rely on it. - handle `.boxed_type == None` for `.boxed_type_str`. - maybe pack any detected input or `exc.message` in `pack_error()`. - comment cruft cleanup in `_mk_msg_type_err()`.runtime_to_msgspec
parent
1db5d4def2
commit
0e8c60ee4a
|
@ -22,6 +22,7 @@ from __future__ import annotations
|
||||||
import builtins
|
import builtins
|
||||||
import importlib
|
import importlib
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
|
import sys
|
||||||
from types import (
|
from types import (
|
||||||
TracebackType,
|
TracebackType,
|
||||||
)
|
)
|
||||||
|
@ -110,6 +111,7 @@ _body_fields: list[str] = list(
|
||||||
'tb_str',
|
'tb_str',
|
||||||
'relay_path',
|
'relay_path',
|
||||||
'cid',
|
'cid',
|
||||||
|
'message',
|
||||||
|
|
||||||
# only ctxc should show it but `Error` does
|
# only ctxc should show it but `Error` does
|
||||||
# have it as an optional field.
|
# have it as an optional field.
|
||||||
|
@ -236,6 +238,7 @@ class RemoteActorError(Exception):
|
||||||
self._boxed_type: BaseException = boxed_type
|
self._boxed_type: BaseException = boxed_type
|
||||||
self._src_type: BaseException|None = None
|
self._src_type: BaseException|None = None
|
||||||
self._ipc_msg: Error|None = ipc_msg
|
self._ipc_msg: Error|None = ipc_msg
|
||||||
|
self._extra_msgdata = extra_msgdata
|
||||||
|
|
||||||
if (
|
if (
|
||||||
extra_msgdata
|
extra_msgdata
|
||||||
|
@ -250,8 +253,6 @@ class RemoteActorError(Exception):
|
||||||
k,
|
k,
|
||||||
v,
|
v,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
self._extra_msgdata = extra_msgdata
|
|
||||||
|
|
||||||
# TODO: mask out eventually or place in `pack_error()`
|
# TODO: mask out eventually or place in `pack_error()`
|
||||||
# pre-`return` lines?
|
# pre-`return` lines?
|
||||||
|
@ -282,6 +283,17 @@ class RemoteActorError(Exception):
|
||||||
# ensure any roundtripping evals to the input value
|
# ensure any roundtripping evals to the input value
|
||||||
assert self.boxed_type is boxed_type
|
assert self.boxed_type is boxed_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def message(self) -> str:
|
||||||
|
'''
|
||||||
|
Be explicit, instead of trying to read it from the the parent
|
||||||
|
type's loosely defined `.args: tuple`:
|
||||||
|
|
||||||
|
https://docs.python.org/3/library/exceptions.html#BaseException.args
|
||||||
|
|
||||||
|
'''
|
||||||
|
return self._message
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ipc_msg(self) -> Struct:
|
def ipc_msg(self) -> Struct:
|
||||||
'''
|
'''
|
||||||
|
@ -355,7 +367,10 @@ class RemoteActorError(Exception):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
bt: Type[BaseException] = self.boxed_type
|
bt: Type[BaseException] = self.boxed_type
|
||||||
return str(bt.__name__)
|
if bt:
|
||||||
|
return str(bt.__name__)
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def boxed_type(self) -> Type[BaseException]:
|
def boxed_type(self) -> Type[BaseException]:
|
||||||
|
@ -426,8 +441,7 @@ class RemoteActorError(Exception):
|
||||||
|
|
||||||
for key in fields:
|
for key in fields:
|
||||||
if (
|
if (
|
||||||
key == 'relay_uid'
|
key == 'relay_uid' and not self.is_inception()
|
||||||
and not self.is_inception()
|
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -504,19 +518,80 @@ class RemoteActorError(Exception):
|
||||||
def pformat(
|
def pformat(
|
||||||
self,
|
self,
|
||||||
with_type_header: bool = True,
|
with_type_header: bool = True,
|
||||||
|
# with_ascii_box: bool = True,
|
||||||
|
|
||||||
) -> str:
|
) -> str:
|
||||||
'''
|
'''
|
||||||
Nicely formatted boxed error meta data + traceback, OR just
|
Format any boxed remote error by multi-line display of,
|
||||||
the normal message from `.args` (for eg. as you'd want shown
|
|
||||||
by a locally raised `ContextCancelled`).
|
- error's src or relay actor meta-data,
|
||||||
|
- remote runtime env's traceback,
|
||||||
|
|
||||||
|
With optional control over the format of,
|
||||||
|
|
||||||
|
- whether the boxed traceback is ascii-decorated with
|
||||||
|
a surrounding "box" annotating the embedded stack-trace.
|
||||||
|
- if the error's type name should be added as margins
|
||||||
|
around the field and tb content like:
|
||||||
|
|
||||||
|
`<RemoteActorError(.. <<multi-line-content>> .. )>`
|
||||||
|
|
||||||
|
- the placement of the `.message: str` (explicit equiv of
|
||||||
|
`.args[0]`), either placed below the `.tb_str` or in the
|
||||||
|
first line's header when the error is raised locally (since
|
||||||
|
the type name is already implicitly shown by python).
|
||||||
|
|
||||||
'''
|
'''
|
||||||
header: str = ''
|
header: str = ''
|
||||||
body: str = ''
|
body: str = ''
|
||||||
|
message: str = ''
|
||||||
|
|
||||||
|
# XXX when the currently raised exception is this instance,
|
||||||
|
# we do not ever use the "type header" style repr.
|
||||||
|
is_being_raised: bool = False
|
||||||
|
if (
|
||||||
|
(exc := sys.exception())
|
||||||
|
and
|
||||||
|
exc is self
|
||||||
|
):
|
||||||
|
is_being_raised: bool = True
|
||||||
|
|
||||||
|
with_type_header: bool = (
|
||||||
|
with_type_header
|
||||||
|
and
|
||||||
|
not is_being_raised
|
||||||
|
)
|
||||||
|
|
||||||
|
# <RemoteActorError( .. )> style
|
||||||
if with_type_header:
|
if with_type_header:
|
||||||
header: str = f'<{type(self).__name__}(\n'
|
header: str = f'<{type(self).__name__}('
|
||||||
|
|
||||||
|
if message := self._message:
|
||||||
|
|
||||||
|
# split off the first line so, if needed, it isn't
|
||||||
|
# indented the same like the "boxed content" which
|
||||||
|
# since there is no `.tb_str` is just the `.message`.
|
||||||
|
lines: list[str] = message.splitlines()
|
||||||
|
first: str = lines[0]
|
||||||
|
message: str = message.removeprefix(first)
|
||||||
|
|
||||||
|
# with a type-style header we,
|
||||||
|
# - have no special message "first line" extraction/handling
|
||||||
|
# - place the message a space in from the header:
|
||||||
|
# `MsgTypeError( <message> ..`
|
||||||
|
# ^-here
|
||||||
|
# - indent the `.message` inside the type body.
|
||||||
|
if with_type_header:
|
||||||
|
first = f' {first} )>'
|
||||||
|
|
||||||
|
message: str = textwrap.indent(
|
||||||
|
message,
|
||||||
|
prefix=' '*2,
|
||||||
|
)
|
||||||
|
message: str = first + message
|
||||||
|
|
||||||
|
# IFF there is an embedded traceback-str we always
|
||||||
|
# draw the ascii-box around it.
|
||||||
if tb_str := self.tb_str:
|
if tb_str := self.tb_str:
|
||||||
fields: str = self._mk_fields_str(
|
fields: str = self._mk_fields_str(
|
||||||
_body_fields
|
_body_fields
|
||||||
|
@ -535,36 +610,19 @@ class RemoteActorError(Exception):
|
||||||
# |___ ..
|
# |___ ..
|
||||||
tb_body_indent=1,
|
tb_body_indent=1,
|
||||||
)
|
)
|
||||||
if not with_type_header:
|
|
||||||
body = '\n' + body
|
|
||||||
|
|
||||||
elif message := self._message:
|
tail = ''
|
||||||
# split off the first line so it isn't indented
|
if (
|
||||||
# the same like the "boxed content".
|
with_type_header
|
||||||
if not with_type_header:
|
and not message
|
||||||
lines: list[str] = message.splitlines()
|
):
|
||||||
first: str = lines[0]
|
tail: str = '>'
|
||||||
message: str = message.removeprefix(first)
|
|
||||||
|
|
||||||
else:
|
|
||||||
first: str = ''
|
|
||||||
|
|
||||||
body: str = (
|
|
||||||
first
|
|
||||||
+
|
|
||||||
message
|
|
||||||
+
|
|
||||||
'\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
if with_type_header:
|
|
||||||
tail: str = ')>'
|
|
||||||
else:
|
|
||||||
tail = ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
header
|
header
|
||||||
+
|
+
|
||||||
|
message
|
||||||
|
+
|
||||||
f'{body}'
|
f'{body}'
|
||||||
+
|
+
|
||||||
tail
|
tail
|
||||||
|
@ -577,7 +635,9 @@ class RemoteActorError(Exception):
|
||||||
# |_ i guess `pexepect` relies on `str`-casing
|
# |_ i guess `pexepect` relies on `str`-casing
|
||||||
# of output?
|
# of output?
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.pformat(with_type_header=False)
|
return self.pformat(
|
||||||
|
with_type_header=False
|
||||||
|
)
|
||||||
|
|
||||||
def unwrap(
|
def unwrap(
|
||||||
self,
|
self,
|
||||||
|
@ -825,9 +885,6 @@ class MsgTypeError(
|
||||||
extra_msgdata['_bad_msg'] = bad_msg
|
extra_msgdata['_bad_msg'] = bad_msg
|
||||||
extra_msgdata['cid'] = bad_msg.cid
|
extra_msgdata['cid'] = bad_msg.cid
|
||||||
|
|
||||||
if 'cid' not in extra_msgdata:
|
|
||||||
import pdbp; pdbp.set_trace()
|
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
message=message,
|
message=message,
|
||||||
boxed_type=cls,
|
boxed_type=cls,
|
||||||
|
@ -889,6 +946,7 @@ def pack_error(
|
||||||
src_uid: tuple[str, str]|None = None,
|
src_uid: tuple[str, str]|None = None,
|
||||||
tb: TracebackType|None = None,
|
tb: TracebackType|None = None,
|
||||||
tb_str: str = '',
|
tb_str: str = '',
|
||||||
|
message: str = '',
|
||||||
|
|
||||||
) -> Error:
|
) -> Error:
|
||||||
'''
|
'''
|
||||||
|
@ -971,7 +1029,7 @@ def pack_error(
|
||||||
# the locally raised error (so, NOT the prior relay's boxed
|
# the locally raised error (so, NOT the prior relay's boxed
|
||||||
# `._ipc_msg.tb_str`).
|
# `._ipc_msg.tb_str`).
|
||||||
error_msg['tb_str'] = tb_str
|
error_msg['tb_str'] = tb_str
|
||||||
|
error_msg['message'] = message or getattr(exc, 'message', '')
|
||||||
if cid is not None:
|
if cid is not None:
|
||||||
error_msg['cid'] = cid
|
error_msg['cid'] = cid
|
||||||
|
|
||||||
|
@ -995,26 +1053,24 @@ def unpack_error(
|
||||||
if not isinstance(msg, Error):
|
if not isinstance(msg, Error):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# retrieve the remote error's msg-encoded details
|
|
||||||
tb_str: str = msg.tb_str
|
|
||||||
message: str = (
|
|
||||||
f'{chan.uid}\n'
|
|
||||||
+
|
|
||||||
tb_str
|
|
||||||
)
|
|
||||||
|
|
||||||
# try to lookup a suitable error type from the local runtime
|
# try to lookup a suitable error type from the local runtime
|
||||||
# env then use it to construct a local instance.
|
# env then use it to construct a local instance.
|
||||||
# boxed_type_str: str = error_dict['boxed_type_str']
|
# boxed_type_str: str = error_dict['boxed_type_str']
|
||||||
boxed_type_str: str = msg.boxed_type_str
|
boxed_type_str: str = msg.boxed_type_str
|
||||||
boxed_type: Type[BaseException] = get_err_type(boxed_type_str)
|
boxed_type: Type[BaseException] = get_err_type(boxed_type_str)
|
||||||
|
|
||||||
if boxed_type_str == 'ContextCancelled':
|
# retrieve the error's msg-encoded remotoe-env info
|
||||||
box_type = ContextCancelled
|
message: str = f'remote task raised a {msg.boxed_type_str!r}\n'
|
||||||
assert boxed_type is box_type
|
|
||||||
|
|
||||||
elif boxed_type_str == 'MsgTypeError':
|
# TODO: do we even really need these checks for RAEs?
|
||||||
box_type = MsgTypeError
|
if boxed_type_str in [
|
||||||
|
'ContextCancelled',
|
||||||
|
'MsgTypeError',
|
||||||
|
]:
|
||||||
|
box_type = {
|
||||||
|
'ContextCancelled': ContextCancelled,
|
||||||
|
'MsgTypeError': MsgTypeError,
|
||||||
|
}[boxed_type_str]
|
||||||
assert boxed_type is box_type
|
assert boxed_type is box_type
|
||||||
|
|
||||||
# TODO: already included by `_this_mod` in else loop right?
|
# TODO: already included by `_this_mod` in else loop right?
|
||||||
|
@ -1029,19 +1085,21 @@ def unpack_error(
|
||||||
exc = box_type(
|
exc = box_type(
|
||||||
message,
|
message,
|
||||||
ipc_msg=msg,
|
ipc_msg=msg,
|
||||||
|
tb_str=msg.tb_str,
|
||||||
)
|
)
|
||||||
|
|
||||||
return exc
|
return exc
|
||||||
|
|
||||||
|
|
||||||
def is_multi_cancelled(exc: BaseException) -> bool:
|
def is_multi_cancelled(
|
||||||
|
exc: BaseException|BaseExceptionGroup
|
||||||
|
) -> bool:
|
||||||
'''
|
'''
|
||||||
Predicate to determine if a possible ``BaseExceptionGroup`` contains
|
Predicate to determine if a possible ``BaseExceptionGroup`` contains
|
||||||
only ``trio.Cancelled`` sub-exceptions (and is likely the result of
|
only ``trio.Cancelled`` sub-exceptions (and is likely the result of
|
||||||
cancelling a collection of subtasks.
|
cancelling a collection of subtasks.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# if isinstance(exc, eg.BaseExceptionGroup):
|
|
||||||
if isinstance(exc, BaseExceptionGroup):
|
if isinstance(exc, BaseExceptionGroup):
|
||||||
return exc.subgroup(
|
return exc.subgroup(
|
||||||
lambda exc: isinstance(exc, trio.Cancelled)
|
lambda exc: isinstance(exc, trio.Cancelled)
|
||||||
|
@ -1184,7 +1242,6 @@ def _mk_msg_type_err(
|
||||||
src_validation_error: ValidationError|None = None,
|
src_validation_error: ValidationError|None = None,
|
||||||
src_type_error: TypeError|None = None,
|
src_type_error: TypeError|None = None,
|
||||||
is_invalid_payload: bool = False,
|
is_invalid_payload: bool = False,
|
||||||
# src_err_msg: Error|None = None,
|
|
||||||
|
|
||||||
**mte_kwargs,
|
**mte_kwargs,
|
||||||
|
|
||||||
|
@ -1251,19 +1308,11 @@ def _mk_msg_type_err(
|
||||||
msg_type: str = type(msg)
|
msg_type: str = type(msg)
|
||||||
any_pld: Any = msgpack.decode(msg.pld)
|
any_pld: Any = msgpack.decode(msg.pld)
|
||||||
message: str = (
|
message: str = (
|
||||||
f'invalid `{msg_type.__qualname__}` payload\n\n'
|
f'invalid `{msg_type.__qualname__}` msg payload\n\n'
|
||||||
f'value: `{any_pld!r}` does not match type-spec: ' #\n'
|
f'value: `{any_pld!r}` does not match type-spec: '
|
||||||
f'`{type(msg).__qualname__}.pld: {codec.pld_spec_str}`'
|
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'
|
|
||||||
)
|
)
|
||||||
# src_err_msg = msg
|
|
||||||
bad_msg = msg
|
bad_msg = msg
|
||||||
# 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?
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# decode the msg-bytes using the std msgpack
|
# decode the msg-bytes using the std msgpack
|
||||||
|
@ -1308,21 +1357,21 @@ def _mk_msg_type_err(
|
||||||
if verb_header:
|
if verb_header:
|
||||||
message = f'{verb_header} ' + message
|
message = f'{verb_header} ' + message
|
||||||
|
|
||||||
# if not isinstance(bad_msg, PayloadMsg):
|
|
||||||
# import pdbp; pdbp.set_trace()
|
|
||||||
|
|
||||||
msgtyperr = MsgTypeError.from_decode(
|
msgtyperr = MsgTypeError.from_decode(
|
||||||
message=message,
|
message=message,
|
||||||
bad_msg=bad_msg,
|
bad_msg=bad_msg,
|
||||||
bad_msg_as_dict=msg_dict,
|
bad_msg_as_dict=msg_dict,
|
||||||
|
|
||||||
# NOTE: for the send-side `.started()` pld-validate
|
# NOTE: for pld-spec MTEs we set the `._ipc_msg` manually:
|
||||||
# case we actually set the `._ipc_msg` AFTER we return
|
# - for the send-side `.started()` pld-validate
|
||||||
# from here inside `Context.started()` since we actually
|
# case we actually raise inline so we don't need to
|
||||||
# want to emulate the `Error` from the mte we build here
|
# set the it at all.
|
||||||
# Bo
|
# - for recv side we set it inside `PldRx.decode_pld()`
|
||||||
# so by default in that case this is set to `None`
|
# after a manual call to `pack_error()` since we
|
||||||
# ipc_msg=src_err_msg,
|
# actually want to emulate the `Error` from the mte we
|
||||||
|
# build here. So by default in that case, this is left
|
||||||
|
# as `None` here.
|
||||||
|
# ipc_msg=src_err_msg,
|
||||||
)
|
)
|
||||||
msgtyperr.__cause__ = src_validation_error
|
msgtyperr.__cause__ = src_validation_error
|
||||||
return msgtyperr
|
return msgtyperr
|
||||||
|
|
Loading…
Reference in New Issue