Reorg frames pformatters, add `Context.repr_state`
A better spot for the pretty-formatting of frame text (and thus tracebacks) is in the new `.devx._code` module: - move from `._exceptions` -> `.devx._code.pformat_boxed_tb()`. - add new `pformat_caller_frame()` factored out the use case in `._exceptions._mk_msg_type_err()` where we dump a stack trace for bad `.send()` side IPC msgs. Add some new pretty-format methods to `Context`: - explicitly implement `.pformat()` and allow an `extra_fields: dict` which can be used to inject additional fields (maybe eventually by default) such as is now used inside `._maybe_cancel_and_set_remote_error()` when reporting the internal `._scope` state in cancel logging. - add a new `.repr_state -> str` which provides a single string status depending on the internal state of the IPC ctx in terms of the shuttle protocol's "phase"; use it from `.pformat()` for the `|_state:`. - set `.started(complain_no_parity=False)` now since we presume decoding with `.pld: Raw` now with the new `PldRx` design. - use new `msgops.current_pldrx()` in `mk_context()`.runtime_to_msgspec
parent
40c972f0ec
commit
88a0e90f82
|
@ -61,7 +61,6 @@ from ._exceptions import (
|
||||||
)
|
)
|
||||||
from .log import get_logger
|
from .log import get_logger
|
||||||
from .msg import (
|
from .msg import (
|
||||||
_codec,
|
|
||||||
Error,
|
Error,
|
||||||
MsgType,
|
MsgType,
|
||||||
MsgCodec,
|
MsgCodec,
|
||||||
|
@ -103,7 +102,6 @@ class Unresolved:
|
||||||
a final return value or raised error is resolved.
|
a final return value or raised error is resolved.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: make this a .msg.types.Struct!
|
# TODO: make this a .msg.types.Struct!
|
||||||
|
@ -116,19 +114,19 @@ class Context:
|
||||||
|
|
||||||
NB: This class should **never be instatiated directly**, it is allocated
|
NB: This class should **never be instatiated directly**, it is allocated
|
||||||
by the runtime in 2 ways:
|
by the runtime in 2 ways:
|
||||||
- by entering ``Portal.open_context()`` which is the primary
|
- by entering `Portal.open_context()` which is the primary
|
||||||
public API for any "caller" task or,
|
public API for any "parent" task or,
|
||||||
- by the RPC machinery's `._rpc._invoke()` as a `ctx` arg
|
- by the RPC machinery's `._rpc._invoke()` as a `ctx` arg
|
||||||
to a remotely scheduled "callee" function.
|
to a remotely scheduled "child" function.
|
||||||
|
|
||||||
AND is always constructed using the below ``mk_context()``.
|
AND is always constructed using the below `mk_context()`.
|
||||||
|
|
||||||
Allows maintaining task or protocol specific state between
|
Allows maintaining task or protocol specific state between
|
||||||
2 cancel-scope-linked, communicating and parallel executing
|
2 cancel-scope-linked, communicating and parallel executing
|
||||||
`trio.Task`s. Contexts are allocated on each side of any task
|
`trio.Task`s. Contexts are allocated on each side of any task
|
||||||
RPC-linked msg dialog, i.e. for every request to a remote
|
RPC-linked msg dialog, i.e. for every request to a remote
|
||||||
actor from a `Portal`. On the "callee" side a context is
|
actor from a `Portal`. On the "callee" side a context is
|
||||||
always allocated inside ``._rpc._invoke()``.
|
always allocated inside `._rpc._invoke()`.
|
||||||
|
|
||||||
TODO: more detailed writeup on cancellation, error and
|
TODO: more detailed writeup on cancellation, error and
|
||||||
streaming semantics..
|
streaming semantics..
|
||||||
|
@ -262,7 +260,13 @@ class Context:
|
||||||
_strict_started: bool = False
|
_strict_started: bool = False
|
||||||
_cancel_on_msgerr: bool = True
|
_cancel_on_msgerr: bool = True
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def pformat(
|
||||||
|
self,
|
||||||
|
extra_fields: dict[str, Any]|None = None,
|
||||||
|
# ^-TODO-^ some built-in extra state fields
|
||||||
|
# we'll want in some devx specific cases?
|
||||||
|
|
||||||
|
) -> str:
|
||||||
ds: str = '='
|
ds: str = '='
|
||||||
# ds: str = ': '
|
# ds: str = ': '
|
||||||
|
|
||||||
|
@ -279,11 +283,7 @@ class Context:
|
||||||
outcome_str: str = self.repr_outcome(
|
outcome_str: str = self.repr_outcome(
|
||||||
show_error_fields=True
|
show_error_fields=True
|
||||||
)
|
)
|
||||||
outcome_typ_str: str = self.repr_outcome(
|
fmtstr: str = (
|
||||||
type_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
f'<Context(\n'
|
f'<Context(\n'
|
||||||
# f'\n'
|
# f'\n'
|
||||||
# f' ---\n'
|
# f' ---\n'
|
||||||
|
@ -304,12 +304,12 @@ class Context:
|
||||||
# f' -----\n'
|
# f' -----\n'
|
||||||
#
|
#
|
||||||
# TODO: better state `str`ids?
|
# TODO: better state `str`ids?
|
||||||
# -[ ] maybe map err-types to strs like 'cancelled',
|
# -[x] maybe map err-types to strs like 'cancelled',
|
||||||
# 'errored', 'streaming', 'started', .. etc.
|
# 'errored', 'streaming', 'started', .. etc.
|
||||||
# -[ ] as well as a final result wrapper like
|
# -[ ] as well as a final result wrapper like
|
||||||
# `outcome.Value`?
|
# `outcome.Value`?
|
||||||
#
|
#
|
||||||
f' |_state: {outcome_typ_str}\n'
|
f' |_state: {self.repr_state!r}\n'
|
||||||
|
|
||||||
f' outcome{ds}{outcome_str}\n'
|
f' outcome{ds}{outcome_str}\n'
|
||||||
f' result{ds}{self._result}\n'
|
f' result{ds}{self._result}\n'
|
||||||
|
@ -324,6 +324,16 @@ class Context:
|
||||||
# -[ ] remove this ^ right?
|
# -[ ] remove this ^ right?
|
||||||
|
|
||||||
# f' _remote_error={self._remote_error}
|
# f' _remote_error={self._remote_error}
|
||||||
|
)
|
||||||
|
if extra_fields:
|
||||||
|
for key, val in extra_fields.items():
|
||||||
|
fmtstr += (
|
||||||
|
f' {key}{ds}{val!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
fmtstr
|
||||||
|
+
|
||||||
')>\n'
|
')>\n'
|
||||||
)
|
)
|
||||||
# NOTE: making this return a value that can be passed to
|
# NOTE: making this return a value that can be passed to
|
||||||
|
@ -335,7 +345,8 @@ class Context:
|
||||||
# logging perspective over `eval()`-ability since we do NOT
|
# logging perspective over `eval()`-ability since we do NOT
|
||||||
# target serializing non-struct instances!
|
# target serializing non-struct instances!
|
||||||
# def __repr__(self) -> str:
|
# def __repr__(self) -> str:
|
||||||
__repr__ = __str__
|
__str__ = pformat
|
||||||
|
__repr__ = pformat
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cancel_called(self) -> bool:
|
def cancel_called(self) -> bool:
|
||||||
|
@ -615,10 +626,10 @@ class Context:
|
||||||
|
|
||||||
whom: str = (
|
whom: str = (
|
||||||
'us' if error.canceller == self._actor.uid
|
'us' if error.canceller == self._actor.uid
|
||||||
else 'peer'
|
else 'a remote peer (not us)'
|
||||||
)
|
)
|
||||||
log.cancel(
|
log.cancel(
|
||||||
f'IPC context cancelled by {whom}!\n\n'
|
f'IPC context was cancelled by {whom}!\n\n'
|
||||||
f'{error}'
|
f'{error}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -626,7 +637,6 @@ class Context:
|
||||||
msgerr = True
|
msgerr = True
|
||||||
log.error(
|
log.error(
|
||||||
f'IPC dialog error due to msg-type caused by {self.peer_side!r} side\n\n'
|
f'IPC dialog error due to msg-type caused by {self.peer_side!r} side\n\n'
|
||||||
|
|
||||||
f'{error}\n'
|
f'{error}\n'
|
||||||
f'{pformat(self)}\n'
|
f'{pformat(self)}\n'
|
||||||
)
|
)
|
||||||
|
@ -696,24 +706,23 @@ class Context:
|
||||||
else:
|
else:
|
||||||
message: str = 'NOT cancelling `Context._scope` !\n\n'
|
message: str = 'NOT cancelling `Context._scope` !\n\n'
|
||||||
|
|
||||||
scope_info: str = 'No `self._scope: CancelScope` was set/used ?'
|
fmt_str: str = 'No `self._scope: CancelScope` was set/used ?'
|
||||||
if cs:
|
if cs:
|
||||||
scope_info: str = (
|
fmt_str: str = self.pformat(
|
||||||
f'self._scope: {cs}\n'
|
extra_fields={
|
||||||
f'|_ .cancel_called: {cs.cancel_called}\n'
|
'._is_self_cancelled()': self._is_self_cancelled(),
|
||||||
f'|_ .cancelled_caught: {cs.cancelled_caught}\n'
|
'._cancel_on_msgerr': self._cancel_on_msgerr,
|
||||||
f'|_ ._cancel_status: {cs._cancel_status}\n\n'
|
|
||||||
|
|
||||||
f'{self}\n'
|
'._scope': cs,
|
||||||
f'|_ ._is_self_cancelled(): {self._is_self_cancelled()}\n'
|
'._scope.cancel_called': cs.cancel_called,
|
||||||
f'|_ ._cancel_on_msgerr: {self._cancel_on_msgerr}\n\n'
|
'._scope.cancelled_caught': cs.cancelled_caught,
|
||||||
|
'._scope._cancel_status': cs._cancel_status,
|
||||||
f'msgerr: {msgerr}\n'
|
}
|
||||||
)
|
)
|
||||||
log.cancel(
|
log.cancel(
|
||||||
message
|
message
|
||||||
+
|
+
|
||||||
f'{scope_info}'
|
fmt_str
|
||||||
)
|
)
|
||||||
# TODO: maybe we should also call `._res_scope.cancel()` if it
|
# TODO: maybe we should also call `._res_scope.cancel()` if it
|
||||||
# exists to support cancelling any drain loop hangs?
|
# exists to support cancelling any drain loop hangs?
|
||||||
|
@ -748,7 +757,7 @@ class Context:
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
# f'{self._nsf}() -{{{codec}}}-> {repr(self.outcome)}:'
|
# f'{self._nsf}() -{{{codec}}}-> {repr(self.outcome)}:'
|
||||||
f'{self._nsf}() -> {outcome_str}:'
|
f'{self._nsf}() -> {outcome_str}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -836,7 +845,7 @@ class Context:
|
||||||
if not self._portal:
|
if not self._portal:
|
||||||
raise InternalError(
|
raise InternalError(
|
||||||
'No portal found!?\n'
|
'No portal found!?\n'
|
||||||
'Why is this supposed caller context missing it?'
|
'Why is this supposed {self.side!r}-side ctx task missing it?!?'
|
||||||
)
|
)
|
||||||
|
|
||||||
cid: str = self.cid
|
cid: str = self.cid
|
||||||
|
@ -1274,11 +1283,11 @@ class Context:
|
||||||
)
|
)
|
||||||
|
|
||||||
log.cancel(
|
log.cancel(
|
||||||
'Ctx drained pre-result msgs:\n'
|
'Ctx drained to final result msgs\n'
|
||||||
f'{pformat(drained_msgs)}\n\n'
|
f'{return_msg}\n\n'
|
||||||
|
|
||||||
f'Final return msg:\n'
|
f'pre-result drained msgs:\n'
|
||||||
f'{return_msg}\n'
|
f'{pformat(drained_msgs)}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
self.maybe_raise(
|
self.maybe_raise(
|
||||||
|
@ -1443,6 +1452,65 @@ class Context:
|
||||||
repr(self._result)
|
repr(self._result)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def repr_state(self) -> str:
|
||||||
|
'''
|
||||||
|
A `str`-status describing the current state of this
|
||||||
|
inter-actor IPC context in terms of the current "phase" state
|
||||||
|
of the SC shuttling dialog protocol.
|
||||||
|
|
||||||
|
'''
|
||||||
|
merr: Exception|None = self.maybe_error
|
||||||
|
outcome: Unresolved|Exception|Any = self.outcome
|
||||||
|
|
||||||
|
match (
|
||||||
|
outcome,
|
||||||
|
merr,
|
||||||
|
):
|
||||||
|
case (
|
||||||
|
Unresolved,
|
||||||
|
ContextCancelled(),
|
||||||
|
) if self.cancel_acked:
|
||||||
|
status = 'self-cancelled'
|
||||||
|
|
||||||
|
case (
|
||||||
|
Unresolved,
|
||||||
|
ContextCancelled(),
|
||||||
|
) if (
|
||||||
|
self.canceller
|
||||||
|
and not self._cancel_called
|
||||||
|
):
|
||||||
|
status = 'peer-cancelled'
|
||||||
|
|
||||||
|
case (
|
||||||
|
Unresolved,
|
||||||
|
BaseException(),
|
||||||
|
) if self.canceller:
|
||||||
|
status = 'errored'
|
||||||
|
|
||||||
|
case (
|
||||||
|
_, # any non-unresolved value
|
||||||
|
None,
|
||||||
|
) if self._final_result_is_set():
|
||||||
|
status = 'returned'
|
||||||
|
|
||||||
|
case (
|
||||||
|
Unresolved, # noqa (weird.. ruff)
|
||||||
|
None,
|
||||||
|
):
|
||||||
|
if stream := self._stream:
|
||||||
|
if stream.closed:
|
||||||
|
status = 'streaming-finished'
|
||||||
|
else:
|
||||||
|
status = 'streaming'
|
||||||
|
elif self._started_called:
|
||||||
|
status = 'started'
|
||||||
|
|
||||||
|
case _:
|
||||||
|
status = 'unknown!?'
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
async def started(
|
async def started(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
|
@ -1451,7 +1519,11 @@ class Context:
|
||||||
value: PayloadT|None = None,
|
value: PayloadT|None = None,
|
||||||
|
|
||||||
strict_parity: bool = False,
|
strict_parity: bool = False,
|
||||||
complain_no_parity: bool = True,
|
|
||||||
|
# TODO: this will always emit now that we do `.pld: Raw`
|
||||||
|
# passthrough.. so maybe just only complain when above strict
|
||||||
|
# flag is set?
|
||||||
|
complain_no_parity: bool = False,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
@ -1511,18 +1583,19 @@ class Context:
|
||||||
)
|
)
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Failed to roundtrip `Started` msg?\n'
|
'Failed to roundtrip `Started` msg?\n'
|
||||||
f'{pformat(rt_started)}\n'
|
f'{pretty_struct.pformat(rt_started)}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
if rt_started != started_msg:
|
if rt_started != started_msg:
|
||||||
# TODO: break these methods out from the struct subtype?
|
# TODO: break these methods out from the struct subtype?
|
||||||
|
|
||||||
|
# TODO: make that one a mod func too..
|
||||||
diff = pretty_struct.Struct.__sub__(
|
diff = pretty_struct.Struct.__sub__(
|
||||||
rt_started,
|
rt_started,
|
||||||
started_msg,
|
started_msg,
|
||||||
)
|
)
|
||||||
complaint: str = (
|
complaint: str = (
|
||||||
'Started value does not match after codec rountrip?\n\n'
|
'Started value does not match after roundtrip?\n\n'
|
||||||
f'{diff}'
|
f'{diff}'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1538,8 +1611,6 @@ class Context:
|
||||||
else:
|
else:
|
||||||
log.warning(complaint)
|
log.warning(complaint)
|
||||||
|
|
||||||
# started_msg = rt_started
|
|
||||||
|
|
||||||
await self.chan.send(started_msg)
|
await self.chan.send(started_msg)
|
||||||
|
|
||||||
# raise any msg type error NO MATTER WHAT!
|
# raise any msg type error NO MATTER WHAT!
|
||||||
|
@ -2354,7 +2425,7 @@ async def open_context_from_portal(
|
||||||
# FINALLY, remove the context from runtime tracking and
|
# FINALLY, remove the context from runtime tracking and
|
||||||
# exit!
|
# exit!
|
||||||
log.runtime(
|
log.runtime(
|
||||||
'De-allocating IPC ctx opened with {ctx.side!r} peer \n'
|
f'De-allocating IPC ctx opened with {ctx.side!r} peer \n'
|
||||||
f'uid: {uid}\n'
|
f'uid: {uid}\n'
|
||||||
f'cid: {ctx.cid}\n'
|
f'cid: {ctx.cid}\n'
|
||||||
)
|
)
|
||||||
|
@ -2390,10 +2461,8 @@ def mk_context(
|
||||||
from .devx._code import find_caller_info
|
from .devx._code import find_caller_info
|
||||||
caller_info: CallerInfo|None = find_caller_info()
|
caller_info: CallerInfo|None = find_caller_info()
|
||||||
|
|
||||||
pld_rx = msgops.PldRx(
|
# TODO: when/how do we apply `.limit_plds()` from here?
|
||||||
# _rx_mc=recv_chan,
|
pld_rx: msgops.PldRx = msgops.current_pldrx()
|
||||||
_msgdec=_codec.mk_dec(spec=pld_spec)
|
|
||||||
)
|
|
||||||
|
|
||||||
ctx = Context(
|
ctx = Context(
|
||||||
chan=chan,
|
chan=chan,
|
||||||
|
|
|
@ -46,7 +46,7 @@ from tractor.msg import (
|
||||||
Error,
|
Error,
|
||||||
MsgType,
|
MsgType,
|
||||||
Stop,
|
Stop,
|
||||||
Yield,
|
# Yield,
|
||||||
types as msgtypes,
|
types as msgtypes,
|
||||||
MsgCodec,
|
MsgCodec,
|
||||||
MsgDec,
|
MsgDec,
|
||||||
|
@ -140,71 +140,6 @@ def get_err_type(type_name: str) -> BaseException|None:
|
||||||
return type_ref
|
return type_ref
|
||||||
|
|
||||||
|
|
||||||
def pformat_boxed_tb(
|
|
||||||
tb_str: str,
|
|
||||||
fields_str: str|None = None,
|
|
||||||
field_prefix: str = ' |_',
|
|
||||||
|
|
||||||
tb_box_indent: int|None = None,
|
|
||||||
tb_body_indent: int = 1,
|
|
||||||
|
|
||||||
) -> str:
|
|
||||||
if (
|
|
||||||
fields_str
|
|
||||||
and
|
|
||||||
field_prefix
|
|
||||||
):
|
|
||||||
fields: str = textwrap.indent(
|
|
||||||
fields_str,
|
|
||||||
prefix=field_prefix,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
fields = fields_str or ''
|
|
||||||
|
|
||||||
tb_body = tb_str
|
|
||||||
if tb_body_indent:
|
|
||||||
tb_body: str = textwrap.indent(
|
|
||||||
tb_str,
|
|
||||||
prefix=tb_body_indent * ' ',
|
|
||||||
)
|
|
||||||
|
|
||||||
tb_box: str = (
|
|
||||||
|
|
||||||
# orig
|
|
||||||
# f' |\n'
|
|
||||||
# f' ------ - ------\n\n'
|
|
||||||
# f'{tb_str}\n'
|
|
||||||
# f' ------ - ------\n'
|
|
||||||
# f' _|\n'
|
|
||||||
|
|
||||||
f'|\n'
|
|
||||||
f' ------ - ------\n\n'
|
|
||||||
# f'{tb_str}\n'
|
|
||||||
f'{tb_body}'
|
|
||||||
f' ------ - ------\n'
|
|
||||||
f'_|\n'
|
|
||||||
)
|
|
||||||
tb_box_indent: str = (
|
|
||||||
tb_box_indent
|
|
||||||
or
|
|
||||||
1
|
|
||||||
|
|
||||||
# (len(field_prefix))
|
|
||||||
# ? ^-TODO-^ ? if you wanted another indent level
|
|
||||||
)
|
|
||||||
if tb_box_indent > 0:
|
|
||||||
tb_box: str = textwrap.indent(
|
|
||||||
tb_box,
|
|
||||||
prefix=tb_box_indent * ' ',
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
fields
|
|
||||||
+
|
|
||||||
tb_box
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pack_from_raise(
|
def pack_from_raise(
|
||||||
local_err: (
|
local_err: (
|
||||||
ContextCancelled
|
ContextCancelled
|
||||||
|
@ -504,12 +439,15 @@ class RemoteActorError(Exception):
|
||||||
reprol_str: str = (
|
reprol_str: str = (
|
||||||
f'{type(self).__name__}' # type name
|
f'{type(self).__name__}' # type name
|
||||||
f'[{self.boxed_type_str}]' # parameterized by boxed type
|
f'[{self.boxed_type_str}]' # parameterized by boxed type
|
||||||
'(' # init-style look
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_repr: str = self._mk_fields_str(
|
_repr: str = self._mk_fields_str(
|
||||||
self.reprol_fields,
|
self.reprol_fields,
|
||||||
end_char=' ',
|
end_char=' ',
|
||||||
)
|
)
|
||||||
|
if _repr:
|
||||||
|
reprol_str += '(' # init-style call
|
||||||
|
|
||||||
return (
|
return (
|
||||||
reprol_str
|
reprol_str
|
||||||
+
|
+
|
||||||
|
@ -521,6 +459,7 @@ class RemoteActorError(Exception):
|
||||||
Nicely formatted boxed error meta data + traceback.
|
Nicely formatted boxed error meta data + traceback.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from tractor.devx._code import pformat_boxed_tb
|
||||||
fields: str = self._mk_fields_str(
|
fields: str = self._mk_fields_str(
|
||||||
_body_fields
|
_body_fields
|
||||||
+
|
+
|
||||||
|
@ -1092,14 +1031,10 @@ def _mk_msg_type_err(
|
||||||
# no src error from `msgspec.msgpack.Decoder.decode()` so
|
# no src error from `msgspec.msgpack.Decoder.decode()` so
|
||||||
# prolly a manual type-check on our part.
|
# prolly a manual type-check on our part.
|
||||||
if message is None:
|
if message is None:
|
||||||
fmt_stack: str = (
|
from tractor.devx._code import (
|
||||||
'\n'.join(traceback.format_stack(limit=3))
|
pformat_caller_frame,
|
||||||
)
|
|
||||||
tb_fmt: str = pformat_boxed_tb(
|
|
||||||
tb_str=fmt_stack,
|
|
||||||
field_prefix=' ',
|
|
||||||
indent='',
|
|
||||||
)
|
)
|
||||||
|
tb_fmt: str = pformat_caller_frame(stack_limit=3)
|
||||||
message: str = (
|
message: str = (
|
||||||
f'invalid msg -> {msg}: {type(msg)}\n\n'
|
f'invalid msg -> {msg}: {type(msg)}\n\n'
|
||||||
f'{tb_fmt}\n'
|
f'{tb_fmt}\n'
|
||||||
|
|
|
@ -23,6 +23,8 @@ from __future__ import annotations
|
||||||
import inspect
|
import inspect
|
||||||
# import msgspec
|
# import msgspec
|
||||||
# from pprint import pformat
|
# from pprint import pformat
|
||||||
|
import textwrap
|
||||||
|
import traceback
|
||||||
from types import (
|
from types import (
|
||||||
FrameType,
|
FrameType,
|
||||||
FunctionType,
|
FunctionType,
|
||||||
|
@ -175,3 +177,103 @@ def find_caller_info(
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def pformat_boxed_tb(
|
||||||
|
tb_str: str,
|
||||||
|
fields_str: str|None = None,
|
||||||
|
field_prefix: str = ' |_',
|
||||||
|
|
||||||
|
tb_box_indent: int|None = None,
|
||||||
|
tb_body_indent: int = 1,
|
||||||
|
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Create a "boxed" looking traceback string.
|
||||||
|
|
||||||
|
Useful for emphasizing traceback text content as being an
|
||||||
|
embedded attribute of some other object (like
|
||||||
|
a `RemoteActorError` or other boxing remote error shuttle
|
||||||
|
container).
|
||||||
|
|
||||||
|
Any other parent/container "fields" can be passed in the
|
||||||
|
`fields_str` input along with other prefix/indent settings.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if (
|
||||||
|
fields_str
|
||||||
|
and
|
||||||
|
field_prefix
|
||||||
|
):
|
||||||
|
fields: str = textwrap.indent(
|
||||||
|
fields_str,
|
||||||
|
prefix=field_prefix,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
fields = fields_str or ''
|
||||||
|
|
||||||
|
tb_body = tb_str
|
||||||
|
if tb_body_indent:
|
||||||
|
tb_body: str = textwrap.indent(
|
||||||
|
tb_str,
|
||||||
|
prefix=tb_body_indent * ' ',
|
||||||
|
)
|
||||||
|
|
||||||
|
tb_box: str = (
|
||||||
|
|
||||||
|
# orig
|
||||||
|
# f' |\n'
|
||||||
|
# f' ------ - ------\n\n'
|
||||||
|
# f'{tb_str}\n'
|
||||||
|
# f' ------ - ------\n'
|
||||||
|
# f' _|\n'
|
||||||
|
|
||||||
|
f'|\n'
|
||||||
|
f' ------ - ------\n\n'
|
||||||
|
# f'{tb_str}\n'
|
||||||
|
f'{tb_body}'
|
||||||
|
f' ------ - ------\n'
|
||||||
|
f'_|\n'
|
||||||
|
)
|
||||||
|
tb_box_indent: str = (
|
||||||
|
tb_box_indent
|
||||||
|
or
|
||||||
|
1
|
||||||
|
|
||||||
|
# (len(field_prefix))
|
||||||
|
# ? ^-TODO-^ ? if you wanted another indent level
|
||||||
|
)
|
||||||
|
if tb_box_indent > 0:
|
||||||
|
tb_box: str = textwrap.indent(
|
||||||
|
tb_box,
|
||||||
|
prefix=tb_box_indent * ' ',
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
fields
|
||||||
|
+
|
||||||
|
tb_box
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pformat_caller_frame(
|
||||||
|
stack_limit: int = 1,
|
||||||
|
box_tb: bool = True,
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Capture and return the traceback text content from
|
||||||
|
`stack_limit` call frames up.
|
||||||
|
|
||||||
|
'''
|
||||||
|
tb_str: str = (
|
||||||
|
'\n'.join(
|
||||||
|
traceback.format_stack(limit=stack_limit)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if box_tb:
|
||||||
|
tb_str: str = pformat_boxed_tb(
|
||||||
|
tb_str=tb_str,
|
||||||
|
field_prefix=' ',
|
||||||
|
indent='',
|
||||||
|
)
|
||||||
|
return tb_str
|
||||||
|
|
Loading…
Reference in New Issue