Compare commits

...

11 Commits

Author SHA1 Message Date
Tyler Goodlet fb04f74605 Draft a (pretty)`Struct.fields_diff()`
For comparing a `msgspec.Struct` against an input `dict` presumably to
be used as input for struct instantiation. The main diff with
`.__sub__()` is that non-existing fields on either are reported
(loudly).
2024-12-10 14:12:37 -05:00
Tyler Goodlet aa1f6fa4b5 Spitballing how to expose custom `msgspec` type hooks
Such that maybe we can eventually offer a nicer higher-level API which
implements much of the boilerplate required by `msgspec` (like
type-matched branching to serialization logic) via a type-table
interface or something?

Not sure if the idea is that useful so leaving it all as TODOs for now
obviously.
2024-12-09 21:09:48 -05:00
Tyler Goodlet 9002f608ee Add `notes_to_self/howtorelease.md` reminder doc 2024-12-09 18:14:11 -05:00
Tyler Goodlet 8ebc022535 Add TODO for a runtime-vars passing mechanism 2024-12-09 18:12:22 -05:00
Tyler Goodlet e26fa8330f Change masked `.pause()` line 2024-12-09 18:04:32 -05:00
Tyler Goodlet a2659069c5 Type the inter-loop chans 2024-12-09 17:37:32 -05:00
Tyler Goodlet 54699d7a0b Denoise duplicate chan logging for now 2024-12-09 17:36:52 -05:00
Tyler Goodlet b91ab9e3a8 Add TODO for a tb frame "filterer" sys.. 2024-12-09 17:14:51 -05:00
Tyler Goodlet cd14c4fe72 Set `RemoteActorError.pformat(boxer_header=self.relay_uid)` by def 2024-12-09 16:57:57 -05:00
Tyler Goodlet ad40fcd2bc Support custom `boxer_header: str` provided by `pformat_boxed_tb()` caller 2024-12-09 16:57:22 -05:00
Tyler Goodlet 508ba510a5 Expose a `_ctlc_ignore_header: str` for use in `sigint_shield()` 2024-12-09 16:56:30 -05:00
12 changed files with 226 additions and 70 deletions

View File

@ -0,0 +1,18 @@
First generate a built disti:
```
python -m pip install --upgrade build
python -m build --sdist --outdir dist/alpha5/
```
Then try a test ``pypi`` upload:
```
python -m twine upload --repository testpypi dist/alpha5/*
```
The push to `pypi` for realz.
```
python -m twine upload --repository testpypi dist/alpha5/*
```

View File

@ -1703,15 +1703,28 @@ class Context:
# TODO: expose as mod func instead! # TODO: expose as mod func instead!
structfmt = pretty_struct.Struct.pformat structfmt = pretty_struct.Struct.pformat
if self._in_overrun: if self._in_overrun:
log.warning( report: str = (
f'Queueing OVERRUN msg on caller task:\n\n'
f'{flow_body}' f'{flow_body}'
f'{structfmt(msg)}\n' f'{structfmt(msg)}\n'
) )
over_q: deque = self._overflow_q
self._overflow_q.append(msg) self._overflow_q.append(msg)
if len(over_q) == over_q.maxlen:
report = (
'FAILED to queue OVERRUN msg, OVERAN the OVERRUN QUEUE !!\n\n'
+ report
)
# log.error(report)
log.debug(report)
else:
report = (
'Queueing OVERRUN msg on caller task:\n\n'
+ report
)
log.debug(report)
# XXX NOTE XXX # XXX NOTE XXX
# overrun is the ONLY case where returning early is fine! # overrun is the ONLY case where returning early is fine!
return False return False

View File

@ -609,6 +609,7 @@ class RemoteActorError(Exception):
# just after <Type( # just after <Type(
# |___ .. # |___ ..
tb_body_indent=1, tb_body_indent=1,
boxer_header=self.relay_uid,
) )
tail = '' tail = ''

View File

