# tractor: structured concurrent "actors". # Copyright 2018-eternity Tyler Goodlet. # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ''' Pretty formatters for use throughout our internals. Handy for logging and exception message content but also for `repr()` in REPL(s). ''' import sys import textwrap import traceback from trio import CancelScope def add_div( message: str, div_str: str = '------ - ------', ) -> str: ''' Add a "divider string" to the input `message` with a little math to center it underneath. ''' div_offset: int = ( round(len(message)/2)+1 - round(len(div_str)/2)+1 ) div_str: str = ( '\n' + ' '*div_offset + f'{div_str}\n' ) return div_str 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, boxer_header: str = '-' ) -> 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 = ( f'|\n' f' ------ {boxer_header} ------\n' f'{tb_body}' f' ------ {boxer_header}- ------\n' f'_|' ) 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_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, ) -> 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=' ', ) return tb_str def pformat_cs( cs: CancelScope, var_name: str = 'cs', field_prefix: str = ' |_', ) -> str: ''' 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 line more or less. ''' fields: str = textwrap.indent( ( f'cancel_called = {cs.cancel_called}\n' f'cancelled_caught = {cs.cancelled_caught}\n' f'_cancel_status = {cs._cancel_status}\n' f'shield = {cs.shield}\n' ), prefix=field_prefix, ) return ( f'{var_name}: {cs}\n' + 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