528 lines
13 KiB
Python
528 lines
13 KiB
Python
# 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 <https://www.gnu.org/licenses/>.
|
|
|
|
'''
|
|
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
|
|
)
|
|
|
|
# <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(
|
|
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
|
|
|
|
|
|
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)
|
|
# |_<Task: blah blah..> repr of toe, in py these look like <objs>
|
|
#
|
|
# >) closing/exiting/stopping,
|
|
# )> closed/exited/stopped,
|
|
# |_<Task: blah blah..>
|
|
# [OR <), )< ?? ]
|
|
#
|
|
# ending = )
|
|
# >c) cancelling to close/exit
|
|
# c)> cancelled (caused close), OR?
|
|
# |_<Actor: ..>
|
|
# OR maybe "<c)" which better indicates the cancel being
|
|
# "delivered/returned" / returned" to LHS?
|
|
#
|
|
# >x) erroring to eventuall exit
|
|
# x)> errored and terminated
|
|
# |_<Actor: ...>
|
|
#
|
|
# scopes: supers/nurseries, IPC-ctxs, sessions, perms, etc.
|
|
# >{ opening
|
|
# {> opened
|
|
# }> closed
|
|
# >} closing
|
|
#
|
|
# storage: like queues, shm-buffers, files, etc..
|
|
# >[ opening
|
|
# [> opened
|
|
# |_<FileObj: ..>
|
|
#
|
|
# >] 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
|