From 4d6b4edce9c00a3c91220d9db4ce632fed57455f Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:09:01 +0300 Subject: [PATCH 1/2] Add support for using QEventLoop inside of QThread --- qasync/__init__.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index 5a81032..ee67316 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -78,24 +78,28 @@ from PyQt5.QtCore import pyqtSlot as Slot QApplication = QtWidgets.QApplication + AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00) elif QtModuleName == "PyQt6": from PyQt6 import QtWidgets from PyQt6.QtCore import pyqtSlot as Slot QApplication = QtWidgets.QApplication + AllEvents = QtCore.QEventLoop.ProcessEventsFlag(0x00) elif QtModuleName == "PySide2": from PySide2 import QtWidgets from PySide2.QtCore import Slot QApplication = QtWidgets.QApplication + AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00) elif QtModuleName == "PySide6": from PySide6 import QtWidgets from PySide6.QtCore import Slot QApplication = QtWidgets.QApplication + AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00) from ._common import with_logger # noqa @@ -234,7 +238,7 @@ def __exit__(self, *args): def _format_handle(handle: asyncio.Handle): cb = getattr(handle, "_callback", None) - if isinstance(getattr(cb, '__self__', None), asyncio.tasks.Task): + if isinstance(getattr(cb, "__self__", None), asyncio.tasks.Task): return repr(cb.__self__) return str(handle) @@ -292,7 +296,11 @@ def timerEvent(self, event): # noqa: N802 handle._run() dt = time.time() - t0 if dt >= loop.slow_callback_duration: - self._logger.warning('Executing %s took %.3f seconds', _format_handle(handle), dt) + self._logger.warning( + "Executing %s took %.3f seconds", + _format_handle(handle), + dt, + ) finally: loop._current_handle = None else: @@ -338,7 +346,7 @@ class _QEventLoop: ... await asyncio.sleep(.1) >>> >>> asyncio.run(xplusy(2, 2), loop_factory=lambda:QEventLoop(app)) - + If the event loop shall be used with an existing and already running QApplication it must be specified in the constructor via already_running=True In this case the user is responsible for loop cleanup with stop() and close() @@ -420,7 +428,9 @@ def stop(*args): self.run_forever() finally: future.remove_done_callback(stop) - self.__app.processEvents() # run loop one last time to process all the events + self.__app.eventDispatcher().processEvents( + AllEvents + ) # run loop one last time to process all the events if not future.done(): raise RuntimeError("Event loop stopped before Future completed.") From 969663cc2537920c3fb32adf16df29981cdc9abc Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:10:42 +0300 Subject: [PATCH 2/2] Add tests for using QEventLoop inside of QThread --- tests/test_qeventloop.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index d2feb96..76d374d 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -5,6 +5,7 @@ import asyncio import ctypes +import threading import logging import multiprocessing import os @@ -874,6 +875,37 @@ async def main(): assert not loop.is_running() +def test_qeventloop_in_qthread(): + class CoroutineExecutorThread(qasync.QtCore.QThread): + def __init__(self, coro): + super().__init__() + self.coro = coro + self.loop = None + + def run(self): + self.loop = qasync.QEventLoop(self) + asyncio.set_event_loop(self.loop) + asyncio.run(self.coro) + + def join(self): + self.loop.stop() + self.loop.close() + self.wait() + + event = threading.Event() + + async def coro(): + await asyncio.sleep(0.1) + event.set() + + thread = CoroutineExecutorThread(coro()) + thread.start() + + assert event.wait(timeout=1), "Coroutine did not execute successfully" + + thread.join() # Ensure thread cleanup + + def teardown_module(module): """ Remove handlers from all loggers