Add initial `repl_fixture` support B)

It turns out to be fairly useful to allow hooking into a given actor's
entry-and-exit around `.devx._debug._pause/._post_mortem()` calls which
engage the `pdbp.Pdb` REPL (really our `._debug.PdbREPL` but yeah).

Some very handy use cases include,
- swapping out-of-band (config) state that may otherwise halt the
  user's app since the actor normally handles kb&mouse input, in thread,
  which means that the handler will be blocked while the REPL is in use.
- (remotely) reporting actor-runtime state for monitoring purposes
  around crashes or pauses in normal operation.
- allowing for crash-handling to be hard-disabled via
  `._state._runtime_vars` say for when you never want a debugger to be
  entered in a production instance where you're not-sure-if/don't-want
  per-actor `debug_mode: bool` settings to always be unset, say bc
  you're still debugging some edge cases that ow you'd normally want to
  REPL up.

Impl details,
- add a new optional `._state._runtime_vars['repl_fixture']` field which
  for now can be manually set; i saw no reason for a formal API yet
  since we want to convert the `dict` to a struct anyway (first).
- augment both `.devx._debug._pause()/._post_mortem()` with a new
  optional `repl_fixture: AbstractContextManager[bool]` kwarg which
  when provided is `with repl_fixture()` opened around the lowlevel
  REPL interaction calls; if the enter-result, an expected `bool`, is
  `False` then the interaction is hard-bypassed.
  * for the `._pause()` case the `@cm` is opened around the entire body
    of the embedded `_enter_repl_sync()` closure (for now) though
    ideally longer term this entire routine is factored to be a lot less
    "nested" Bp
  * in `_post_mortem()` the entire previous body is wrapped similarly
    and also now excepts an optional `boxed_maybe_exc: BoxedMaybeException`
    only passed in the `open_crash_handler()` caller case.
- when the new runtime-var is overridden, (only manually atm) it is used
  instead but only whenever the above `repl_fixture` kwarg is left null.
- add a `BoxedMaybeException.pformat() = __repr__()` which when
  a `.value: Exception` is set renders a more "objecty" repr of the exc.

Obviously tests for all this should be coming soon!
repl_fixture
Tyler Goodlet 2025-05-11 20:23:35 -04:00
parent 2bb33da9c8
commit f604c8836d
2 changed files with 216 additions and 114 deletions

View File

@ -41,16 +41,24 @@ _current_actor: Actor|None = None # type: ignore # noqa
_last_actor_terminated: Actor|None = None _last_actor_terminated: Actor|None = None
# TODO: mk this a `msgspec.Struct`! # TODO: mk this a `msgspec.Struct`!
# -[ ] type out all fields obvi!
# -[ ] (eventually) mk wire-ready for monitoring?
_runtime_vars: dict[str, Any] = { _runtime_vars: dict[str, Any] = {
'_debug_mode': False, # root of actor-process tree info
'_is_root': False, '_is_root': False, # bool
'_root_mailbox': (None, None), '_root_mailbox': (None, None), # tuple[str|None, str|None]
# registrar info
'_registry_addrs': [], '_registry_addrs': [],
'_is_infected_aio': False, # `debug_mode: bool` settings
'_debug_mode': False, # bool
'repl_fixture': False, # |AbstractContextManager[bool]
# for `tractor.pause_from_sync()` & `breakpoint()` support # for `tractor.pause_from_sync()` & `breakpoint()` support
'use_greenback': False, 'use_greenback': False,
# infected-`asyncio`-mode: `trio` running as guest.
'_is_infected_aio': False,
} }

View File

