Improve `TransportClosed.__repr__()`, add `src_exc`

By borrowing from the implementation of `RemoteActorError.pformat()`
which is now factored into a new `.devx.pformat_exc()` and re-used for
both error types while maintaining the same func-sig. Obviously delegate
`RemoteActorError.pformat()` to the new helper accordingly and keeping
the prior `body` generation from `.devx.pformat_boxed_tb()` as before.

The new helper allows for,
- passing any of a `header|message|body: str` which are all combined in
  that order in the final output.
- getting the `exc.message` as the default `message` part.
- generating an objecty-looking "type-name" header to be rendered by
  default when `header` is not overridden.
- "first-line-of `message`" processing which we split-off and then
  re-inject as a `f'<{type(exc).__name__}( {first} )>'` top line header.
- an optional `tail: str = '>'` to "close the object"-look only added
  when `with_type_header: bool = True`.

Adjustments to `TransportClosed` around this include,
- replacing the init `cause` arg for a `src_exc` which is now always
  assigned to a same named instance var.
- displaying that new `.src_exc` in the `body: str` arg to the
  `.devx.pformat.pformat_exc()` call so you can always see the
  underlying (normally `trio`) source error.
- just make it inherit from `Exception` not `trio.BrokenResourceError`
  to avoid handlers catching `TransportClosed` as the former
  particularly in testing when we want to sometimes to distinguish them.
structural_dynamics_of_flow
Tyler Goodlet 2025-04-06 13:54:10 -04:00
parent 692bd0edf6
commit 74df5034c0
3 changed files with 130 additions and 84 deletions

View File

@ -23,7 +23,6 @@ import builtins
import importlib import importlib
from pprint import pformat from pprint import pformat
from pdb import bdb from pdb import bdb
import sys
from types import ( from types import (
TracebackType, TracebackType,
) )
@ -543,7 +542,6 @@ class RemoteActorError(Exception):
if val: if val:
_repr += f'{key}={val_str}{end_char}' _repr += f'{key}={val_str}{end_char}'
return _repr return _repr
def reprol(self) -> str: def reprol(self) -> str:
@ -622,56 +620,9 @@ class RemoteActorError(Exception):
the type name is already implicitly shown by python). the type name is already implicitly shown by python).
''' '''
header: 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:
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 # IFF there is an embedded traceback-str we always
# draw the ascii-box around it. # draw the ascii-box around it.
body: str = ''
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
@ -692,21 +643,15 @@ class RemoteActorError(Exception):
boxer_header=self.relay_uid, boxer_header=self.relay_uid,
) )
tail = '' # !TODO, it'd be nice to import these top level without
if ( # cycles!
with_type_header from tractor.devx.pformat import (
and not message pformat_exc,
): )
tail: str = '>' return pformat_exc(
exc=self,
return ( with_type_header=with_type_header,
header body=body,
+
message
+
f'{body}'
+
tail
) )
__repr__ = pformat __repr__ = pformat
@ -984,7 +929,7 @@ class StreamOverrun(
''' '''
class TransportClosed(trio.BrokenResourceError): class TransportClosed(Exception):
''' '''
IPC transport (protocol) connection was closed or broke and IPC transport (protocol) connection was closed or broke and
indicates that the wrapping communication `Channel` can no longer indicates that the wrapping communication `Channel` can no longer
@ -995,16 +940,21 @@ class TransportClosed(trio.BrokenResourceError):
self, self,
message: str, message: str,
loglevel: str = 'transport', loglevel: str = 'transport',
cause: BaseException|None = None, src_exc: Exception|None = None,
raise_on_report: bool = False, raise_on_report: bool = False,
) -> None: ) -> None:
self.message: str = message self.message: str = message
self._loglevel = loglevel self._loglevel: str = loglevel
super().__init__(message) super().__init__(message)
if cause is not None: self.src_exc = src_exc
self.__cause__ = cause if (
src_exc is not None
and
not self.__cause__
):
self.__cause__ = src_exc
# flag to toggle whether the msg loop should raise # flag to toggle whether the msg loop should raise
# the exc in its `TransportClosed` handler block. # the exc in its `TransportClosed` handler block.
@ -1041,6 +991,26 @@ class TransportClosed(trio.BrokenResourceError):
if self._raise_on_report: if self._raise_on_report:
raise self from cause raise self from cause
def pformat(self) -> str:
from tractor.devx.pformat import (
pformat_exc,
)
src_err: Exception|None = self.src_exc or '<unknown>'
src_msg: tuple[str] = src_err.args
src_exc_repr: str = (
f'{type(src_err).__name__}[ {src_msg} ]'
)
return pformat_exc(
exc=self,
# message=self.message, # implicit!
body=(
f' |_src_exc: {src_exc_repr}\n'
),
)
# delegate to `str`-ified pformat
__repr__ = pformat
class NoResult(RuntimeError): class NoResult(RuntimeError):
"No final result is expected for this actor" "No final result is expected for this actor"

