@@ -354,7 +354,9 @@ class _QEventLoop:
354354 The set_running_loop parameter is there for backwards compatibility and does nothing.
355355 """
356356
357- def __init__ (self , app = None , set_running_loop = False , already_running = False ):
357+ def __init__ (
358+ self , app = None , set_running_loop = False , already_running = False , qtparent = None
359+ ):
358360 self .__app = app or QApplication .instance ()
359361 assert self .__app is not None , "No QApplication has been instantiated"
360362 self .__is_running = False
@@ -364,8 +366,10 @@ def __init__(self, app=None, set_running_loop=False, already_running=False):
364366 self ._read_notifiers = {}
365367 self ._write_notifiers = {}
366368 self ._timer = _SimpleTimer ()
369+ self .qtparent = qtparent or self .__app
367370
368371 self .__call_soon_signaller = signaller = _make_signaller (QtCore , object , tuple )
372+
369373 self .__call_soon_signal = signaller .signal
370374 self .__call_soon_signal .connect (
371375 lambda callback , args : self .call_soon (callback , * args )
@@ -374,6 +378,18 @@ def __init__(self, app=None, set_running_loop=False, already_running=False):
374378 assert self .__app is not None
375379 super ().__init__ ()
376380
381+ # Parent helper objects, such as timers, to this Qt parent for safe
382+ # lifetime management.
383+ if (
384+ self .qtparent is not None
385+ and self .qtparent .thread () is not QtCore .QThread .currentThread ()
386+ ):
387+ raise RuntimeError (
388+ "qt_parent must belong to the same QThread as the event loop"
389+ )
390+ self ._timer .setParent (self .qtparent )
391+ signaller .setParent (self .qtparent )
392+
377393 # We have to set __is_running to True after calling
378394 # super().__init__() because of a bug in BaseEventLoop.
379395 if already_running :
@@ -387,6 +403,9 @@ def __init__(self, app=None, set_running_loop=False, already_running=False):
387403 # for asyncio to recognize the already running loop
388404 asyncio .events ._set_running_loop (self )
389405
406+ def get_qtparent (self ):
407+ return self .qtparent
408+
390409 def run_forever (self ):
391410 """Run eventloop forever."""
392411
@@ -463,26 +482,53 @@ def close(self):
463482 if self .is_closed ():
464483 return
465484
485+ # the following code places try/catch around possibly failing
486+ # operations for safety and to guard against implementation
487+ # difference in the QT bindings.
488+
466489 self .__log_debug ("Closing event loop..." )
490+ # Catch exceptions for safety between bindings.
491+ try :
492+ poller = self .get_proactor_event_poller ()
493+ except AttributeError :
494+ pass
495+ else :
496+ poller .stop ()
497+
467498 if self .__default_executor is not None :
468499 self .__default_executor .shutdown ()
469500
470- if self .__call_soon_signal :
501+ # Disconnect thread-safe signaller and schedule deletion of helper QObjects
502+ try :
471503 self .__call_soon_signal .disconnect ()
504+ except Exception : # pragma: no cover
505+ pass
506+ try :
507+ # may raise if already deleted
508+ self .__call_soon_signaller .deleteLater ()
509+ except Exception : # pragma: no cover
510+ pass
472511
473- super ().close ()
474-
512+ # Stop timers first to avoid late invocations during teardown
475513 self ._timer .stop ()
476- self .__app = None
514+ try :
515+ self ._timer .deleteLater ()
516+ except Exception : # pragma: no cover
517+ pass
477518
519+ # Disable and disconnect any remaining notifiers before closing
478520 for notifier in itertools .chain (
479521 self ._read_notifiers .values (), self ._write_notifiers .values ()
480522 ):
481- notifier .setEnabled (False )
482- notifier .activated ["int" ].disconnect ()
523+ self ._delete_notifier (notifier )
483524
484- self ._read_notifiers = None
485- self ._write_notifiers = None
525+ self ._read_notifiers .clear ()
526+ self ._write_notifiers .clear ()
527+
528+ super ().close ()
529+
530+ # Finally, clear app reference
531+ self .__app = None
486532
487533 def call_later (self , delay , callback , * args , context = None ):
488534 """Register callback to be invoked after a certain delay."""
@@ -531,15 +577,16 @@ def _add_reader(self, fd, callback, *args):
531577 pass
532578 else :
533579 # this is necessary to avoid race condition-like issues
534- existing .setEnabled (False )
535- existing .activated ["int" ].disconnect ()
580+ self ._delete_notifier (existing )
536581 # will get overwritten by the assignment below anyways
537582
538- notifier = QtCore .QSocketNotifier (_fileno (fd ), QtCore .QSocketNotifier .Type .Read )
583+ notifier = QtCore .QSocketNotifier (
584+ _fileno (fd ), QtCore .QSocketNotifier .Type .Read , self .__app
585+ )
539586 notifier .setEnabled (True )
540587 self .__log_debug ("Adding reader callback for file descriptor %s" , fd )
541588 notifier .activated ["int" ].connect (
542- lambda : self .__on_notifier_ready (
589+ lambda * _ : self .__on_notifier_ready (
543590 self ._read_notifiers , notifier , fd , callback , args
544591 ) # noqa: C812
545592 )
@@ -556,8 +603,7 @@ def _remove_reader(self, fd):
556603 except KeyError :
557604 return False
558605 else :
559- notifier .setEnabled (False )
560- notifier .activated ["int" ].disconnect ()
606+ self ._delete_notifier (notifier )
561607 return True
562608
563609 def _add_writer (self , fd , callback , * args ):
@@ -569,18 +615,18 @@ def _add_writer(self, fd, callback, *args):
569615 pass
570616 else :
571617 # this is necessary to avoid race condition-like issues
572- existing .setEnabled (False )
573- existing .activated ["int" ].disconnect ()
618+ self ._delete_notifier (existing )
574619 # will get overwritten by the assignment below anyways
575620
576621 notifier = QtCore .QSocketNotifier (
577622 _fileno (fd ),
578623 QtCore .QSocketNotifier .Type .Write ,
624+ self .__app ,
579625 )
580626 notifier .setEnabled (True )
581627 self .__log_debug ("Adding writer callback for file descriptor %s" , fd )
582628 notifier .activated ["int" ].connect (
583- lambda : self .__on_notifier_ready (
629+ lambda * _ : self .__on_notifier_ready (
584630 self ._write_notifiers , notifier , fd , callback , args
585631 ) # noqa: C812
586632 )
@@ -597,8 +643,7 @@ def _remove_writer(self, fd):
597643 except KeyError :
598644 return False
599645 else :
600- notifier .setEnabled (False )
601- notifier .activated ["int" ].disconnect ()
646+ self ._delete_notifier (notifier )
602647 return True
603648
604649 def __notifier_cb_wrapper (self , notifiers , notifier , fd , callback , args ):
@@ -616,15 +661,14 @@ def __notifier_cb_wrapper(self, notifiers, notifier, fd, callback, args):
616661 notifier .setEnabled (True )
617662
618663 def __on_notifier_ready (self , notifiers , notifier , fd , callback , args ):
619- if fd not in notifiers :
664+ if fd not in notifiers : # pragma: no cover
620665 self ._logger .warning (
621666 "Socket notifier for fd %s is ready, even though it should "
622667 "be disabled, not calling %s and disabling" ,
623668 fd ,
624669 callback ,
625670 )
626- notifier .setEnabled (False )
627- notifier .activated ["int" ].disconnect ()
671+ self ._delete_notifier (notifier )
628672 return
629673
630674 # It can be necessary to disable QSocketNotifier when e.g. checking
@@ -636,6 +680,18 @@ def __on_notifier_ready(self, notifiers, notifier, fd, callback, args):
636680 self .__notifier_cb_wrapper , notifiers , notifier , fd , callback , args
637681 )
638682
683+ @staticmethod
684+ def _delete_notifier (notifier ):
685+ notifier .setEnabled (False )
686+ try :
687+ notifier .activated ["int" ].disconnect ()
688+ except Exception : # pragma: no cover
689+ pass
690+ try :
691+ notifier .deleteLater ()
692+ except Exception : # pragma: no cover
693+ pass
694+
639695 # Methods for interacting with threads.
640696
641697 def call_soon_threadsafe (self , callback , * args , context = None ):
0 commit comments