Re-impl as `DebugStatus.maybe_enter_repl_fixture()`
Dropping the `_maybe_open_repl_fixture()` approach and instead using a `DebugStatus._fixture_stack = ExitStack()` which provides for much simpler support around both sync and async pausing APIs thanks to only invoking `repl_fixture.__exit__()` on actual `PdbREPL` interaction being complete! Deats, - all `repl_fixture` detection logic still happens in one place (the new method) but we aren't limited to closing it via an immediate post REPL `.__exit__()` call which instead is triggered by, - `DebugStatus.release()` which now calls `._fixture_stack.close()` and thus only invokes `repl_fixture.__exit__()` when user REPL-ing is **actually complete** an arbitrary amount of debugging time later. - include the notes for `@acm` support above the new method, though not sure if they're as relevant any more? Benefits, - we can drop the previously added indent levels from `_enter_repl_sync()` and `_post_mortem()`. - now we automatically have support for the `.pause_from_sync()` API since `_enter_repl_sync()` doesn't close the prior `_maybe_open_repl_fixture()` immediately when `debug_func=None`; the user's `__exit__()` is only ever called once `.release()` is. Other, - add big 'CASE' comments around the various blocks in `.pause_from_sync()`, i was having trouble figuring out which i was using from a `breakpoint()` in a dependent app..repl_fixture
parent
d716c57234
commit
2a37f6abdd
|
@ -64,7 +64,6 @@ from tractor._exceptions import (
|
|||
)
|
||||
from ._trace import (
|
||||
_pause,
|
||||
_maybe_open_repl_fixture,
|
||||
)
|
||||
from ._tty_lock import (
|
||||
DebugStatus,
|
||||
|
@ -143,14 +142,14 @@ def _post_mortem(
|
|||
'''
|
||||
__tracebackhide__: bool = hide_tb
|
||||
|
||||
with _maybe_open_repl_fixture(
|
||||
# maybe enter any user fixture
|
||||
enter_repl: bool = DebugStatus.maybe_enter_repl_fixture(
|
||||
repl=repl,
|
||||
repl_fixture=repl_fixture,
|
||||
boxed_maybe_exc=boxed_maybe_exc,
|
||||
) as enter_repl:
|
||||
)
|
||||
if not enter_repl:
|
||||
return
|
||||
|
||||
try:
|
||||
actor: Actor = current_actor()
|
||||
actor_repr: str = str(actor.uid)
|
||||
|
|
|
@ -28,8 +28,6 @@ import asyncio
|
|||
import bdb
|
||||
from contextlib import (
|
||||
AbstractContextManager,
|
||||
contextmanager as cm,
|
||||
nullcontext,
|
||||
)
|
||||
from functools import (
|
||||
partial,
|
||||
|
@ -37,7 +35,6 @@ from functools import (
|
|||
import inspect
|
||||
import threading
|
||||
from typing import (
|
||||
Iterator,
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
@ -89,7 +86,7 @@ if TYPE_CHECKING:
|
|||
from tractor._runtime import (
|
||||
Actor,
|
||||
)
|
||||
from ._post_mortem import BoxedMaybeException
|
||||
# from ._post_mortem import BoxedMaybeException
|
||||
from ._repl import PdbREPL
|
||||
|
||||
log = get_logger(__package__)
|
||||
|
@ -99,69 +96,6 @@ _repl_fail_msg: str|None = (
|
|||
'Failed to REPl via `_pause()` '
|
||||
)
|
||||
|
||||
# TODO, support @acm?
|
||||
# -[ ] what about a return-proto for determining
|
||||
# whether the REPL should be allowed to enage?
|
||||
# -[ ] consider factoring this `_repl_fixture` block into
|
||||
# a common @cm somehow so it can be repurposed both here and
|
||||
# in `._pause()`??
|
||||
# -[ ] we could also use the `ContextDecorator`-type in that
|
||||
# case to simply decorate the `_enter_repl_sync()` closure?
|
||||
# |_https://docs.python.org/3/library/contextlib.html#using-a-context-manager-as-a-function-decorator
|
||||
@cm
|
||||
def _maybe_open_repl_fixture(
|
||||
repl: PdbREPL,
|
||||
# ^XXX **always provided** by the low-level REPL-invoker,
|
||||
# - _post_mortem()
|
||||
# - _pause()
|
||||
|
||||
repl_fixture: (
|
||||
AbstractContextManager[bool]
|
||||
|None
|
||||
) = None,
|
||||
boxed_maybe_exc: BoxedMaybeException|None = None,
|
||||
) -> Iterator[bool]:
|
||||
'''
|
||||
Maybe open a pre/post REPL entry "fixture" `@cm` provided by the
|
||||
user, the caller should use the delivered `bool` to determine
|
||||
whether to engage the `PdbREPL`.
|
||||
|
||||
'''
|
||||
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
|
||||
)(
|
||||
repl=repl,
|
||||
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'
|
||||
)
|
||||
yield False # no don't enter REPL
|
||||
return
|
||||
|
||||
yield True # yes enter REPL
|
||||
|
||||
|
||||
|
||||
async def _pause(
|
||||
|
||||
debug_func: Callable|partial|None,
|
||||
|
@ -255,15 +189,11 @@ async def _pause(
|
|||
) -> None:
|
||||
__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
|
||||
|
||||
with _maybe_open_repl_fixture(
|
||||
# maybe enter any user fixture
|
||||
enter_repl: bool = DebugStatus.maybe_enter_repl_fixture(
|
||||
repl=repl,
|
||||
repl_fixture=repl_fixture,
|
||||
) as enter_repl:
|
||||
)
|
||||
if not enter_repl:
|
||||
return
|
||||
|
||||
|
@ -338,7 +268,7 @@ async def _pause(
|
|||
|
||||
raise
|
||||
|
||||
log.devx(
|
||||
log.debug(
|
||||
'Entering `._pause()` for requesting task\n'
|
||||
f'|_{task}\n'
|
||||
)
|
||||
|
@ -352,7 +282,7 @@ async def _pause(
|
|||
or
|
||||
DebugStatus.repl_release.is_set()
|
||||
):
|
||||
log.devx(
|
||||
log.debug(
|
||||
'Setting new `DebugStatus.repl_release: trio.Event` for requesting task\n'
|
||||
f'|_{task}\n'
|
||||
)
|
||||
|
@ -431,7 +361,7 @@ async def _pause(
|
|||
'with no request ctx !?!?'
|
||||
)
|
||||
|
||||
log.devx(
|
||||
log.debug(
|
||||
f'attempting to {acq_prefix}acquire '
|
||||
f'{ctx_line}'
|
||||
)
|
||||
|
@ -1022,6 +952,8 @@ def pause_from_sync(
|
|||
# noop: non-cancelled `.to_thread`
|
||||
# `trio.Cancelled`: cancelled `.to_thread`
|
||||
|
||||
# CASE: bg-thread spawned via `trio.to_thread`
|
||||
# -----
|
||||
# when called from a (bg) thread, run an async task in a new
|
||||
# thread which will call `._pause()` manually with special
|
||||
# handling for root-actor caller usage.
|
||||
|
@ -1107,6 +1039,9 @@ def pause_from_sync(
|
|||
# '`tractor.pause[_from_sync]()` not yet supported '
|
||||
# 'for infected `asyncio` mode!'
|
||||
# )
|
||||
#
|
||||
# CASE: bg-thread running `asyncio.Task`
|
||||
# -----
|
||||
elif (
|
||||
not is_trio_thread
|
||||
and
|
||||
|
@ -1184,7 +1119,9 @@ def pause_from_sync(
|
|||
f'- greenback.bestow_portal(<task>)\n'
|
||||
)
|
||||
|
||||
else: # we are presumably the `trio.run()` + main thread
|
||||
# CASE: `trio.run()` + "main thread"
|
||||
# -----
|
||||
else:
|
||||
# raises on not-found by default
|
||||
greenback: ModuleType = maybe_import_greenback()
|
||||
|
||||
|
@ -1286,7 +1223,9 @@ def _sync_pause_from_builtin(
|
|||
Proxy call `.pause_from_sync()` but indicate the caller is the
|
||||
`breakpoint()` built-in.
|
||||
|
||||
Note: this assigned to `os.environ['PYTHONBREAKPOINT']` inside `._root`
|
||||
Note: this always assigned to `os.environ['PYTHONBREAKPOINT']`
|
||||
inside `._root.open_root_actor()` whenever `debug_mode=True` is
|
||||
set.
|
||||
|
||||
'''
|
||||
pause_from_sync(
|
||||
|
|
|
@ -22,7 +22,9 @@ Root-actor TTY mutex-locking machinery.
|
|||
from __future__ import annotations
|
||||
import asyncio
|
||||
from contextlib import (
|
||||
AbstractContextManager,
|
||||
asynccontextmanager as acm,
|
||||
ExitStack,
|
||||
)
|
||||
import textwrap
|
||||
import threading
|
||||
|
@ -75,6 +77,9 @@ if TYPE_CHECKING:
|
|||
from ._repl import (
|
||||
PdbREPL,
|
||||
)
|
||||
from ._post_mortem import (
|
||||
BoxedMaybeException,
|
||||
)
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
@ -601,6 +606,10 @@ class DebugStatus:
|
|||
# request.
|
||||
repl: PdbREPL|None = None
|
||||
|
||||
# any `repl_fixture` provided by user are entered and
|
||||
# latered closed on `.release()`
|
||||
_fixture_stack = ExitStack()
|
||||
|
||||
# TODO: yet again this looks like a task outcome where we need
|
||||
# to sync to the completion of one task (and get its result)
|
||||
# being used everywhere for syncing..
|
||||
|
@ -803,6 +812,70 @@ class DebugStatus:
|
|||
|
||||
return False
|
||||
|
||||
# TODO, support @acm?
|
||||
# -[ ] what about a return-proto for determining
|
||||
# whether the REPL should be allowed to enage?
|
||||
# -[x] consider factoring this `_repl_fixture` block into
|
||||
# a common @cm somehow so it can be repurposed both here and
|
||||
# in `._pause()`??
|
||||
# -[ ] we could also use the `ContextDecorator`-type in that
|
||||
# case to simply decorate the `_enter_repl_sync()` closure?
|
||||
# |_https://docs.python.org/3/library/contextlib.html#using-a-context-manager-as-a-function-decorator
|
||||
@classmethod
|
||||
def maybe_enter_repl_fixture(
|
||||
cls,
|
||||
# ^XXX **always provided** by the low-level REPL-invoker,
|
||||
# - _post_mortem()
|
||||
# - _pause()
|
||||
repl: PdbREPL,
|
||||
|
||||
# maybe pre/post REPL entry
|
||||
repl_fixture: (
|
||||
AbstractContextManager[bool]
|
||||
|None
|
||||
) = None,
|
||||
|
||||
# if called from crashed context, provided by
|
||||
# `open_crash_handler()`
|
||||
boxed_maybe_exc: BoxedMaybeException|None = None,
|
||||
) -> bool:
|
||||
'''
|
||||
Maybe open a pre/post REPL entry "fixture" `@cm` provided by the
|
||||
user, the caller should use the delivered `bool` to determine
|
||||
whether to engage the `PdbREPL`.
|
||||
|
||||
'''
|
||||
if not (
|
||||
repl_fixture
|
||||
or
|
||||
(rt_repl_fixture := _state._runtime_vars.get('repl_fixture'))
|
||||
):
|
||||
return True # YES always enter
|
||||
|
||||
_repl_fixture = (
|
||||
repl_fixture
|
||||
or
|
||||
rt_repl_fixture
|
||||
)
|
||||
enter_repl: bool = DebugStatus._fixture_stack.enter_context(
|
||||
_repl_fixture(
|
||||
repl=repl,
|
||||
maybe_bxerr=boxed_maybe_exc,
|
||||
)
|
||||
)
|
||||
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'
|
||||
)
|
||||
|
||||
log.devx(
|
||||
f'User provided `repl_fixture` entered with,\n'
|
||||
f'{repl_fixture!r} -> {enter_repl!r}\n'
|
||||
)
|
||||
return enter_repl
|
||||
|
||||
@classmethod
|
||||
# @pdbp.hideframe
|
||||
def release(
|
||||
|
@ -890,6 +963,8 @@ class DebugStatus:
|
|||
if current_actor(err_on_no_runtime=False):
|
||||
cls.unshield_sigint()
|
||||
|
||||
cls._fixture_stack.close()
|
||||
|
||||
|
||||
# TODO: use the new `@lowlevel.singleton` for this!
|
||||
def get_debug_req() -> DebugStatus|None:
|
||||
|
|
Loading…
Reference in New Issue