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
							
								
									ce2d06cad9
								
							
						
					
					
						commit
						a5126862b9
					
				| 
						 | 
				
			
			@ -64,7 +64,6 @@ from tractor._exceptions import (
 | 
			
		|||
)
 | 
			
		||||
from ._trace import (
 | 
			
		||||
    _pause,
 | 
			
		||||
    _maybe_open_repl_fixture,
 | 
			
		||||
)
 | 
			
		||||
from ._tty_lock import (
 | 
			
		||||
    DebugStatus,
 | 
			
		||||
| 
						 | 
				
			
			@ -143,65 +142,65 @@ 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
 | 
			
		||||
    )
 | 
			
		||||
    if not enter_repl:
 | 
			
		||||
        return
 | 
			
		||||
    try:
 | 
			
		||||
        actor: Actor = current_actor()
 | 
			
		||||
        actor_repr: str = str(actor.uid)
 | 
			
		||||
        # ^TODO, instead a nice runtime-info + maddr + uid?
 | 
			
		||||
        # -[ ] impl a `Actor.__repr()__`??
 | 
			
		||||
        #  |_ <task>:<thread> @ <actor>
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            actor: Actor = current_actor()
 | 
			
		||||
            actor_repr: str = str(actor.uid)
 | 
			
		||||
            # ^TODO, instead a nice runtime-info + maddr + uid?
 | 
			
		||||
            # -[ ] impl a `Actor.__repr()__`??
 | 
			
		||||
            #  |_ <task>:<thread> @ <actor>
 | 
			
		||||
    except NoRuntime:
 | 
			
		||||
        actor_repr: str = '<no-actor-runtime?>'
 | 
			
		||||
 | 
			
		||||
        except NoRuntime:
 | 
			
		||||
            actor_repr: str = '<no-actor-runtime?>'
 | 
			
		||||
    try:
 | 
			
		||||
        task_repr: Task = trio.lowlevel.current_task()
 | 
			
		||||
    except RuntimeError:
 | 
			
		||||
        task_repr: str = '<unknown-Task>'
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            task_repr: Task = trio.lowlevel.current_task()
 | 
			
		||||
        except RuntimeError:
 | 
			
		||||
            task_repr: str = '<unknown-Task>'
 | 
			
		||||
    # TODO: print the actor supervion tree up to the root
 | 
			
		||||
    # here! Bo
 | 
			
		||||
    log.pdb(
 | 
			
		||||
        f'{_crash_msg}\n'
 | 
			
		||||
        f'x>(\n'
 | 
			
		||||
        f' |_ {task_repr} @ {actor_repr}\n'
 | 
			
		||||
 | 
			
		||||
        # TODO: print the actor supervion tree up to the root
 | 
			
		||||
        # here! Bo
 | 
			
		||||
        log.pdb(
 | 
			
		||||
            f'{_crash_msg}\n'
 | 
			
		||||
            f'x>(\n'
 | 
			
		||||
            f' |_ {task_repr} @ {actor_repr}\n'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
        )
 | 
			
		||||
    # XXX NOTE(s) on `pdbp.xpm()` version..
 | 
			
		||||
    #
 | 
			
		||||
    # - seems to lose the up-stack tb-info?
 | 
			
		||||
    # - currently we're (only) replacing this from `pdbp.xpm()`
 | 
			
		||||
    #   to add the `end=''` to the print XD
 | 
			
		||||
    #
 | 
			
		||||
    print(traceback.format_exc(), end='')
 | 
			
		||||
    caller_frame: FrameType = api_frame.f_back
 | 
			
		||||
 | 
			
		||||
        # XXX NOTE(s) on `pdbp.xpm()` version..
 | 
			
		||||
        #
 | 
			
		||||
        # - seems to lose the up-stack tb-info?
 | 
			
		||||
        # - currently we're (only) replacing this from `pdbp.xpm()`
 | 
			
		||||
        #   to add the `end=''` to the print XD
 | 
			
		||||
        #
 | 
			
		||||
        print(traceback.format_exc(), end='')
 | 
			
		||||
        caller_frame: FrameType = api_frame.f_back
 | 
			
		||||
    # NOTE, see the impl details of these in the lib to
 | 
			
		||||
    # understand usage:
 | 
			
		||||
    # - `pdbp.post_mortem()`
 | 
			
		||||
    # - `pdbp.xps()`
 | 
			
		||||
    # - `bdb.interaction()`
 | 
			
		||||
    repl.reset()
 | 
			
		||||
    repl.interaction(
 | 
			
		||||
        frame=caller_frame,
 | 
			
		||||
        # frame=None,
 | 
			
		||||
        traceback=tb,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
        # NOTE, see the impl details of these in the lib to
 | 
			
		||||
        # understand usage:
 | 
			
		||||
        # - `pdbp.post_mortem()`
 | 
			
		||||
        # - `pdbp.xps()`
 | 
			
		||||
        # - `bdb.interaction()`
 | 
			
		||||
        repl.reset()
 | 
			
		||||
        repl.interaction(
 | 
			
		||||
            frame=caller_frame,
 | 
			
		||||
            # frame=None,
 | 
			
		||||
            traceback=tb,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # XXX NOTE XXX: this is abs required to avoid hangs!
 | 
			
		||||
        #
 | 
			
		||||
        # Since we presume the post-mortem was enaged to
 | 
			
		||||
        # a task-ending error, we MUST release the local REPL request
 | 
			
		||||
        # so that not other local task nor the root remains blocked!
 | 
			
		||||
        DebugStatus.release()
 | 
			
		||||
    # XXX NOTE XXX: this is abs required to avoid hangs!
 | 
			
		||||
    #
 | 
			
		||||
    # Since we presume the post-mortem was enaged to
 | 
			
		||||
    # a task-ending error, we MUST release the local REPL request
 | 
			
		||||
    # so that not other local task nor the root remains blocked!
 | 
			
		||||
    DebugStatus.release()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def post_mortem(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,90 +189,86 @@ 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
 | 
			
		||||
        )
 | 
			
		||||
        if not enter_repl:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
            debug_func_name: str = (
 | 
			
		||||
                debug_func.func.__name__ if debug_func else 'None'
 | 
			
		||||
            )
 | 
			
		||||
        debug_func_name: str = (
 | 
			
		||||
            debug_func.func.__name__ if debug_func else 'None'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
            # TODO: do we want to support using this **just** for the
 | 
			
		||||
            # locking / common code (prolly to help address #320)?
 | 
			
		||||
            task_status.started((task, repl))
 | 
			
		||||
            try:
 | 
			
		||||
                if debug_func:
 | 
			
		||||
                    # block here one (at the appropriate frame *up*) where
 | 
			
		||||
                    # ``breakpoint()`` was awaited and begin handling stdio.
 | 
			
		||||
                    log.devx(
 | 
			
		||||
                        'Entering sync world of the `pdb` REPL for task..\n'
 | 
			
		||||
                        f'{repl}\n'
 | 
			
		||||
                        f'  |_{task}\n'
 | 
			
		||||
                     )
 | 
			
		||||
        # TODO: do we want to support using this **just** for the
 | 
			
		||||
        # locking / common code (prolly to help address #320)?
 | 
			
		||||
        task_status.started((task, repl))
 | 
			
		||||
        try:
 | 
			
		||||
            if debug_func:
 | 
			
		||||
                # block here one (at the appropriate frame *up*) where
 | 
			
		||||
                # ``breakpoint()`` was awaited and begin handling stdio.
 | 
			
		||||
                log.devx(
 | 
			
		||||
                    'Entering sync world of the `pdb` REPL for task..\n'
 | 
			
		||||
                    f'{repl}\n'
 | 
			
		||||
                    f'  |_{task}\n'
 | 
			
		||||
                 )
 | 
			
		||||
 | 
			
		||||
                    # set local task on process-global state to avoid
 | 
			
		||||
                    # recurrent entries/requests from the same
 | 
			
		||||
                    # actor-local task.
 | 
			
		||||
                    DebugStatus.repl_task = task
 | 
			
		||||
                    if repl:
 | 
			
		||||
                        DebugStatus.repl = repl
 | 
			
		||||
                    else:
 | 
			
		||||
                        log.error(
 | 
			
		||||
                            'No REPl instance set before entering `debug_func`?\n'
 | 
			
		||||
                            f'{debug_func}\n'
 | 
			
		||||
                        )
 | 
			
		||||
 | 
			
		||||
                    # invoke the low-level REPL activation routine which itself
 | 
			
		||||
                    # should call into a `Pdb.set_trace()` of some sort.
 | 
			
		||||
                    debug_func(
 | 
			
		||||
                        repl=repl,
 | 
			
		||||
                        hide_tb=hide_tb,
 | 
			
		||||
                        **debug_func_kwargs,
 | 
			
		||||
                # set local task on process-global state to avoid
 | 
			
		||||
                # recurrent entries/requests from the same
 | 
			
		||||
                # actor-local task.
 | 
			
		||||
                DebugStatus.repl_task = task
 | 
			
		||||
                if repl:
 | 
			
		||||
                    DebugStatus.repl = repl
 | 
			
		||||
                else:
 | 
			
		||||
                    log.error(
 | 
			
		||||
                        'No REPl instance set before entering `debug_func`?\n'
 | 
			
		||||
                        f'{debug_func}\n'
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                # TODO: maybe invert this logic and instead
 | 
			
		||||
                # do `assert debug_func is None` when
 | 
			
		||||
                # `called_from_sync`?
 | 
			
		||||
                else:
 | 
			
		||||
                    if (
 | 
			
		||||
                        called_from_sync
 | 
			
		||||
                        and
 | 
			
		||||
                        not DebugStatus.is_main_trio_thread()
 | 
			
		||||
                    ):
 | 
			
		||||
                        assert called_from_bg_thread
 | 
			
		||||
                        assert DebugStatus.repl_task is not task
 | 
			
		||||
 | 
			
		||||
                    return (task, repl)
 | 
			
		||||
 | 
			
		||||
            except trio.Cancelled:
 | 
			
		||||
                log.exception(
 | 
			
		||||
                    'Cancelled during invoke of internal\n\n'
 | 
			
		||||
                    f'`debug_func = {debug_func_name}`\n'
 | 
			
		||||
                # invoke the low-level REPL activation routine which itself
 | 
			
		||||
                # should call into a `Pdb.set_trace()` of some sort.
 | 
			
		||||
                debug_func(
 | 
			
		||||
                    repl=repl,
 | 
			
		||||
                    hide_tb=hide_tb,
 | 
			
		||||
                    **debug_func_kwargs,
 | 
			
		||||
                )
 | 
			
		||||
                # XXX NOTE: DON'T release lock yet
 | 
			
		||||
                raise
 | 
			
		||||
 | 
			
		||||
            except BaseException:
 | 
			
		||||
                __tracebackhide__: bool = False
 | 
			
		||||
                log.exception(
 | 
			
		||||
                    'Failed to invoke internal\n\n'
 | 
			
		||||
                    f'`debug_func = {debug_func_name}`\n'
 | 
			
		||||
                )
 | 
			
		||||
                # NOTE: OW this is ONLY called from the
 | 
			
		||||
                # `.set_continue/next` hooks!
 | 
			
		||||
                DebugStatus.release(cancel_req_task=True)
 | 
			
		||||
            # TODO: maybe invert this logic and instead
 | 
			
		||||
            # do `assert debug_func is None` when
 | 
			
		||||
            # `called_from_sync`?
 | 
			
		||||
            else:
 | 
			
		||||
                if (
 | 
			
		||||
                    called_from_sync
 | 
			
		||||
                    and
 | 
			
		||||
                    not DebugStatus.is_main_trio_thread()
 | 
			
		||||
                ):
 | 
			
		||||
                    assert called_from_bg_thread
 | 
			
		||||
                    assert DebugStatus.repl_task is not task
 | 
			
		||||
 | 
			
		||||
                raise
 | 
			
		||||
                return (task, repl)
 | 
			
		||||
 | 
			
		||||
    log.devx(
 | 
			
		||||
        except trio.Cancelled:
 | 
			
		||||
            log.exception(
 | 
			
		||||
                'Cancelled during invoke of internal\n\n'
 | 
			
		||||
                f'`debug_func = {debug_func_name}`\n'
 | 
			
		||||
            )
 | 
			
		||||
            # XXX NOTE: DON'T release lock yet
 | 
			
		||||
            raise
 | 
			
		||||
 | 
			
		||||
        except BaseException:
 | 
			
		||||
            __tracebackhide__: bool = False
 | 
			
		||||
            log.exception(
 | 
			
		||||
                'Failed to invoke internal\n\n'
 | 
			
		||||
                f'`debug_func = {debug_func_name}`\n'
 | 
			
		||||
            )
 | 
			
		||||
            # NOTE: OW this is ONLY called from the
 | 
			
		||||
            # `.set_continue/next` hooks!
 | 
			
		||||
            DebugStatus.release(cancel_req_task=True)
 | 
			
		||||
 | 
			
		||||
            raise
 | 
			
		||||
 | 
			
		||||
    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