@ -23,6 +23,7 @@ from __future__ import annotations
import asyncio import asyncio
import bdb import bdb
from contextlib import ( from contextlib import (
AbstractContextManager,
asynccontextmanager as acm, asynccontextmanager as acm,
contextmanager as cm, contextmanager as cm,
nullcontext, nullcontext,
@ -1774,6 +1775,13 @@ async def _pause(
tuple[Task, PdbREPL], tuple[Task, PdbREPL],
trio.Event trio.Event
] = trio.TASK_STATUS_IGNORED, ] = trio.TASK_STATUS_IGNORED,
# maybe pre/post REPL entry
repl_fixture: (
AbstractContextManager[bool]
|None
) = None,
**debug_func_kwargs, **debug_func_kwargs,
) -> tuple[Task, PdbREPL]|None: ) -> tuple[Task, PdbREPL]|None:
@ -1836,6 +1844,33 @@ async def _pause(
debug_func: partial[None], debug_func: partial[None],
) -> None: ) -> None:
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
# TODO, support @acm?
# -[ ] what about a return-proto for determining
# whether the REPL should be allowed to enage?
nonlocal repl_fixture
if not (
repl_fixture
or
(rt_repl_fixture := _state._runtime_vars.get('repl_fixture'))
):
repl_fixture = nullcontext(
enter_result=True,
)
_repl_fixture = repl_fixture or rt_repl_fixture
with _repl_fixture(maybe_bxerr=None) as enter_repl:
# XXX when the fixture doesn't allow it, skip
# the crash-handler REPL and raise now!
if not enter_repl:
log.pdb(
f'pdbp-REPL blocked by a `repl_fixture()` which yielded `False` !\n'
f'repl_fixture: {repl_fixture}\n'
f'rt_repl_fixture: {rt_repl_fixture}\n'
)
return
debug_func_name: str = ( debug_func_name: str = (
debug_func.func.__name__ if debug_func else 'None' debug_func.func.__name__ if debug_func else 'None'
) )
@ -2899,6 +2934,14 @@ def _post_mortem(
shield: bool = False, shield: bool = False,
hide_tb: bool = False, hide_tb: bool = False,
# maybe pre/post REPL entry
repl_fixture: (
AbstractContextManager[bool]
|None
) = None,
boxed_maybe_exc: BoxedMaybeException|None = None,
) -> None: ) -> None:
''' '''
Enter the ``pdbpp`` port mortem entrypoint using our custom Enter the ``pdbpp`` port mortem entrypoint using our custom
@ -2906,6 +2949,33 @@ def _post_mortem(
''' '''
__tracebackhide__: bool = hide_tb __tracebackhide__: bool = hide_tb
# TODO, support @acm?
# -[ ] what about a return-proto for determining
# whether the REPL should be allowed to enage?
if not (
repl_fixture
or
(rt_repl_fixture := _state._runtime_vars.get('repl_fixture'))
):
_repl_fixture = nullcontext(
enter_result=True,
)
else:
_repl_fixture = (repl_fixture or rt_repl_fixture)(maybe_bxerr=boxed_maybe_exc)
with _repl_fixture as enter_repl:
# XXX when the fixture doesn't allow it, skip
# the crash-handler REPL and raise now!
if not enter_repl:
log.pdb(
f'pdbp-REPL blocked by a `repl_fixture()` which yielded `False` !\n'
f'repl_fixture: {repl_fixture}\n'
f'rt_repl_fixture: {rt_repl_fixture}\n'
)
return
try: try:
actor: tractor.Actor = current_actor() actor: tractor.Actor = current_actor()
actor_repr: str = str(actor.uid) actor_repr: str = str(actor.uid)
@ -2935,7 +3005,6 @@ def _post_mortem(
# NOTE only replacing this from `pdbp.xpm()` to add the # NOTE only replacing this from `pdbp.xpm()` to add the
# `end=''` to the print XD # `end=''` to the print XD
print(traceback.format_exc(), end='') print(traceback.format_exc(), end='')
caller_frame: FrameType = api_frame.f_back caller_frame: FrameType = api_frame.f_back
# NOTE: see the impl details of followings to understand usage: # NOTE: see the impl details of followings to understand usage:
@ -3210,6 +3279,23 @@ class BoxedMaybeException(Struct):
''' '''
value: BaseException|None = None value: BaseException|None = None
def pformat(self) -> str:
'''
Repr the boxed `.value` error in more-than-string
repr form.
'''
if not self.value:
return f'<{type(self).__name__}( .value=None )>\n'
return (
f'<{type(self.value).__name__}(\n'
f' |_.value = {self.value}\n'
f')>\n'
)
__repr__ = pformat
# TODO: better naming and what additionals? # TODO: better naming and what additionals?
# - [ ] optional runtime plugging? # - [ ] optional runtime plugging?
@ -3226,7 +3312,12 @@ def open_crash_handler(
KeyboardInterrupt, KeyboardInterrupt,
trio.Cancelled, trio.Cancelled,
}, },
tb_hide: bool = True, tb_hide: bool = False,
repl_fixture: (
AbstractContextManager[bool] # pre/post REPL entry
|None
) = None,
): ):
''' '''
Generic "post mortem" crash handler using `pdbp` REPL debugger. Generic "post mortem" crash handler using `pdbp` REPL debugger.
@ -3266,6 +3357,9 @@ def open_crash_handler(
repl=mk_pdb(), repl=mk_pdb(),
tb=sys.exc_info()[2], tb=sys.exc_info()[2],
api_frame=inspect.currentframe().f_back, api_frame=inspect.currentframe().f_back,
repl_fixture=repl_fixture,
boxed_maybe_exc=boxed_maybe_exc,
) )
except bdb.BdbQuit: except bdb.BdbQuit:
__tracebackhide__: bool = False __tracebackhide__: bool = False
@ -3281,7 +3375,7 @@ def open_crash_handler(
@cm @cm
def maybe_open_crash_handler( def maybe_open_crash_handler(
pdb: bool|None = None, pdb: bool|None = None,
tb_hide: bool = True, tb_hide: bool = False,
**kwargs, **kwargs,
): ):
@ -3293,11 +3387,11 @@ def maybe_open_crash_handler(
flag is passed the pdb REPL is engaed on any crashes B) flag is passed the pdb REPL is engaed on any crashes B)
''' '''
__tracebackhide__: bool = tb_hide
if pdb is None: if pdb is None:
pdb: bool = _state.is_debug_mode() pdb: bool = _state.is_debug_mode()
__tracebackhide__: bool = tb_hide
rtctx = nullcontext( rtctx = nullcontext(
enter_result=BoxedMaybeException() enter_result=BoxedMaybeException()
) )