Skip to content

Commit e5f4214

Browse files
authored
Merge pull request #158 from kristjanvalur/robustify-pyside
Robustify pyside
2 parents 1137fb1 + bd598b8 commit e5f4214

4 files changed

Lines changed: 147 additions & 36 deletions

File tree

src/qasync/__init__.py

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -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):

src/qasync/_unix.py

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@
1010

1111
import asyncio
1212
import collections
13+
import itertools
1314
import selectors
1415

1516
from . import QtCore, _fileno, with_logger
1617

1718
EVENT_READ = 1 << 0
1819
EVENT_WRITE = 1 << 1
1920

21+
# Qt5/Qt6 compatibility
22+
NotifierEnum = getattr(QtCore.QSocketNotifier, "Type", QtCore.QSocketNotifier)
23+
2024

2125
class _SelectorMapping(collections.abc.Mapping):
2226
"""Mapping of file objects to selector keys."""
@@ -40,14 +44,15 @@ def __iter__(self):
4044

4145
@with_logger
4246
class _Selector(selectors.BaseSelector):
43-
def __init__(self, parent):
47+
def __init__(self, parent, qtparent=None):
4448
# this maps file descriptors to keys
4549
self._fd_to_key = {}
4650
# read-only mapping returned by get_map()
4751
self.__map = _SelectorMapping(self)
4852
self.__read_notifiers = {}
4953
self.__write_notifiers = {}
5054
self.__parent = parent
55+
self.__qtparent = qtparent
5156

5257
def select(self, *args, **kwargs):
5358
"""Implement abstract method even though we don't need it."""
@@ -86,11 +91,17 @@ def register(self, fileobj, events, data=None):
8691
self._fd_to_key[key.fd] = key
8792

8893
if events & EVENT_READ:
89-
notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Read)
94+
notifier = QtCore.QSocketNotifier(
95+
key.fd, NotifierEnum.Read, self.__qtparent
96+
)
97+
notifier.setEnabled(True)
9098
notifier.activated["int"].connect(self.__on_read_activated)
9199
self.__read_notifiers[key.fd] = notifier
92100
if events & EVENT_WRITE:
93-
notifier = QtCore.QSocketNotifier(key.fd, QtCore.QSocketNotifier.Write)
101+
notifier = QtCore.QSocketNotifier(
102+
key.fd, NotifierEnum.Write, self.__qtparent
103+
)
104+
notifier.setEnabled(True)
94105
notifier.activated["int"].connect(self.__on_write_activated)
95106
self.__write_notifiers[key.fd] = notifier
96107

@@ -112,10 +123,10 @@ def unregister(self, fileobj):
112123
def drop_notifier(notifiers):
113124
try:
114125
notifier = notifiers.pop(key.fd)
115-
except KeyError:
126+
except KeyError: # pragma: no cover
116127
pass
117128
else:
118-
notifier.activated["int"].disconnect()
129+
self._delete_notifier(notifier)
119130

120131
try:
121132
key = self._fd_to_key.pop(self._fileobj_lookup(fileobj))
@@ -144,6 +155,10 @@ def modify(self, fileobj, events, data=None):
144155
def close(self):
145156
self._logger.debug("Closing")
146157
self._fd_to_key.clear()
158+
for notifier in itertools.chain(
159+
self.__read_notifiers.values(), self.__write_notifiers.values()
160+
):
161+
self._delete_notifier(notifier)
147162
self.__read_notifiers.clear()
148163
self.__write_notifiers.clear()
149164

@@ -166,13 +181,33 @@ def _key_from_fd(self, fd):
166181
except KeyError:
167182
return None
168183

184+
@staticmethod
185+
def _delete_notifier(notifier):
186+
notifier.setEnabled(False)
187+
try:
188+
notifier.activated["int"].disconnect()
189+
except Exception: # pragma: no cover
190+
pass
191+
try:
192+
notifier.deleteLater()
193+
except Exception: # pragma: no cover
194+
pass
195+
169196

170197
class _SelectorEventLoop(asyncio.SelectorEventLoop):
171198
def __init__(self):
172199
self._signal_safe_callbacks = []
173200

174-
selector = _Selector(self)
175-
asyncio.SelectorEventLoop.__init__(self, selector)
201+
try:
202+
qtparent = self.get_qtparent()
203+
except AttributeError: # pragma: no cover
204+
qtparent = None
205+
self._qtselector = _Selector(self, qtparent=qtparent)
206+
asyncio.SelectorEventLoop.__init__(self, self._qtselector)
207+
208+
def close(self):
209+
self._qtselector.close()
210+
super().close()
176211

177212
def _before_run_forever(self):
178213
pass

src/qasync/_windows.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def __init__(self):
3838
self.__event_signal.connect(self._process_events)
3939
self.__event_poller = _EventPoller(self.__event_signal)
4040

41+
def get_proactor_event_poller(self):
42+
return self.__event_poller
43+
4144
def _process_events(self, events):
4245
"""Process events from proactor."""
4346
for f, callback, transferred, key, ov in events:
@@ -210,6 +213,7 @@ class _EventPoller:
210213

211214
def __init__(self, sig_events):
212215
self.sig_events = sig_events
216+
self.__worker = None
213217

214218
def start(self, proactor):
215219
self._logger.debug("Starting (proactor: %s)...", proactor)
@@ -218,4 +222,6 @@ def start(self, proactor):
218222

219223
def stop(self):
220224
self._logger.debug("Stopping worker thread...")
221-
self.__worker.stop()
225+
if self.__worker is not None:
226+
self.__worker.stop()
227+
self.__worker = None

0 commit comments

Comments
 (0)