diff --git a/tractor/_entry.py b/tractor/_entry.py index 0f6a91c7..8af05b66 100644 --- a/tractor/_entry.py +++ b/tractor/_entry.py @@ -22,7 +22,6 @@ from __future__ import annotations from functools import partial import multiprocessing as mp import os -import textwrap from typing import ( Any, TYPE_CHECKING, @@ -35,7 +34,10 @@ from .log import ( get_logger, ) from . import _state -from .devx import _debug +from .devx import ( + _debug, + pformat, +) from .to_asyncio import run_as_asyncio_guest from ._addr import UnwrappedAddress from ._runtime import ( @@ -103,107 +105,6 @@ def _mp_main( ) -# TODO: move this func to some kinda `.devx._conc_lang.py` eventually -# as we work out our multi-domain state-flow-syntax! -def nest_from_op( - input_op: str, - # - # ?TODO? an idea for a syntax to the state of concurrent systems - # as a "3-domain" (execution, scope, storage) model and using - # a minimal ascii/utf-8 operator-set. - # - # try not to take any of this seriously yet XD - # - # > is a "play operator" indicating (CPU bound) - # exec/work/ops required at the "lowest level computing" - # - # execution primititves (tasks, threads, actors..) denote their - # lifetime with '(' and ')' since parentheses normally are used - # in many langs to denote function calls. - # - # starting = ( - # >( opening/starting; beginning of the thread-of-exec (toe?) - # (> opened/started, (finished spawning toe) - # |_ repr of toe, in py these look like - # - # >) closing/exiting/stopping, - # )> closed/exited/stopped, - # |_ - # [OR <), )< ?? ] - # - # ending = ) - # >c) cancelling to close/exit - # c)> cancelled (caused close), OR? - # |_ - # OR maybe "x) erroring to eventuall exit - # x)> errored and terminated - # |_ - # - # scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc. - # >{ opening - # {> opened - # }> closed - # >} closing - # - # storage: like queues, shm-buffers, files, etc.. - # >[ opening - # [> opened - # |_ - # - # >] closing - # ]> closed - - # IPC ops: channels, transports, msging - # => req msg - # <= resp msg - # <=> 2-way streaming (of msgs) - # <- recv 1 msg - # -> send 1 msg - # - # TODO: still not sure on R/L-HS approach..? - # =>( send-req to exec start (task, actor, thread..) - # (<= recv-req to ^ - # - # (<= recv-req ^ - # <=( recv-resp opened remote exec primitive - # <=) recv-resp closed - # - # )<=c req to stop due to cancel - # c=>) req to stop due to cancel - # - # =>{ recv-req to open - # <={ send-status that it closed - - tree_str: str, - - # NOTE: so move back-from-the-left of the `input_op` by - # this amount. - back_from_op: int = 0, -) -> str: - ''' - Depth-increment the input (presumably hierarchy/supervision) - input "tree string" below the provided `input_op` execution - operator, so injecting a `"\n|_{input_op}\n"`and indenting the - `tree_str` to nest content aligned with the ops last char. - - ''' - return ( - f'{input_op}\n' - + - textwrap.indent( - tree_str, - prefix=( - len(input_op) - - - (back_from_op + 1) - ) * ' ', - ) - ) - - def _trio_main( actor: Actor, *, @@ -234,22 +135,22 @@ def _trio_main( f' loglevel: {actor.loglevel}\n' ) log.info( - 'Starting new `trio` subactor:\n' + 'Starting new `trio` subactor\n' + - nest_from_op( + pformat.nest_from_op( input_op='>(', # see syntax ideas above - tree_str=actor_info, - back_from_op=2, # since "complete" + text=actor_info, + nest_indent=2, # since "complete" ) ) logmeth = log.info exit_status: str = ( 'Subactor exited\n' + - nest_from_op( + pformat.nest_from_op( input_op=')>', # like a "closed-to-play"-icon from super perspective - tree_str=actor_info, - back_from_op=1, + text=actor_info, + nest_indent=1, ) ) try: @@ -264,9 +165,9 @@ def _trio_main( exit_status: str = ( 'Actor received KBI (aka an OS-cancel)\n' + - nest_from_op( + pformat.nest_from_op( input_op='c)>', # closed due to cancel (see above) - tree_str=actor_info, + text=actor_info, ) ) except BaseException as err: @@ -274,9 +175,9 @@ def _trio_main( exit_status: str = ( 'Main actor task exited due to crash?\n' + - nest_from_op( + pformat.nest_from_op( input_op='x)>', # closed by error - tree_str=actor_info, + text=actor_info, ) ) # NOTE since we raise a tb will already be shown on the diff --git a/tractor/devx/pformat.py b/tractor/devx/pformat.py index e04b4fe8..38b942ff 100644 --- a/tractor/devx/pformat.py +++ b/tractor/devx/pformat.py @@ -15,8 +15,10 @@ # along with this program. If not, see . ''' -Pretty formatters for use throughout the code base. -Mostly handy for logging and exception message content. +Pretty formatters for use throughout our internals. + +Handy for logging and exception message content but also for `repr()` +in REPL(s). ''' import sys @@ -224,8 +226,8 @@ def pformat_cs( field_prefix: str = ' |_', ) -> str: ''' - Pretty format info about a `trio.CancelScope` including most - of its public state and `._cancel_status`. + Pretty format info about a `trio.CancelScope` including most of + its public state and `._cancel_status`. The output can be modified to show a "var name" for the instance as a field prefix, just a simple str before each @@ -247,3 +249,279 @@ def pformat_cs( + fields ) + + +def nest_from_op( + input_op: str, # TODO, Literal of all op-"symbols" from below? + text: str, + prefix_op: bool = True, # unset is to suffix the first line + # optionally suffix `text`, by def on a newline + op_suffix='\n', + + nest_prefix: str = '|_', + nest_indent: int|None = None, + # XXX indent `next_prefix` "to-the-right-of" `input_op` + # by this count of whitespaces (' '). + rm_from_first_ln: str|None = None, + +) -> str: + ''' + Depth-increment the input (presumably hierarchy/supervision) + input "tree string" below the provided `input_op` execution + operator, so injecting a `"\n|_{input_op}\n"`and indenting the + `tree_str` to nest content aligned with the ops last char. + + ''' + # `sclang` "structurred-concurrency-language": an ascii-encoded + # symbolic alphabet to describe concurrent systems. + # + # ?TODO? aa more fomal idea for a syntax to the state of + # concurrent systems as a "3-domain" (execution, scope, storage) + # model and using a minimal ascii/utf-8 operator-set. + # + # try not to take any of this seriously yet XD + # + # > is a "play operator" indicating (CPU bound) + # exec/work/ops required at the "lowest level computing" + # + # execution primititves (tasks, threads, actors..) denote their + # lifetime with '(' and ')' since parentheses normally are used + # in many langs to denote function calls. + # + # starting = ( + # >( opening/starting; beginning of the thread-of-exec (toe?) + # (> opened/started, (finished spawning toe) + # |_ repr of toe, in py these look like + # + # >) closing/exiting/stopping, + # )> closed/exited/stopped, + # |_ + # [OR <), )< ?? ] + # + # ending = ) + # >c) cancelling to close/exit + # c)> cancelled (caused close), OR? + # |_ + # OR maybe "x) erroring to eventuall exit + # x)> errored and terminated + # |_ + # + # scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc. + # >{ opening + # {> opened + # }> closed + # >} closing + # + # storage: like queues, shm-buffers, files, etc.. + # >[ opening + # [> opened + # |_ + # + # >] closing + # ]> closed + + # IPC ops: channels, transports, msging + # => req msg + # <= resp msg + # <=> 2-way streaming (of msgs) + # <- recv 1 msg + # -> send 1 msg + # + # TODO: still not sure on R/L-HS approach..? + # =>( send-req to exec start (task, actor, thread..) + # (<= recv-req to ^ + # + # (<= recv-req ^ + # <=( recv-resp opened remote exec primitive + # <=) recv-resp closed + # + # )<=c req to stop due to cancel + # c=>) req to stop due to cancel + # + # =>{ recv-req to open + # <={ send-status that it closed + # + if ( + nest_prefix + and + nest_indent != 0 + ): + if nest_indent is not None: + nest_prefix: str = textwrap.indent( + nest_prefix, + prefix=nest_indent*' ', + ) + nest_indent: int = len(nest_prefix) + + # determine body-text indent either by, + # - using wtv explicit indent value is provided, + # OR + # - auto-calcing the indent to embed `text` under + # the `nest_prefix` if provided, **IFF** `nest_indent=None`. + tree_str_indent: int = 0 + if nest_indent not in {0, None}: + tree_str_indent = nest_indent + elif ( + nest_prefix + and + nest_indent != 0 + ): + tree_str_indent = len(nest_prefix) + + indented_tree_str: str = text + if tree_str_indent: + indented_tree_str: str = textwrap.indent( + text, + prefix=' '*tree_str_indent, + ) + + # inject any provided nesting-prefix chars + # into the head of the first line. + if nest_prefix: + indented_tree_str: str = ( + f'{nest_prefix}{indented_tree_str[tree_str_indent:]}' + ) + + if ( + not prefix_op + or + rm_from_first_ln + ): + tree_lns: list[str] = indented_tree_str.splitlines() + first: str = tree_lns[0] + if rm_from_first_ln: + first = first.strip().replace( + rm_from_first_ln, + '', + ) + indented_tree_str: str = '\n'.join(tree_lns[1:]) + + if prefix_op: + indented_tree_str = ( + f'{first}\n' + f'{indented_tree_str}' + ) + + if prefix_op: + return ( + f'{input_op}{op_suffix}' + f'{indented_tree_str}' + ) + else: + return ( + f'{first}{input_op}{op_suffix}' + f'{indented_tree_str}' + ) + + +# ------ modden.repr ------ +# XXX originally taken verbaatim from `modden.repr` +''' +More "multi-line" representation then the stdlib's `pprint` equivs. + +''' +from inspect import ( + FrameInfo, + stack, +) +import pprint +import reprlib +from typing import ( + Callable, +) + + +def mk_repr( + **repr_kws, +) -> Callable[[str], str]: + ''' + Allocate and deliver a `repr.Repr` instance with provided input + settings using the std-lib's `reprlib` mod, + * https://docs.python.org/3/library/reprlib.html + + ------ Ex. ------ + An up to 6-layer-nested `dict` as multi-line: + - https://stackoverflow.com/a/79102479 + - https://docs.python.org/3/library/reprlib.html#reprlib.Repr.maxlevel + + ''' + def_kws: dict[str, int] = dict( + indent=3, # indent used for repr of recursive objects + maxlevel=616, # recursion levels + maxdict=616, # max items shown for `dict` + maxlist=616, # max items shown for `dict` + maxstring=616, # match editor line-len limit + maxtuple=616, # match editor line-len limit + maxother=616, # match editor line-len limit + ) + def_kws |= repr_kws + reprr = reprlib.Repr(**def_kws) + return reprr.repr + + +def ppfmt( + obj: object, + do_print: bool = False, +) -> str: + ''' + The `pprint.pformat()` version of `pprint.pp()`, namely + a default `sort_dicts=False`.. (which i think should be + the normal default in the stdlib). + + ''' + pprepr: Callable = mk_repr() + repr_str: str = pprepr(obj) + + if do_print: + return pprint.pp(repr_str) + + return repr_str + + +pformat = ppfmt + + +def pfmt_frame_info(fi: FrameInfo) -> str: + ''' + Like a std `inspect.FrameInfo.__repr__()` but multi-line.. + + ''' + return ( + 'FrameInfo(\n' + ' frame={!r},\n' + ' filename={!r},\n' + ' lineno={!r},\n' + ' function={!r},\n' + ' code_context={!r},\n' + ' index={!r},\n' + ' positions={!r})' + ).format( + fi.frame, + fi.filename, + fi.lineno, + fi.function, + fi.code_context, + fi.index, + fi.positions + ) + + +def pfmt_callstack(frames: int = 1) -> str: + ''' + Generate a string of nested `inspect.FrameInfo` objects returned + from a `inspect.stack()` call such that only the `.frame` field + for each layer is pprinted. + + ''' + caller_frames: list[FrameInfo] = stack()[1:1+frames] + frames_str: str = '' + for i, frame_info in enumerate(caller_frames): + frames_str += textwrap.indent( + f'{frame_info.frame!r}\n', + prefix=' '*i, + + ) + return frames_str diff --git a/tractor/log.py b/tractor/log.py index 48b5cbd4..393c9571 100644 --- a/tractor/log.py +++ b/tractor/log.py @@ -270,7 +270,9 @@ def get_logger( subsys_spec: str|None = None, ) -> StackLevelAdapter: - '''Return the package log or a sub-logger for ``name`` if provided. + ''' + Return the `tractor`-library root logger or a sub-logger for + `name` if provided. ''' log: Logger @@ -282,7 +284,7 @@ def get_logger( name != _proj_name ): - # NOTE: for handling for modules that use ``get_logger(__name__)`` + # NOTE: for handling for modules that use `get_logger(__name__)` # we make the following stylistic choice: # - always avoid duplicate project-package token # in msg output: i.e. tractor.tractor.ipc._chan.py in header @@ -331,7 +333,7 @@ def get_logger( def get_console_log( level: str|None = None, - logger: Logger|None = None, + logger: Logger|StackLevelAdapter|None = None, **kwargs, ) -> LoggerAdapter: @@ -344,12 +346,23 @@ def get_console_log( Yeah yeah, i know we can use `logging.config.dictConfig()`. You do it. ''' - log = get_logger( - logger=logger, - **kwargs - ) # set a root logger - logger: Logger = log.logger + # get/create a stack-aware-adapter + if ( + logger + and + isinstance(logger, StackLevelAdapter) + ): + # XXX, for ex. when passed in by a caller wrapping some + # other lib's logger instance with our level-adapter. + log = logger + else: + log: StackLevelAdapter = get_logger( + logger=logger, + **kwargs + ) + + logger: Logger|StackLevelAdapter = log.logger if not level: return log @@ -367,10 +380,7 @@ def get_console_log( None, ) ): - fmt = LOG_FORMAT - # if logger: - # fmt = None - + fmt: str = LOG_FORMAT # always apply our format? handler = StreamHandler() formatter = colorlog.ColoredFormatter( fmt=fmt, diff --git a/tractor/msg/pretty_struct.py b/tractor/msg/pretty_struct.py index 91eba8bd..169cb461 100644 --- a/tractor/msg/pretty_struct.py +++ b/tractor/msg/pretty_struct.py @@ -20,6 +20,7 @@ Prettified version of `msgspec.Struct` for easier console grokin. ''' from __future__ import annotations from collections import UserList +import textwrap from typing import ( Any, Iterator, @@ -105,27 +106,11 @@ def iter_fields(struct: Struct) -> Iterator[ ) -def pformat( +def iter_struct_ppfmt_lines( struct: Struct, - field_indent: int = 2, - indent: int = 0, + field_indent: int = 0, +) -> Iterator[tuple[str, str]]: -) -> str: - ''' - Recursion-safe `pprint.pformat()` style formatting of - a `msgspec.Struct` for sane reading by a human using a REPL. - - ''' - # global whitespace indent - ws: str = ' '*indent - - # field whitespace indent - field_ws: str = ' '*(field_indent + indent) - - # qtn: str = ws + struct.__class__.__qualname__ - qtn: str = struct.__class__.__qualname__ - - obj_str: str = '' # accumulator fi: structs.FieldInfo k: str v: Any @@ -135,15 +120,18 @@ def pformat( # ..]` over .__name__ == `Literal` but still get only the # latter for simple types like `str | int | None` etc..? ft: type = fi.type - typ_name: str = getattr(ft, '__name__', str(ft)) + typ_name: str = getattr( + ft, + '__name__', + str(ft) + ).replace(' ', '') # recurse to get sub-struct's `.pformat()` output Bo if isinstance(v, Struct): - val_str: str = v.pformat( - indent=field_indent + indent, - field_indent=indent + field_indent, + yield from iter_struct_ppfmt_lines( + struct=v, + field_indent=field_indent+field_indent, ) - else: val_str: str = repr(v) @@ -161,8 +149,39 @@ def pformat( # raise # return _Struct.__repr__(struct) - # TODO: LOLOL use `textwrap.indent()` instead dawwwwwg! - obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n') + yield ( + ' '*field_indent, # indented ws prefix + f'{k}: {typ_name} = {val_str},', # field's repr line content + ) + + +def pformat( + struct: Struct, + field_indent: int = 2, + indent: int = 0, +) -> str: + ''' + Recursion-safe `pprint.pformat()` style formatting of + a `msgspec.Struct` for sane reading by a human using a REPL. + + ''' + obj_str: str = '' # accumulator + for prefix, field_repr, in iter_struct_ppfmt_lines( + struct, + field_indent=field_indent, + ): + obj_str += f'{prefix}{field_repr}\n' + + # global whitespace indent + ws: str = ' '*indent + if indent: + obj_str: str = textwrap.indent( + text=obj_str, + prefix=ws, + ) + + # qtn: str = ws + struct.__class__.__qualname__ + qtn: str = struct.__class__.__qualname__ return ( f'{qtn}(\n'