forked from goodboy/tractor
Rework and first draft of `.devx._frame_stack.py`
Proto-ing a little suite of call-stack-frame annotation-for-scanning sub-systems for the purposes of both, - the `.devx._debug`er and its traceback and frame introspection needs when entering the REPL, - detailed trace-style logging such that we can explicitly report on "which and where" `tractor`'s APIs are used in the "app" code. Deats: - change mod name obvi from `._code` and adjust client mod imports. - using `wrapt` (for perf) implement a `@api_frame` annot decorator which both stashes per-call-stack-frame instances of `CallerInfo` in a table and marks the function such that API endpoints can be easily found via runtime stack scanning despite any internal impl changes. - add a global `_frame2callerinfo_cache: dict[FrameType, CallerInfo]` table for providing the per func-frame info caching. - Re-implement `CallerInfo` to require less (types of) inputs: |_ `_api_func: Callable`, a ref to the (singleton) func def. |_ `_api_frame: FrameType` taken from the `@api_frame` marked `tractor`-API func's runtime call-stack, from which we can determine the app code's `.caller_frame`. |_`_caller_frames_up: int|None` allowing the specific `@api_frame` to determine "how many frames up" the application / calling code is. And, a better set of derived attrs: |_`caller_frame: FrameType` which finds and caches the API-eps calling frame. |_`caller_frame: FrameType` which finds and caches the API-eps calling - add a new attempt at "getting a method ref from its runtime frame" with `get_ns_and_func_from_frame()` using a heuristic that the `CodeType.co_qualname: str` should have a "." in it for methods. - main issue is still that the func-ref lookup will require searching for the method's instance type by name, and that name isn't guaranteed to be defined in any particular ns.. |_rn we try to read it from the `FrameType.f_locals` but that is going to obvi fail any time the method is called in a module where it's type is not also defined/imported. - returns both the ns and the func ref FYI.runtime_to_msgspec
parent
343b7c9712
commit
6690968236
|
@ -94,7 +94,7 @@ if TYPE_CHECKING:
|
||||||
from ._portal import Portal
|
from ._portal import Portal
|
||||||
from ._runtime import Actor
|
from ._runtime import Actor
|
||||||
from ._ipc import MsgTransport
|
from ._ipc import MsgTransport
|
||||||
from .devx._code import (
|
from .devx._frame_stack import (
|
||||||
CallerInfo,
|
CallerInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2513,7 +2513,7 @@ def mk_context(
|
||||||
send_chan, recv_chan = trio.open_memory_channel(msg_buffer_size)
|
send_chan, recv_chan = trio.open_memory_channel(msg_buffer_size)
|
||||||
|
|
||||||
# TODO: only scan caller-info if log level so high!
|
# TODO: only scan caller-info if log level so high!
|
||||||
from .devx._code import find_caller_info
|
from .devx._frame_stack import find_caller_info
|
||||||
caller_info: CallerInfo|None = find_caller_info()
|
caller_info: CallerInfo|None = find_caller_info()
|
||||||
|
|
||||||
# TODO: when/how do we apply `.limit_plds()` from here?
|
# TODO: when/how do we apply `.limit_plds()` from here?
|
||||||
|
|
|
@ -20,11 +20,8 @@ as it pertains to improving the grok-ability of our runtime!
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from functools import partial
|
||||||
import inspect
|
import inspect
|
||||||
# import msgspec
|
|
||||||
# from pprint import pformat
|
|
||||||
import textwrap
|
|
||||||
import traceback
|
|
||||||
from types import (
|
from types import (
|
||||||
FrameType,
|
FrameType,
|
||||||
FunctionType,
|
FunctionType,
|
||||||
|
@ -32,9 +29,8 @@ from types import (
|
||||||
# CodeType,
|
# CodeType,
|
||||||
)
|
)
|
||||||
from typing import (
|
from typing import (
|
||||||
# Any,
|
Any,
|
||||||
Callable,
|
Callable,
|
||||||
# TYPE_CHECKING,
|
|
||||||
Type,
|
Type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,6 +38,7 @@ from tractor.msg import (
|
||||||
pretty_struct,
|
pretty_struct,
|
||||||
NamespacePath,
|
NamespacePath,
|
||||||
)
|
)
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
|
||||||
# TODO: yeah, i don't love this and we should prolly just
|
# TODO: yeah, i don't love this and we should prolly just
|
||||||
|
@ -83,6 +80,31 @@ def get_class_from_frame(fr: FrameType) -> (
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_ns_and_func_from_frame(
|
||||||
|
frame: FrameType,
|
||||||
|
) -> Callable:
|
||||||
|
'''
|
||||||
|
Return the corresponding function object reference from
|
||||||
|
a `FrameType`, and return it and it's parent namespace `dict`.
|
||||||
|
|
||||||
|
'''
|
||||||
|
ns: dict[str, Any]
|
||||||
|
|
||||||
|
# for a method, go up a frame and lookup the name in locals()
|
||||||
|
if '.' in (qualname := frame.f_code.co_qualname):
|
||||||
|
cls_name, _, func_name = qualname.partition('.')
|
||||||
|
ns = frame.f_back.f_locals[cls_name].__dict__
|
||||||
|
|
||||||
|
else:
|
||||||
|
func_name: str = frame.f_code.co_name
|
||||||
|
ns = frame.f_globals
|
||||||
|
|
||||||
|
return (
|
||||||
|
ns,
|
||||||
|
ns[func_name],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def func_ref_from_frame(
|
def func_ref_from_frame(
|
||||||
frame: FrameType,
|
frame: FrameType,
|
||||||
) -> Callable:
|
) -> Callable:
|
||||||
|
@ -98,34 +120,63 @@ def func_ref_from_frame(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: move all this into new `.devx._code`!
|
|
||||||
# -[ ] prolly create a `@runtime_api` dec?
|
|
||||||
# -[ ] ^- make it capture and/or accept buncha optional
|
|
||||||
# meta-data like a fancier version of `@pdbp.hideframe`.
|
|
||||||
#
|
|
||||||
class CallerInfo(pretty_struct.Struct):
|
class CallerInfo(pretty_struct.Struct):
|
||||||
rt_fi: inspect.FrameInfo
|
# https://docs.python.org/dev/reference/datamodel.html#frame-objects
|
||||||
call_frame: FrameType
|
# https://docs.python.org/dev/library/inspect.html#the-interpreter-stack
|
||||||
|
_api_frame: FrameType
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_func_ref(self) -> Callable|None:
|
def api_frame(self) -> FrameType:
|
||||||
return func_ref_from_frame(self.rt_fi.frame)
|
try:
|
||||||
|
self._api_frame.clear()
|
||||||
|
except RuntimeError:
|
||||||
|
# log.warning(
|
||||||
|
print(
|
||||||
|
f'Frame {self._api_frame} for {self.api_func} is still active!'
|
||||||
|
)
|
||||||
|
|
||||||
|
return self._api_frame
|
||||||
|
|
||||||
|
_api_func: Callable
|
||||||
|
|
||||||
|
@property
|
||||||
|
def api_func(self) -> Callable:
|
||||||
|
return self._api_func
|
||||||
|
|
||||||
|
_caller_frames_up: int|None = 1
|
||||||
|
_caller_frame: FrameType|None = None # cached after first stack scan
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def api_nsp(self) -> NamespacePath|None:
|
def api_nsp(self) -> NamespacePath|None:
|
||||||
func: FunctionType = self.api_func_ref
|
func: FunctionType = self.api_func
|
||||||
if func:
|
if func:
|
||||||
return NamespacePath.from_ref(func)
|
return NamespacePath.from_ref(func)
|
||||||
|
|
||||||
return '<unknown>'
|
return '<unknown>'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def caller_func_ref(self) -> Callable|None:
|
def caller_frame(self) -> FrameType:
|
||||||
return func_ref_from_frame(self.call_frame)
|
|
||||||
|
# if not already cached, scan up stack explicitly by
|
||||||
|
# configured count.
|
||||||
|
if not self._caller_frame:
|
||||||
|
if self._caller_frames_up:
|
||||||
|
for _ in range(self._caller_frames_up):
|
||||||
|
caller_frame: FrameType|None = self.api_frame.f_back
|
||||||
|
|
||||||
|
if not caller_frame:
|
||||||
|
raise ValueError(
|
||||||
|
'No frame exists {self._caller_frames_up} up from\n'
|
||||||
|
f'{self.api_frame} @ {self.api_nsp}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
self._caller_frame = caller_frame
|
||||||
|
|
||||||
|
return self._caller_frame
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def caller_nsp(self) -> NamespacePath|None:
|
def caller_nsp(self) -> NamespacePath|None:
|
||||||
func: FunctionType = self.caller_func_ref
|
func: FunctionType = self.api_func
|
||||||
if func:
|
if func:
|
||||||
return NamespacePath.from_ref(func)
|
return NamespacePath.from_ref(func)
|
||||||
|
|
||||||
|
@ -172,108 +223,66 @@ def find_caller_info(
|
||||||
call_frame = call_frame.f_back
|
call_frame = call_frame.f_back
|
||||||
|
|
||||||
return CallerInfo(
|
return CallerInfo(
|
||||||
rt_fi=fi,
|
_api_frame=rt_frame,
|
||||||
call_frame=call_frame,
|
_api_func=func_ref_from_frame(rt_frame),
|
||||||
|
_caller_frames_up=go_up_iframes,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def pformat_boxed_tb(
|
_frame2callerinfo_cache: dict[FrameType, CallerInfo] = {}
|
||||||
tb_str: str,
|
|
||||||
fields_str: str|None = None,
|
|
||||||
field_prefix: str = ' |_',
|
|
||||||
|
|
||||||
tb_box_indent: int|None = None,
|
|
||||||
tb_body_indent: int = 1,
|
|
||||||
|
|
||||||
) -> str:
|
# TODO: -[x] move all this into new `.devx._code`!
|
||||||
'''
|
# -[ ] consider rename to _callstack?
|
||||||
Create a "boxed" looking traceback string.
|
# -[ ] prolly create a `@runtime_api` dec?
|
||||||
|
# |_ @api_frame seems better?
|
||||||
|
# -[ ] ^- make it capture and/or accept buncha optional
|
||||||
|
# meta-data like a fancier version of `@pdbp.hideframe`.
|
||||||
|
#
|
||||||
|
def api_frame(
|
||||||
|
wrapped: Callable|None = None,
|
||||||
|
*,
|
||||||
|
caller_frames_up: int = 1,
|
||||||
|
|
||||||
Useful for emphasizing traceback text content as being an
|
) -> Callable:
|
||||||
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
|
# handle the decorator called WITHOUT () case,
|
||||||
`fields_str` input along with other prefix/indent settings.
|
# i.e. just @api_frame, NOT @api_frame(extra=<blah>)
|
||||||
|
if wrapped is None:
|
||||||
|
return partial(
|
||||||
|
api_frame,
|
||||||
|
caller_frames_up=caller_frames_up,
|
||||||
|
)
|
||||||
|
|
||||||
'''
|
@wrapt.decorator
|
||||||
if (
|
async def wrapper(
|
||||||
fields_str
|
wrapped: Callable,
|
||||||
and
|
instance: object,
|
||||||
field_prefix
|
args: tuple,
|
||||||
|
kwargs: dict,
|
||||||
):
|
):
|
||||||
fields: str = textwrap.indent(
|
# maybe cache the API frame for this call
|
||||||
fields_str,
|
global _frame2callerinfo_cache
|
||||||
prefix=field_prefix,
|
this_frame: FrameType = inspect.currentframe()
|
||||||
)
|
api_frame: FrameType = this_frame.f_back
|
||||||
else:
|
|
||||||
fields = fields_str or ''
|
|
||||||
|
|
||||||
tb_body = tb_str
|
if not _frame2callerinfo_cache.get(api_frame):
|
||||||
if tb_body_indent:
|
_frame2callerinfo_cache[api_frame] = CallerInfo(
|
||||||
tb_body: str = textwrap.indent(
|
_api_frame=api_frame,
|
||||||
tb_str,
|
_api_func=wrapped,
|
||||||
prefix=tb_body_indent * ' ',
|
_caller_frames_up=caller_frames_up,
|
||||||
)
|
)
|
||||||
|
|
||||||
tb_box: str = (
|
return wrapped(*args, **kwargs)
|
||||||
|
|
||||||
# orig
|
# annotate the function as a "api function", meaning it is
|
||||||
# f' |\n'
|
# a function for which the function above it in the call stack should be
|
||||||
# f' ------ - ------\n\n'
|
# non-`tractor` code aka "user code".
|
||||||
# f'{tb_str}\n'
|
#
|
||||||
# f' ------ - ------\n'
|
# in the global frame cache for easy lookup from a given
|
||||||
# f' _|\n'
|
# func-instance
|
||||||
|
wrapped._call_infos: dict[FrameType, CallerInfo] = _frame2callerinfo_cache
|
||||||
f'|\n'
|
wrapped.__api_func__: bool = True
|
||||||
f' ------ - ------\n\n'
|
return wrapper(wrapped)
|
||||||
# f'{tb_str}\n'
|
|
||||||
f'{tb_body}'
|
|
||||||
f' ------ - ------\n'
|
|
||||||
f'_|\n'
|
|
||||||
)
|
|
||||||
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_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
|
|
Loading…
Reference in New Issue