@ -95,6 +95,10 @@ async def open_root_actor(
hide_tb: bool = True, hide_tb: bool = True,
# TODO, a way for actors to augment passing derived
# read-only state to sublayers?
# extra_rt_vars: dict|None = None,
) -> Actor: ) -> Actor:
''' '''
Runtime init entry point for ``tractor``. Runtime init entry point for ``tractor``.

View File

@ -456,11 +456,14 @@ class Actor:
) )
if _pre_chan: if _pre_chan:
log.warning(
# con_status += ( # con_status += (
# ^TODO^ swap once we minimize conn duplication # ^TODO^ swap once we minimize conn duplication
f' -> Wait, we already have IPC with `{uid_short}`??\n' # -[ ] last thing might be reg/unreg runtime reqs?
f' |_{_pre_chan}\n' # log.warning(
log.debug(
f'?Wait?\n'
f'We already have IPC with peer {uid_short!r}\n'
f'|_{_pre_chan}\n'
) )
# IPC connection tracking for both peers and new children: # IPC connection tracking for both peers and new children:

View File

@ -1420,6 +1420,10 @@ def any_connected_locker_child() -> bool:
return False return False
_ctlc_ignore_header: str = (
'Ignoring SIGINT while debug REPL in use'
)
def sigint_shield( def sigint_shield(
signum: int, signum: int,
frame: 'frame', # type: ignore # noqa frame: 'frame', # type: ignore # noqa
@ -1501,7 +1505,9 @@ def sigint_shield(
# NOTE: don't emit this with `.pdb()` level in # NOTE: don't emit this with `.pdb()` level in
# root without a higher level. # root without a higher level.
log.runtime( log.runtime(
f'Ignoring SIGINT while debug REPL in use by child ' _ctlc_ignore_header
+
f' by child '
f'{uid_in_debug}\n' f'{uid_in_debug}\n'
) )
problem = None problem = None
@ -1535,7 +1541,9 @@ def sigint_shield(
# NOTE: since we emit this msg on ctl-c, we should # NOTE: since we emit this msg on ctl-c, we should
# also always re-print the prompt the tail block! # also always re-print the prompt the tail block!
log.pdb( log.pdb(
'Ignoring SIGINT while pdb REPL in use by root actor..\n' _ctlc_ignore_header
+
f' by root actor..\n'
f'{DebugStatus.repl_task}\n' f'{DebugStatus.repl_task}\n'
f' |_{repl}\n' f' |_{repl}\n'
) )
@ -1596,16 +1604,20 @@ def sigint_shield(
repl repl
): ):
log.pdb( log.pdb(
f'Ignoring SIGINT while local task using debug REPL\n' _ctlc_ignore_header
f'|_{repl_task}\n' +
f' |_{repl}\n' f' by local task\n\n'
f'{repl_task}\n'
f' |_{repl}\n'
) )
elif req_task: elif req_task:
log.debug( log.debug(
'Ignoring SIGINT while debug request task is open but either,\n' _ctlc_ignore_header
'- someone else is already REPL-in and has the `Lock`, or\n' +
'- some other local task already is replin?\n' f' by local request-task and either,\n'
f'|_{req_task}\n' f'- someone else is already REPL-in and has the `Lock`, or\n'
f'- some other local task already is replin?\n\n'
f'{req_task}\n'
) )
# TODO can we remove this now? # TODO can we remove this now?

View File

@ -234,7 +234,7 @@ def find_caller_info(
_frame2callerinfo_cache: dict[FrameType, CallerInfo] = {} _frame2callerinfo_cache: dict[FrameType, CallerInfo] = {}
# TODO: -[x] move all this into new `.devx._code`! # TODO: -[x] move all this into new `.devx._frame_stack`!
# -[ ] consider rename to _callstack? # -[ ] consider rename to _callstack?
# -[ ] prolly create a `@runtime_api` dec? # -[ ] prolly create a `@runtime_api` dec?
# |_ @api_frame seems better? # |_ @api_frame seems better?
@ -286,3 +286,18 @@ def api_frame(
wrapped._call_infos: dict[FrameType, CallerInfo] = _frame2callerinfo_cache wrapped._call_infos: dict[FrameType, CallerInfo] = _frame2callerinfo_cache
wrapped.__api_func__: bool = True wrapped.__api_func__: bool = True
return wrapper(wrapped) return wrapper(wrapped)
# TODO: something like this instead of the adhoc frame-unhiding
# blocks all over the runtime!! XD
# -[ ] ideally we can expect a certain error (set) and if something
# else is raised then all frames below the wrapped one will be
# un-hidden via `__tracebackhide__: bool = False`.
# |_ might need to dynamically mutate the code objs like
# `pdbp.hideframe()` does?
# -[ ] use this as a `@acm` decorator as introed in 3.10?
# @acm
# async def unhide_frame_when_not(
# error_set: set[BaseException],
# ) -> TracebackType:
# ...

View File

@ -53,6 +53,7 @@ def pformat_boxed_tb(
tb_box_indent: int|None = None, tb_box_indent: int|None = None,
tb_body_indent: int = 1, tb_body_indent: int = 1,
boxer_header: str = '-'
) -> str: ) -> str:
''' '''
@ -88,9 +89,9 @@ def pformat_boxed_tb(
tb_box: str = ( tb_box: str = (
f'|\n' f'|\n'
f' ------ - ------\n' f' ------ {boxer_header} ------\n'
f'{tb_body}' f'{tb_body}'
f' ------ - ------\n' f' ------ {boxer_header}- ------\n'
f'_|\n' f'_|\n'
) )
tb_box_indent: str = ( tb_box_indent: str = (

View File

@ -41,8 +41,10 @@ import textwrap
from typing import ( from typing import (
Any, Any,
Callable, Callable,
Protocol,
Type, Type,
TYPE_CHECKING, TYPE_CHECKING,
TypeVar,
Union, Union,
) )
from types import ModuleType from types import ModuleType
@ -181,7 +183,11 @@ def mk_dec(
dec_hook: Callable|None = None, dec_hook: Callable|None = None,
) -> MsgDec: ) -> MsgDec:
'''
Create an IPC msg decoder, normally used as the
`PayloadMsg.pld: PayloadT` field decoder inside a `PldRx`.
'''
return MsgDec( return MsgDec(
_dec=msgpack.Decoder( _dec=msgpack.Decoder(
type=spec, # like `MsgType[Any]` type=spec, # like `MsgType[Any]`
@ -227,6 +233,13 @@ def pformat_msgspec(
join_char: str = '\n', join_char: str = '\n',
) -> str: ) -> str:
'''
Pretty `str` format the `msgspec.msgpack.Decoder.type` attribute
for display in (console) log messages as a nice (maybe multiline)
presentation of all supported `Struct`s (subtypes) available for
typed decoding.
'''
dec: msgpack.Decoder = getattr(codec, 'dec', codec) dec: msgpack.Decoder = getattr(codec, 'dec', codec)
return join_char.join( return join_char.join(
mk_msgspec_table( mk_msgspec_table(
@ -630,31 +643,57 @@ def limit_msg_spec(
# # import pdbp; pdbp.set_trace() # # import pdbp; pdbp.set_trace()
# assert ext_codec.pld_spec == extended_spec # assert ext_codec.pld_spec == extended_spec
# yield ext_codec # yield ext_codec
#
# ^-TODO-^ is it impossible to make something like this orr!?
# TODO: make an auto-custom hook generator from a set of input custom
# types?
# -[ ] below is a proto design using a `TypeCodec` idea?
#
# type var for the expected interchange-lib's
# IPC-transport type when not available as a built-in
# serialization output.
WireT = TypeVar('WireT')
# TODO: make something similar to this inside `._codec` such that # TODO: some kinda (decorator) API for built-in subtypes
# user can just pass a type table of some sort? # that builds this implicitly by inspecting the `mro()`?
# -[ ] we would need to decode all msgs to `pretty_struct.Struct` class TypeCodec(Protocol):
# and then call `.to_dict()` on them? '''
# -[x] we're going to need to re-impl all the stuff changed in the A per-custom-type wire-transport serialization translator
# runtime port such that it can handle dicts or `Msg`s? description type.
#
# def mk_dict_msg_codec_hooks() -> tuple[Callable, Callable]: '''
# ''' src_type: Type
# Deliver a `enc_hook()`/`dec_hook()` pair which does wire_type: WireT
# manual convertion from our above native `Msg` set
# to `dict` equivalent (wire msgs) in order to keep legacy compat def encode(obj: Type) -> WireT:
# with the original runtime implementation. ...
#
# Note: this is is/was primarly used while moving the core def decode(
# runtime over to using native `Msg`-struct types wherein we obj_type: Type[WireT],
# start with the send side emitting without loading obj: WireT,
# a typed-decoder and then later flipping the switch over to ) -> Type:
# load to the native struct types once all runtime usage has ...
# been adjusted appropriately.
#
# ''' class MsgpackTypeCodec(TypeCodec):
# return ( ...
# # enc_to_dict,
# dec_from_dict,
# ) def mk_codec_hooks(
type_codecs: list[TypeCodec],
) -> tuple[Callable, Callable]:
'''
Deliver a `enc_hook()`/`dec_hook()` pair which handle
manual convertion from an input `Type` set such that whenever
the `TypeCodec.filter()` predicate matches the
`TypeCodec.decode()` is called on the input native object by
the `dec_hook()` and whenever the
`isiinstance(obj, TypeCodec.type)` matches against an
`enc_hook(obj=obj)` the return value is taken from a
`TypeCodec.encode(obj)` callback.
'''
...

View File

@ -30,9 +30,9 @@ from msgspec import (
Struct as _Struct, Struct as _Struct,
structs, structs,
) )
from pprint import ( # from pprint import (
saferepr, # saferepr,
) # )
from tractor.log import get_logger from tractor.log import get_logger
@ -75,8 +75,8 @@ class DiffDump(UserList):
for k, left, right in self: for k, left, right in self:
repstr += ( repstr += (
f'({k},\n' f'({k},\n'
f'\t{repr(left)},\n' f' |_{repr(left)},\n'
f'\t{repr(right)},\n' f' |_{repr(right)},\n'
')\n' ')\n'
) )
repstr += ']\n' repstr += ']\n'
@ -144,15 +144,22 @@ def pformat(
field_indent=indent + field_indent, field_indent=indent + field_indent,
) )
else: # the `pprint` recursion-safe format: else:
val_str: str = repr(v)
# XXX LOL, below just seems to be f#$%in causing
# recursion errs..
#
# the `pprint` recursion-safe format:
# https://docs.python.org/3.11/library/pprint.html#pprint.saferepr # https://docs.python.org/3.11/library/pprint.html#pprint.saferepr
try: # try:
val_str: str = saferepr(v) # val_str: str = saferepr(v)
except Exception: # except Exception:
log.exception( # log.exception(
'Failed to `saferepr({type(struct)})` !?\n' # 'Failed to `saferepr({type(struct)})` !?\n'
) # )
return _Struct.__repr__(struct) # raise
# return _Struct.__repr__(struct)
# TODO: LOLOL use `textwrap.indent()` instead dawwwwwg! # TODO: LOLOL use `textwrap.indent()` instead dawwwwwg!
obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n') obj_str += (field_ws + f'{k}: {typ_name} = {val_str},\n')
@ -203,12 +210,7 @@ class Struct(
return sin_props return sin_props
pformat = pformat pformat = pformat
# __repr__ = pformat
# __str__ = __repr__ = pformat
# TODO: use a pprint.PrettyPrinter instance around ONLY rendering
# inside a known tty?
# def __repr__(self) -> str:
# ...
def __repr__(self) -> str: def __repr__(self) -> str:
try: try:
return pformat(self) return pformat(self)
@ -218,6 +220,13 @@ class Struct(
) )
return _Struct.__repr__(self) return _Struct.__repr__(self)
# __repr__ = pformat
# __str__ = __repr__ = pformat
# TODO: use a pprint.PrettyPrinter instance around ONLY rendering
# inside a known tty?
# def __repr__(self) -> str:
# ...
def copy( def copy(
self, self,
update: dict | None = None, update: dict | None = None,
@ -267,13 +276,15 @@ class Struct(
fi.type(getattr(self, fi.name)), fi.type(getattr(self, fi.name)),
) )
# TODO: make a mod func instead and just point to it here for
# method impl?
def __sub__( def __sub__(
self, self,
other: Struct, other: Struct,
) -> DiffDump[tuple[str, Any, Any]]: ) -> DiffDump[tuple[str, Any, Any]]:
''' '''
Compare fields/items key-wise and return a ``DiffDump`` Compare fields/items key-wise and return a `DiffDump`
for easy visual REPL comparison B) for easy visual REPL comparison B)
''' '''
@ -290,3 +301,42 @@ class Struct(
)) ))
return diffs return diffs
@classmethod
def fields_diff(
cls,
other: dict|Struct,
) -> DiffDump[tuple[str, Any, Any]]:
'''
Very similar to `PrettyStruct.__sub__()` except accepts an
input `other: dict` (presumably that would normally be called
like `Struct(**other)`) which returns a `DiffDump` of the
fields of the struct and the `dict`'s fields.
'''
nullish = object()
consumed: dict = other.copy()
diffs: DiffDump[tuple[str, Any, Any]] = DiffDump()
for fi in structs.fields(cls):
field_name: str = fi.name
# ours: Any = getattr(self, field_name)
theirs: Any = consumed.pop(field_name, nullish)
if theirs is nullish:
diffs.append((
field_name,
f'{fi.type!r}',
'NOT-DEFINED in `other: dict`',
))
# when there are lingering fields in `other` that this struct
# DOES NOT define we also append those.
if consumed:
for k, v in consumed.items():
diffs.append((
k,
f'NOT-DEFINED for `{cls.__name__}`',
f'`other: dict` has value = {v!r}',
))
return diffs

View File

@ -245,14 +245,14 @@ def _run_asyncio_task(
result != orig and result != orig and
aio_err is None and aio_err is None and
# in the ``open_channel_from()`` case we don't # in the `open_channel_from()` case we don't
# relay through the "return value". # relay through the "return value".
not provide_channels not provide_channels
): ):
to_trio.send_nowait(result) to_trio.send_nowait(result)
finally: finally:
# if the task was spawned using ``open_channel_from()`` # if the task was spawned using `open_channel_from()`
# then we close the channels on exit. # then we close the channels on exit.
if provide_channels: if provide_channels:
# only close the sender side which will relay # only close the sender side which will relay
@ -500,7 +500,7 @@ async def run_task(
''' '''
# simple async func # simple async func
chan = _run_asyncio_task( chan: LinkedTaskChannel = _run_asyncio_task(
func, func,
qsize=1, qsize=1,
**kwargs, **kwargs,
@ -530,7 +530,7 @@ async def open_channel_from(
spawned ``asyncio`` task and ``trio``. spawned ``asyncio`` task and ``trio``.
''' '''
chan = _run_asyncio_task( chan: LinkedTaskChannel = _run_asyncio_task(
target, target,
qsize=2**8, qsize=2**8,
provide_channels=True, provide_channels=True,

View File

@ -382,7 +382,7 @@ class BroadcastReceiver(ReceiveChannel):
# likely it makes sense to unwind back to the # likely it makes sense to unwind back to the
# underlying? # underlying?
# import tractor # import tractor
# await tractor.breakpoint() # await tractor.pause()
log.warning( log.warning(
f'Only one sub left for {self}?\n' f'Only one sub left for {self}?\n'
'We can probably unwind from breceiver?' 'We can probably unwind from breceiver?'