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 (
|
from ._trace import (
|
||||||
_pause,
|
_pause,
|
||||||
_maybe_open_repl_fixture,
|
|
||||||
)
|
)
|
||||||
from ._tty_lock import (
|
from ._tty_lock import (
|
||||||
DebugStatus,
|
DebugStatus,
|
||||||
|
@ -143,14 +142,14 @@ def _post_mortem(
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__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=repl,
|
||||||
repl_fixture=repl_fixture,
|
repl_fixture=repl_fixture,
|
||||||
boxed_maybe_exc=boxed_maybe_exc,
|
boxed_maybe_exc=boxed_maybe_exc,
|
||||||
) as enter_repl:
|
)
|
||||||
if not enter_repl:
|
if not enter_repl:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actor: Actor = current_actor()
|
actor: Actor = current_actor()
|
||||||
actor_repr: str = str(actor.uid)
|
actor_repr: str = str(actor.uid)
|
||||||
|
|
|
@ -28,8 +28,6 @@ import asyncio
|
||||||
import bdb
|
import bdb
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
AbstractContextManager,
|
AbstractContextManager,
|
||||||
contextmanager as cm,
|
|
||||||
nullcontext,
|
|
||||||
)
|
)
|
||||||
from functools import (
|
from functools import (
|
||||||
partial,
|
partial,
|
||||||
|
@ -37,7 +35,6 @@ from functools import (
|
||||||
import inspect
|
import inspect
|
||||||
import threading
|
import threading
|
||||||
from typing import (
|
from typing import (
|
||||||
Iterator,
|
|
||||||
Callable,
|
Callable,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
@ -89,7 +86,7 @@ if TYPE_CHECKING:
|
||||||
from tractor._runtime import (
|
from tractor._runtime import (
|
||||||
Actor,
|
Actor,
|
||||||
)
|
)
|
||||||
from ._post_mortem import BoxedMaybeException
|
# from ._post_mortem import BoxedMaybeException
|
||||||
from ._repl import PdbREPL
|
from ._repl import PdbREPL
|
||||||
|
|
||||||
log = get_logger(__package__)
|
log = get_logger(__package__)
|
||||||
|
@ -99,69 +96,6 @@ _repl_fail_msg: str|None = (
|
||||||
'Failed to REPl via `_pause()` '
|
'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(
|
async def _pause(
|
||||||
|
|
||||||
debug_func: Callable|partial|None,
|
debug_func: Callable|partial|None,
|
||||||
|
@ -255,15 +189,11 @@ async def _pause(
|
||||||
) -> None:
|
) -> None:
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
|
||||||
# TODO, support @acm?
|
# maybe enter any user fixture
|
||||||
# -[ ] what about a return-proto for determining
|
enter_repl: bool = DebugStatus.maybe_enter_repl_fixture(
|
||||||
# whether the REPL should be allowed to enage?
|
|
||||||
# nonlocal repl_fixture
|
|
||||||
|
|
||||||
with _maybe_open_repl_fixture(
|
|
||||||
repl=repl,
|
repl=repl,
|
||||||
repl_fixture=repl_fixture,
|
repl_fixture=repl_fixture,
|
||||||
) as enter_repl:
|
)
|
||||||
if not enter_repl:
|
if not enter_repl:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -338,7 +268,7 @@ async def _pause(
|
||||||
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
log.devx(
|
log.debug(
|
||||||
'Entering `._pause()` for requesting task\n'
|
'Entering `._pause()` for requesting task\n'
|
||||||
f'|_{task}\n'
|
f'|_{task}\n'
|
||||||
)
|
)
|
||||||
|
@ -352,7 +282,7 @@ async def _pause(
|
||||||
or
|
or
|
||||||
DebugStatus.repl_release.is_set()
|
DebugStatus.repl_release.is_set()
|
||||||
):
|
):
|
||||||
log.devx(
|
log.debug(
|
||||||
'Setting new `DebugStatus.repl_release: trio.Event` for requesting task\n'
|
'Setting new `DebugStatus.repl_release: trio.Event` for requesting task\n'
|
||||||
f'|_{task}\n'
|
f'|_{task}\n'
|
||||||
)
|
)
|
||||||
|
@ -431,7 +361,7 @@ async def _pause(
|
||||||
'with no request ctx !?!?'
|
'with no request ctx !?!?'
|
||||||
)
|
)
|
||||||
|
|
||||||
log.devx(
|
log.debug(
|
||||||
f'attempting to {acq_prefix}acquire '
|
f'attempting to {acq_prefix}acquire '
|
||||||
f'{ctx_line}'
|
f'{ctx_line}'
|
||||||
)
|
)
|
||||||
|
@ -1022,6 +952,8 @@ def pause_from_sync(
|
||||||
# noop: non-cancelled `.to_thread`
|
# noop: non-cancelled `.to_thread`
|
||||||
# `trio.Cancelled`: 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
|
# when called from a (bg) thread, run an async task in a new
|
||||||
# thread which will call `._pause()` manually with special
|
# thread which will call `._pause()` manually with special
|
||||||
# handling for root-actor caller usage.
|
# handling for root-actor caller usage.
|
||||||
|
@ -1107,6 +1039,9 @@ def pause_from_sync(
|
||||||
# '`tractor.pause[_from_sync]()` not yet supported '
|
# '`tractor.pause[_from_sync]()` not yet supported '
|
||||||
# 'for infected `asyncio` mode!'
|
# 'for infected `asyncio` mode!'
|
||||||
# )
|
# )
|
||||||
|
#
|
||||||
|
# CASE: bg-thread running `asyncio.Task`
|
||||||
|
# -----
|
||||||
elif (
|
elif (
|
||||||
not is_trio_thread
|
not is_trio_thread
|
||||||
and
|
and
|
||||||
|
@ -1184,7 +1119,9 @@ def pause_from_sync(
|
||||||
f'- greenback.bestow_portal(<task>)\n'
|
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
|
# raises on not-found by default
|
||||||
greenback: ModuleType = maybe_import_greenback()
|
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
|
Proxy call `.pause_from_sync()` but indicate the caller is the
|
||||||
`breakpoint()` built-in.
|
`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(
|
pause_from_sync(
|
||||||
|
|
|
@ -22,7 +22,9 @@ Root-actor TTY mutex-locking machinery.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
|
AbstractContextManager,
|
||||||
asynccontextmanager as acm,
|
asynccontextmanager as acm,
|
||||||
|
ExitStack,
|
||||||
)
|
)
|
||||||
import textwrap
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
|
@ -75,6 +77,9 @@ if TYPE_CHECKING:
|
||||||
from ._repl import (
|
from ._repl import (
|
||||||
PdbREPL,
|
PdbREPL,
|
||||||
)
|
)
|
||||||
|
from ._post_mortem import (
|
||||||
|
BoxedMaybeException,
|
||||||
|
)
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
@ -601,6 +606,10 @@ class DebugStatus:
|
||||||
# request.
|
# request.
|
||||||
repl: PdbREPL|None = None
|
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
|
# TODO: yet again this looks like a task outcome where we need
|
||||||
# to sync to the completion of one task (and get its result)
|
# to sync to the completion of one task (and get its result)
|
||||||
# being used everywhere for syncing..
|
# being used everywhere for syncing..
|
||||||
|
@ -803,6 +812,70 @@ class DebugStatus:
|
||||||
|
|
||||||
return False
|
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
|
@classmethod
|
||||||
# @pdbp.hideframe
|
# @pdbp.hideframe
|
||||||
def release(
|
def release(
|
||||||
|
@ -890,6 +963,8 @@ class DebugStatus:
|
||||||
if current_actor(err_on_no_runtime=False):
|
if current_actor(err_on_no_runtime=False):
|
||||||
cls.unshield_sigint()
|
cls.unshield_sigint()
|
||||||
|
|
||||||
|
cls._fixture_stack.close()
|
||||||
|
|
||||||
|
|
||||||
# TODO: use the new `@lowlevel.singleton` for this!
|
# TODO: use the new `@lowlevel.singleton` for this!
|
||||||
def get_debug_req() -> DebugStatus|None:
|
def get_debug_req() -> DebugStatus|None:
|
||||||
|
|
Loading…
Reference in New Issue