diff --git a/tractor/_exceptions.py b/tractor/_exceptions.py index 0b4e8196..1238220e 100644 --- a/tractor/_exceptions.py +++ b/tractor/_exceptions.py @@ -23,7 +23,6 @@ import builtins import importlib from pprint import pformat from pdb import bdb -import sys from types import ( TracebackType, ) @@ -543,7 +542,6 @@ class RemoteActorError(Exception): if val: _repr += f'{key}={val_str}{end_char}' - return _repr def reprol(self) -> str: @@ -622,56 +620,9 @@ class RemoteActorError(Exception): 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 - ) - - # 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( ..` - # ^-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. + body: str = '' if tb_str := self.tb_str: fields: str = self._mk_fields_str( _body_fields @@ -692,21 +643,15 @@ class RemoteActorError(Exception): boxer_header=self.relay_uid, ) - tail = '' - if ( - with_type_header - and not message - ): - tail: str = '>' - - return ( - header - + - message - + - f'{body}' - + - tail + # !TODO, it'd be nice to import these top level without + # cycles! + from tractor.devx.pformat import ( + pformat_exc, + ) + return pformat_exc( + exc=self, + with_type_header=with_type_header, + body=body, ) __repr__ = pformat @@ -984,7 +929,7 @@ class StreamOverrun( ''' -class TransportClosed(trio.BrokenResourceError): +class TransportClosed(Exception): ''' IPC transport (protocol) connection was closed or broke and indicates that the wrapping communication `Channel` can no longer @@ -995,16 +940,21 @@ class TransportClosed(trio.BrokenResourceError): self, message: str, loglevel: str = 'transport', - cause: BaseException|None = None, + src_exc: Exception|None = None, raise_on_report: bool = False, ) -> None: self.message: str = message - self._loglevel = loglevel + self._loglevel: str = loglevel super().__init__(message) - if cause is not None: - self.__cause__ = cause + self.src_exc = src_exc + if ( + src_exc is not None + and + not self.__cause__ + ): + self.__cause__ = src_exc # flag to toggle whether the msg loop should raise # the exc in its `TransportClosed` handler block. @@ -1041,6 +991,26 @@ class TransportClosed(trio.BrokenResourceError): if self._raise_on_report: raise self from cause + def pformat(self) -> str: + from tractor.devx.pformat import ( + pformat_exc, + ) + src_err: Exception|None = self.src_exc or '' + 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): "No final result is expected for this actor" diff --git a/tractor/devx/pformat.py b/tractor/devx/pformat.py index 1530ef02..e04b4fe8 100644 --- a/tractor/devx/pformat.py +++ b/tractor/devx/pformat.py @@ -19,6 +19,7 @@ Pretty formatters for use throughout the code base. Mostly handy for logging and exception message content. ''' +import sys import textwrap 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 + ) + + # 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( ..` + # ^-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( stack_limit: int = 1, box_tb: bool = True, diff --git a/tractor/ipc/_transport.py b/tractor/ipc/_transport.py index 98403c1f..160423c8 100644 --- a/tractor/ipc/_transport.py +++ b/tractor/ipc/_transport.py @@ -208,6 +208,7 @@ class MsgpackTransport(MsgTransport): ''' decodes_failed: int = 0 + tpt_name: str = f'{type(self).__name__!r}' while True: try: header: bytes = await self.recv_stream.receive_exactly(4) @@ -252,10 +253,9 @@ class MsgpackTransport(MsgTransport): raise TransportClosed( message=( - f'IPC transport already closed by peer\n' - f'x)> {type(trans_err)}\n' - f' |_{self}\n' + f'{tpt_name} already closed by peer\n' ), + src_exc=trans_err, loglevel=loglevel, ) from trans_err @@ -267,18 +267,17 @@ class MsgpackTransport(MsgTransport): # # NOTE: as such we always re-raise this error from the # RPC msg loop! - except trio.ClosedResourceError as closure_err: + except trio.ClosedResourceError as cre: + closure_err = cre + raise TransportClosed( message=( - f'IPC transport already manually closed locally?\n' - f'x)> {type(closure_err)} \n' - f' |_{self}\n' + f'{tpt_name} was already closed locally ?\n' ), + src_exc=closure_err, loglevel='error', raise_on_report=( - closure_err.args[0] == 'another task closed this fd' - or - closure_err.args[0] in ['another task closed this fd'] + 'another task closed this fd' in closure_err.args ), ) from closure_err @@ -286,12 +285,9 @@ class MsgpackTransport(MsgTransport): if header == b'': raise TransportClosed( message=( - f'IPC transport already gracefully closed\n' - f')>\n' - f'|_{self}\n' + f'{tpt_name} already gracefully closed\n' ), loglevel='transport', - # cause=??? # handy or no? ) size: int