@@ -271,6 +271,7 @@ def __init__(
271271 self ._is_running = False
272272 self .future : Future [_AppResult ] | None = None
273273 self .loop : AbstractEventLoop | None = None
274+ self ._loop_thread : threading .Thread | None = None
274275 self .context : contextvars .Context | None = None
275276
276277 #: Quoted insert. This flag is set if we go into quoted insert mode.
@@ -771,14 +772,16 @@ def flush_input() -> None:
771772 return result
772773
773774 @contextmanager
774- def get_loop () -> Iterator [AbstractEventLoop ]:
775+ def set_loop () -> Iterator [AbstractEventLoop ]:
775776 loop = get_running_loop ()
776777 self .loop = loop
778+ self ._loop_thread = threading .current_thread ()
777779
778780 try :
779781 yield loop
780782 finally :
781783 self .loop = None
784+ self ._loop_thread = None
782785
783786 @contextmanager
784787 def set_is_running () -> Iterator [None ]:
@@ -853,7 +856,7 @@ def create_future(
853856 # `max_postpone_time`.
854857 self ._invalidated = False
855858
856- loop = stack .enter_context (get_loop ())
859+ loop = stack .enter_context (set_loop ())
857860
858861 stack .enter_context (set_handle_sigint (loop ))
859862 stack .enter_context (set_exception_handler_ctx (loop ))
@@ -999,6 +1002,10 @@ def _breakpointhook(self, *a: object, **kw: object) -> None:
9991002 """
10001003 Breakpointhook which uses PDB, but ensures that the application is
10011004 hidden and input echoing is restored during each debugger dispatch.
1005+
1006+ This can be called from any thread. In any case, the application's
1007+ event loop will be blocked while the PDB input is displayed. The event
1008+ will continue after leaving the debugger.
10021009 """
10031010 app = self
10041011 # Inline import on purpose. We don't want to import pdb, if not needed.
@@ -1007,22 +1014,71 @@ def _breakpointhook(self, *a: object, **kw: object) -> None:
10071014
10081015 TraceDispatch = Callable [[FrameType , str , Any ], Any ]
10091016
1010- class CustomPdb (pdb .Pdb ):
1011- def trace_dispatch (
1012- self , frame : FrameType , event : str , arg : Any
1013- ) -> TraceDispatch :
1017+ @contextmanager
1018+ def hide_app_from_eventloop_thread () -> Generator [None , None , None ]:
1019+ """Stop application if `__breakpointhook__` is called from within
1020+ the App's event loop."""
1021+ # Hide application.
1022+ app .renderer .erase ()
1023+
1024+ # Detach input and dispatch to debugger.
1025+ with app .input .detach ():
1026+ with app .input .cooked_mode ():
1027+ yield
1028+
1029+ # Note: we don't render the application again here, because
1030+ # there's a good chance that there's a breakpoint on the next
1031+ # line. This paint/erase cycle would move the PDB prompt back
1032+ # to the middle of the screen.
1033+
1034+ @contextmanager
1035+ def hide_app_from_other_thread () -> Generator [None , None , None ]:
1036+ """Stop application if `__breakpointhook__` is called from a
1037+ thread other than the App's event loop."""
1038+ ready = threading .Event ()
1039+ done = threading .Event ()
1040+
1041+ async def in_loop () -> None :
1042+ # from .run_in_terminal import in_terminal
1043+ # async with in_terminal():
1044+ # ready.set()
1045+ # await asyncio.get_running_loop().run_in_executor(None, done.wait)
1046+ # return
1047+
10141048 # Hide application.
10151049 app .renderer .erase ()
10161050
10171051 # Detach input and dispatch to debugger.
10181052 with app .input .detach ():
10191053 with app .input .cooked_mode ():
1054+ ready .set ()
1055+ # Here we block the App's event loop thread until the
1056+ # debugger resumes. We could have used `with
1057+ # run_in_terminal.in_terminal():` like the commented
1058+ # code above, but it seems to work better if we
1059+ # completely stop the main event loop while debugging.
1060+ done .wait ()
1061+
1062+ self .create_background_task (in_loop ())
1063+ ready .wait ()
1064+ try :
1065+ yield
1066+ finally :
1067+ done .set ()
1068+
1069+ class CustomPdb (pdb .Pdb ):
1070+ def trace_dispatch (
1071+ self , frame : FrameType , event : str , arg : Any
1072+ ) -> TraceDispatch :
1073+ if app ._loop_thread is None :
1074+ return super ().trace_dispatch (frame , event , arg )
1075+
1076+ if app ._loop_thread == threading .current_thread ():
1077+ with hide_app_from_eventloop_thread ():
10201078 return super ().trace_dispatch (frame , event , arg )
10211079
1022- # Note: we don't render the application again here, because
1023- # there's a good chance that there's a breakpoint on the next
1024- # line. This paint/erase cycle would move the PDB prompt back
1025- # to the middle of the screen.
1080+ with hide_app_from_other_thread ():
1081+ return super ().trace_dispatch (frame , event , arg )
10261082
10271083 frame = sys ._getframe ().f_back
10281084 CustomPdb (stdout = sys .__stdout__ ).set_trace (frame )
0 commit comments