View File

@ -19,6 +19,7 @@ Pretty formatters for use throughout the code base.
Mostly handy for logging and exception message content. Mostly handy for logging and exception message content.
''' '''
import sys
import textwrap import textwrap
import traceback import traceback
@ -115,6 +116,85 @@ def pformat_boxed_tb(
) )
def pformat_exc(
exc: Exception,
header: str = '',
message: str = '',
body: str = '',
with_type_header: bool = True,
) -> 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 (
(curr_exc := sys.exception())
and
curr_exc is exc
):
is_being_raised: bool = True
with_type_header: bool = (
with_type_header
and
not is_being_raised
)
# <RemoteActorError( .. )> style
if (
with_type_header
and
not header
):
header: str = f'<{type(exc).__name__}('
message: str = (
message
or
exc.message
)
if 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
tail: str = ''
if (
with_type_header
and
not message
):
tail: str = '>'
return (
header
+
message
+
f'{body}'
+
tail
)
def pformat_caller_frame( def pformat_caller_frame(
stack_limit: int = 1, stack_limit: int = 1,
box_tb: bool = True, box_tb: bool = True,

View File

@ -208,6 +208,7 @@ class MsgpackTransport(MsgTransport):
''' '''
decodes_failed: int = 0 decodes_failed: int = 0
tpt_name: str = f'{type(self).__name__!r}'
while True: while True:
try: try:
header: bytes = await self.recv_stream.receive_exactly(4) header: bytes = await self.recv_stream.receive_exactly(4)
@ -252,10 +253,9 @@ class MsgpackTransport(MsgTransport):
raise TransportClosed( raise TransportClosed(
message=( message=(
f'IPC transport already closed by peer\n' f'{tpt_name} already closed by peer\n'
f'x)> {type(trans_err)}\n'
f' |_{self}\n'
), ),
src_exc=trans_err,
loglevel=loglevel, loglevel=loglevel,
) from trans_err ) from trans_err
@ -267,18 +267,17 @@ class MsgpackTransport(MsgTransport):
# #
# NOTE: as such we always re-raise this error from the # NOTE: as such we always re-raise this error from the
# RPC msg loop! # RPC msg loop!
except trio.ClosedResourceError as closure_err: except trio.ClosedResourceError as cre:
closure_err = cre
raise TransportClosed( raise TransportClosed(
message=( message=(
f'IPC transport already manually closed locally?\n' f'{tpt_name} was already closed locally ?\n'
f'x)> {type(closure_err)} \n'
f' |_{self}\n'
), ),
src_exc=closure_err,
loglevel='error', loglevel='error',
raise_on_report=( raise_on_report=(
closure_err.args[0] == 'another task closed this fd' 'another task closed this fd' in closure_err.args
or
closure_err.args[0] in ['another task closed this fd']
), ),
) from closure_err ) from closure_err
@ -286,12 +285,9 @@ class MsgpackTransport(MsgTransport):
if header == b'': if header == b'':
raise TransportClosed( raise TransportClosed(
message=( message=(
f'IPC transport already gracefully closed\n' f'{tpt_name} already gracefully closed\n'
f')>\n'
f'|_{self}\n'
), ),
loglevel='transport', loglevel='transport',
# cause=??? # handy or no?
) )
size: int size: int