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..
							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