diff options
-rw-r--r-- | examples/async/eratosthenes/doc/eratosthenes.rst | 30 | ||||
-rw-r--r-- | examples/async/eratosthenes/eratosthenes_asyncio.py | 89 | ||||
-rw-r--r-- | examples/async/minimal/doc/minimal.rst | 30 | ||||
-rw-r--r-- | examples/async/minimal/minimal_asyncio.py | 71 |
4 files changed, 30 insertions, 190 deletions
diff --git a/examples/async/eratosthenes/doc/eratosthenes.rst b/examples/async/eratosthenes/doc/eratosthenes.rst index 494a94df3..5b8303911 100644 --- a/examples/async/eratosthenes/doc/eratosthenes.rst +++ b/examples/async/eratosthenes/doc/eratosthenes.rst @@ -6,22 +6,16 @@ The Python language provides keywords for asynchronous operations, i.e., event loop (see `PEP 492 <https://peps.python.org/pep-0492/>`_). It is up to packages to implement an event loop, support for these keywords, and more. -The best-known package for this is `asyncio`. Since both an async package and -Qt itself work with event loops, special care must be taken to ensure that both -event loops work with each other. asyncio offers a function `stop` that allows -stopping an event loop without closing it. If it is called while a loop is -running through `run_forever`, the loop will run the current batch of callbacks -and then exit. New callbacks wil be scheduled the next time `run_forever` is -called. +The best-known package for this is `asyncio`. asyncio offers an API that allows +for the asyncio event loop to be replaced by a custom implementation. Such an +implementation is available with the `QtAsyncio` module. It is based on Qt and +uses Qt's event loop in the backend. -This approach is highly experimental and does not represent the state of the -art of integrating Qt with asyncio. Instead it should rather be regarded more -as a proof of concept to contrast asyncio with other async packages such as -`trio`, which offers a dedicated `low-level API +`trio` is another popular package that offers a dedicated `low-level API <https://trio.readthedocs.io/en/stable/reference-lowlevel.html>`_ for more -complicated use cases such as this. Specifically, there exists a function -`start_guest_run` that enables running the Trio event loop as a "guest" inside -another event loop - Qt's in our case. +complex use cases. Specifically, there exists a function `start_guest_run` that +enables running the Trio event loop as a "guest" inside another event loop - +Qt's in our case, standing in contrast to asyncio's approach. Based on this functionality, two examples for async usage with Qt have been implemented: `eratosthenes` and `minimal`: @@ -39,14 +33,6 @@ implemented: `eratosthenes` and `minimal`: boilerplate code is essential for an async program with Qt and offers a starting point for more complex programs. -Both examples feature: - -1. A window class. -2. An `AsyncHelper` class containing `start_guest_run` plus helpers and - callbacks necessary for its invocation. The entry point for the Trio/asyncio - guest run is provided as an argument from outside, which can be any async - function. - While `eratosthenes` offloads the asynchronous logic that will run in trio's/asyncio's event loop into a separate class, `minimal` demonstrates that async functions can be integrated into any class, including subclasses of Qt diff --git a/examples/async/eratosthenes/eratosthenes_asyncio.py b/examples/async/eratosthenes/eratosthenes_asyncio.py index a5177a94b..c7b124587 100644 --- a/examples/async/eratosthenes/eratosthenes_asyncio.py +++ b/examples/async/eratosthenes/eratosthenes_asyncio.py @@ -1,10 +1,12 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from PySide6.QtCore import (Qt, QEvent, QObject, QTimer, Signal, Slot) +from PySide6.QtCore import (Qt, QObject, Signal, Slot) from PySide6.QtGui import (QColor, QFont, QPalette) from PySide6.QtWidgets import (QApplication, QGridLayout, QLabel, QMainWindow, QVBoxLayout, QWidget) +from PySide6.QtAsyncio import QAsyncioEventLoopPolicy + import asyncio import signal import sys @@ -125,79 +127,6 @@ class Eratosthenes(QObject): self.done_signal.emit() -class AsyncHelper(QObject): - - class ReenterQtObject(QObject): - """ This is a QObject to which an event will be posted, allowing - asyncio to resume when the event is handled. event.fn() is - the next entry point of the asyncio event loop. """ - def event(self, event): - if event.type() == QEvent.Type.User + 1: - event.fn() - return True - return False - - class ReenterQtEvent(QEvent): - """ This is the QEvent that will be handled by the ReenterQtObject. - self.fn is the next entry point of the asyncio event loop. """ - def __init__(self, fn): - super().__init__(QEvent.Type(QEvent.Type.User + 1)) - self.fn = fn - - def __init__(self, worker, entry): - super().__init__() - self.reenter_qt = self.ReenterQtObject() - self.entry = entry - self.loop = asyncio.new_event_loop() - self.done = False - - self.worker = worker - if hasattr(self.worker, "start_signal") and isinstance(self.worker.start_signal, Signal): - self.worker.start_signal.connect(self.on_worker_started) - if hasattr(self.worker, "done_signal") and isinstance(self.worker.done_signal, Signal): - self.worker.done_signal.connect(self.on_worker_done) - - @Slot() - def on_worker_started(self): - """ To use asyncio and Qt together, one must run the asyncio - event loop as a "guest" inside the Qt "host" event loop. """ - if not self.entry: - raise Exception("No entry point for the asyncio event loop was set.") - asyncio.set_event_loop(self.loop) - self.loop.create_task(self.entry()) - self.loop.call_soon(self.next_guest_run_schedule) - self.done = False # Set this explicitly as we might want to restart the guest run. - self.loop.run_forever() - - @Slot() - def on_worker_done(self): - """ When all our current asyncio tasks are finished, we must end - the "guest run" lest we enter a quasi idle loop of switching - back and forth between the asyncio and Qt loops. We can - launch a new guest run by calling launch_guest_run() again. """ - self.done = True - - def continue_loop(self): - """ This function is called by an event posted to the Qt event - loop to continue the asyncio event loop. """ - if not self.done: - self.loop.call_soon(self.next_guest_run_schedule) - self.loop.run_forever() - - def next_guest_run_schedule(self): - """ This function serves to pause and re-schedule the guest - (asyncio) event loop inside the host (Qt) event loop. It is - registered in asyncio as a callback to be called at the next - iteration of the event loop. When this function runs, it - first stops the asyncio event loop, then by posting an event - on the Qt event loop, it both relinquishes to Qt's event - loop and also schedules the asyncio event loop to run again. - Upon handling this event, a function will be called that - resumes the asyncio event loop. """ - self.loop.stop() - QApplication.postEvent(self.reenter_qt, self.ReenterQtEvent(self.continue_loop)) - - if __name__ == "__main__": rows = 40 cols = 40 @@ -206,15 +135,11 @@ if __name__ == "__main__": app = QApplication(sys.argv) main_window = MainWindow(rows, cols) eratosthenes = Eratosthenes(num, main_window) - async_helper = AsyncHelper(eratosthenes, eratosthenes.start) - - # This establishes the entry point for the asyncio guest run. It - # varies depending on how and when its event loop is to be - # triggered, e.g., from the beginning (as here) or rather at a - # specific moment like a button press. - QTimer.singleShot(0, async_helper.on_worker_started) main_window.show() signal.signal(signal.SIGINT, signal.SIG_DFL) - app.exec() + + asyncio.set_event_loop_policy(QAsyncioEventLoopPolicy()) + asyncio.ensure_future(eratosthenes.start()) + asyncio.get_event_loop().run_forever() diff --git a/examples/async/minimal/doc/minimal.rst b/examples/async/minimal/doc/minimal.rst index 5a1cf8544..10af70910 100644 --- a/examples/async/minimal/doc/minimal.rst +++ b/examples/async/minimal/doc/minimal.rst @@ -6,22 +6,16 @@ The Python language provides keywords for asynchronous operations, i.e., event loop (see `PEP 492 <https://peps.python.org/pep-0492/>`_). It is up to packages to implement an event loop, support for these keywords, and more. -The best-known package for this is `asyncio`. Since both an async package and -Qt itself work with event loops, special care must be taken to ensure that both -event loops work with each other. asyncio offers a function `stop` that allows -stopping an event loop without closing it. If it is called while a loop is -running through `run_forever`, the loop will run the current batch of callbacks -and then exit. New callbacks wil be scheduled the next time `run_forever` is -called. +The best-known package for this is `asyncio`. asyncio offers an API that allows +for the asyncio event loop to be replaced by a custom implementation. Such an +implementation is available with the `QtAsyncio` module. It is based on Qt and +uses Qt's event loop in the backend. -This approach is highly experimental and does not represent the state of the -art of integrating Qt with asyncio. Instead it should rather be regarded more -as a proof of concept to contrast asyncio with other async packages such as -`trio`, which offers a dedicated `low-level API +`trio` is another popular package that offers a dedicated `low-level API <https://trio.readthedocs.io/en/stable/reference-lowlevel.html>`_ for more -complicated use cases such as this. Specifically, there exists a function -`start_guest_run` that enables running the Trio event loop as a "guest" inside -another event loop - Qt's in our case. +complex use cases. Specifically, there exists a function `start_guest_run` that +enables running the Trio event loop as a "guest" inside another event loop - +Qt's in our case, standing in contrast to asyncio's approach. Based on this functionality, two examples for async usage with Qt have been implemented: `eratosthenes` and `minimal`: @@ -38,14 +32,6 @@ implemented: `eratosthenes` and `minimal`: boilerplate code is essential for an async program with Qt and offers a starting point for more complex programs. -Both examples feature: - -1. A window class. -2. An `AsyncHelper` class containing `start_guest_run` plus helpers and - callbacks necessary for its invocation. The entry point for the Trio/asyncio - guest run is provided as an argument from outside, which can be any async - function. - While `eratosthenes` offloads the asynchronous logic that will run in trio's/asyncio's event loop into a separate class, `minimal` demonstrates that async functions can be integrated into any class, including subclasses of Qt diff --git a/examples/async/minimal/minimal_asyncio.py b/examples/async/minimal/minimal_asyncio.py index 80c81da3b..a66e07ef6 100644 --- a/examples/async/minimal/minimal_asyncio.py +++ b/examples/async/minimal/minimal_asyncio.py @@ -1,9 +1,11 @@ # Copyright (C) 2022 The Qt Company Ltd. # SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause -from PySide6.QtCore import (Qt, QEvent, QObject, Signal, Slot) +from PySide6.QtCore import (Qt, QObject, Signal, Slot) from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget) +from PySide6.QtAsyncio import QAsyncioEventLoopPolicy + import asyncio import signal import sys @@ -12,7 +14,6 @@ import sys class MainWindow(QMainWindow): start_signal = Signal() - done_signal = Signal() def __init__(self): super().__init__() @@ -36,80 +37,20 @@ class MainWindow(QMainWindow): async def set_text(self): await asyncio.sleep(1) self.text.setText("What do you get if you multiply six by nine?") - self.done_signal.emit() class AsyncHelper(QObject): - class ReenterQtObject(QObject): - """ This is a QObject to which an event will be posted, allowing - asyncio to resume when the event is handled. event.fn() is - the next entry point of the asyncio event loop. """ - def event(self, event): - if event.type() == QEvent.Type.User + 1: - event.fn() - return True - return False - - class ReenterQtEvent(QEvent): - """ This is the QEvent that will be handled by the ReenterQtObject. - self.fn is the next entry point of the asyncio event loop. """ - def __init__(self, fn): - super().__init__(QEvent.Type(QEvent.Type.User + 1)) - self.fn = fn - def __init__(self, worker, entry): super().__init__() - self.reenter_qt = self.ReenterQtObject() self.entry = entry - self.loop = asyncio.new_event_loop() - self.done = False - self.worker = worker if hasattr(self.worker, "start_signal") and isinstance(self.worker.start_signal, Signal): self.worker.start_signal.connect(self.on_worker_started) - if hasattr(self.worker, "done_signal") and isinstance(self.worker.done_signal, Signal): - self.worker.done_signal.connect(self.on_worker_done) @Slot() def on_worker_started(self): - """ To use asyncio and Qt together, one must run the asyncio - event loop as a "guest" inside the Qt "host" event loop. """ - if not self.entry: - raise Exception("No entry point for the asyncio event loop was set.") - asyncio.set_event_loop(self.loop) - self.loop.create_task(self.entry()) - self.loop.call_soon(self.next_guest_run_schedule) - self.done = False # Set this explicitly as we might want to restart the guest run. - self.loop.run_forever() - - @Slot() - def on_worker_done(self): - """ When all our current asyncio tasks are finished, we must end - the "guest run" lest we enter a quasi idle loop of switching - back and forth between the asyncio and Qt loops. We can - launch a new guest run by calling launch_guest_run() again. """ - self.done = True - - def continue_loop(self): - """ This function is called by an event posted to the Qt event - loop to continue the asyncio event loop. """ - if not self.done: - self.loop.call_soon(self.next_guest_run_schedule) - self.loop.run_forever() - - def next_guest_run_schedule(self): - """ This function serves to pause and re-schedule the guest - (asyncio) event loop inside the host (Qt) event loop. It is - registered in asyncio as a callback to be called at the next - iteration of the event loop. When this function runs, it - first stops the asyncio event loop, then by posting an event - on the Qt event loop, it both relinquishes to Qt's event - loop and also schedules the asyncio event loop to run again. - Upon handling this event, a function will be called that - resumes the asyncio event loop. """ - self.loop.stop() - QApplication.postEvent(self.reenter_qt, self.ReenterQtEvent(self.continue_loop)) + asyncio.ensure_future(self.entry()) if __name__ == "__main__": @@ -120,4 +61,6 @@ if __name__ == "__main__": main_window.show() signal.signal(signal.SIGINT, signal.SIG_DFL) - app.exec() + + asyncio.set_event_loop_policy(QAsyncioEventLoopPolicy()) + asyncio.get_event_loop().run_forever() |