@@ -82,15 +82,19 @@ def _notify_stream_qt(kernel):
8282 def enum_helper (name ):
8383 return operator .attrgetter (name .rpartition ("." )[0 ])(sys .modules [QtCore .__package__ ])
8484
85+ def exit_loop ():
86+ """fall back to main loop"""
87+ kernel ._qt_notifier .setEnabled (False )
88+ kernel .app .qt_event_loop .quit ()
89+
8590 def process_stream_events ():
8691 """fall back to main loop when there's a socket event"""
8792 # call flush to ensure that the stream doesn't lose events
8893 # due to our consuming of the edge-triggered FD
8994 # flush returns the number of events consumed.
9095 # if there were any, wake it up
9196 if kernel .shell_stream .flush (limit = 1 ):
92- kernel ._qt_notifier .setEnabled (False )
93- kernel .app .qt_event_loop .quit ()
97+ exit_loop ()
9498
9599 if not hasattr (kernel , "_qt_notifier" ):
96100 fd = kernel .shell_stream .getsockopt (zmq .FD )
@@ -101,18 +105,31 @@ def process_stream_events():
101105 else :
102106 kernel ._qt_notifier .setEnabled (True )
103107
108+ # allow for scheduling exits from the loop in case a timeout needs to
109+ # be set from the kernel level
110+ def _schedule_exit (delay ):
111+ """schedule fall back to main loop in [delay] seconds"""
112+ # The signatures of QtCore.QTimer.singleShot are inconsistent between PySide and PyQt
113+ # if setting the TimerType, so we create a timer explicitly and store it
114+ # to avoid a memory leak.
115+ # PreciseTimer is needed so we exit after _at least_ the specified delay, not within 5% of it
116+ if not hasattr (kernel , "_qt_timer" ):
117+ kernel ._qt_timer = QtCore .QTimer (kernel .app )
118+ kernel ._qt_timer .setSingleShot (True )
119+ kernel ._qt_timer .setTimerType (enum_helper ("QtCore.Qt.TimerType" ).PreciseTimer )
120+ kernel ._qt_timer .timeout .connect (exit_loop )
121+ kernel ._qt_timer .start (int (1000 * delay ))
122+
123+ loop_qt ._schedule_exit = _schedule_exit
124+
104125 # there may already be unprocessed events waiting.
105126 # these events will not wake zmq's edge-triggered FD
106127 # since edge-triggered notification only occurs on new i/o activity.
107128 # process all the waiting events immediately
108129 # so we start in a clean state ensuring that any new i/o events will notify.
109130 # schedule first call on the eventloop as soon as it's running,
110131 # so we don't block here processing events
111- if not hasattr (kernel , "_qt_timer" ):
112- kernel ._qt_timer = QtCore .QTimer (kernel .app )
113- kernel ._qt_timer .setSingleShot (True )
114- kernel ._qt_timer .timeout .connect (process_stream_events )
115- kernel ._qt_timer .start (0 )
132+ QtCore .QTimer .singleShot (0 , process_stream_events )
116133
117134
118135@register_integration ("qt" , "qt5" , "qt6" )
@@ -229,23 +246,33 @@ def __init__(self, app):
229246 self .app = app
230247 self .app .withdraw ()
231248
232- def process_stream_events (stream , * a , ** kw ):
249+ def exit_loop ():
250+ """fall back to main loop"""
251+ app .tk .deletefilehandler (kernel .shell_stream .getsockopt (zmq .FD ))
252+ app .quit ()
253+ app .destroy ()
254+ del kernel .app_wrapper
255+
256+ def process_stream_events (* a , ** kw ):
233257 """fall back to main loop when there's a socket event"""
234- if stream .flush (limit = 1 ):
235- app .tk .deletefilehandler (stream .getsockopt (zmq .FD ))
236- app .quit ()
237- app .destroy ()
238- del kernel .app_wrapper
258+ if kernel .shell_stream .flush (limit = 1 ):
259+ exit_loop ()
260+
261+ # allow for scheduling exits from the loop in case a timeout needs to
262+ # be set from the kernel level
263+ def _schedule_exit (delay ):
264+ """schedule fall back to main loop in [delay] seconds"""
265+ app .after (int (1000 * delay ), exit_loop )
266+
267+ loop_tk ._schedule_exit = _schedule_exit
239268
240269 # For Tkinter, we create a Tk object and call its withdraw method.
241270 kernel .app_wrapper = BasicAppWrapper (app )
242-
243- notifier = partial (process_stream_events , kernel .shell_stream )
244- # seems to be needed for tk
245- notifier .__name__ = "notifier" # type:ignore[attr-defined]
246- app .tk .createfilehandler (kernel .shell_stream .getsockopt (zmq .FD ), READABLE , notifier )
271+ app .tk .createfilehandler (
272+ kernel .shell_stream .getsockopt (zmq .FD ), READABLE , process_stream_events
273+ )
247274 # schedule initial call after start
248- app .after (0 , notifier )
275+ app .after (0 , process_stream_events )
249276
250277 app .mainloop ()
251278
@@ -560,6 +587,10 @@ def enable_gui(gui, kernel=None):
560587 # User wants to turn off integration; clear any evidence if Qt was the last one.
561588 if hasattr (kernel , "app" ):
562589 delattr (kernel , "app" )
590+ if hasattr (kernel , "_qt_notifier" ):
591+ delattr (kernel , "_qt_notifier" )
592+ if hasattr (kernel , "_qt_timer" ):
593+ delattr (kernel , "_qt_timer" )
563594 else :
564595 if gui .startswith ("qt" ):
565596 # Prepare the kernel here so any exceptions are displayed in the client.
0 commit comments