@@ -271,6 +271,7 @@ def __init__(
271
271
self ._is_running = False
272
272
self .future : Future [_AppResult ] | None = None
273
273
self .loop : AbstractEventLoop | None = None
274
+ self ._loop_thread : threading .Thread | None = None
274
275
self .context : contextvars .Context | None = None
275
276
276
277
#: Quoted insert. This flag is set if we go into quoted insert mode.
@@ -771,14 +772,16 @@ def flush_input() -> None:
771
772
return result
772
773
773
774
@contextmanager
774
- def get_loop () -> Iterator [AbstractEventLoop ]:
775
+ def set_loop () -> Iterator [AbstractEventLoop ]:
775
776
loop = get_running_loop ()
776
777
self .loop = loop
778
+ self ._loop_thread = threading .current_thread ()
777
779
778
780
try :
779
781
yield loop
780
782
finally :
781
783
self .loop = None
784
+ self ._loop_thread = None
782
785
783
786
@contextmanager
784
787
def set_is_running () -> Iterator [None ]:
@@ -853,7 +856,7 @@ def create_future(
853
856
# `max_postpone_time`.
854
857
self ._invalidated = False
855
858
856
- loop = stack .enter_context (get_loop ())
859
+ loop = stack .enter_context (set_loop ())
857
860
858
861
stack .enter_context (set_handle_sigint (loop ))
859
862
stack .enter_context (set_exception_handler_ctx (loop ))
@@ -999,6 +1002,10 @@ def _breakpointhook(self, *a: object, **kw: object) -> None:
999
1002
"""
1000
1003
Breakpointhook which uses PDB, but ensures that the application is
1001
1004
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.
1002
1009
"""
1003
1010
app = self
1004
1011
# 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:
1007
1014
1008
1015
TraceDispatch = Callable [[FrameType , str , Any ], Any ]
1009
1016
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
+
1014
1048
# Hide application.
1015
1049
app .renderer .erase ()
1016
1050
1017
1051
# Detach input and dispatch to debugger.
1018
1052
with app .input .detach ():
1019
1053
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 ():
1020
1078
return super ().trace_dispatch (frame , event , arg )
1021
1079
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 )
1026
1082
1027
1083
frame = sys ._getframe ().f_back
1028
1084
CustomPdb (stdout = sys .__stdout__ ).set_trace (frame )
0 commit comments