diff options
Diffstat (limited to 'examples/async/minimal')
-rw-r--r-- | examples/async/minimal/doc/minimal.png | bin | 0 -> 8135 bytes | |||
-rw-r--r-- | examples/async/minimal/doc/minimal.rst | 41 | ||||
-rw-r--r-- | examples/async/minimal/minimal.py | 152 | ||||
-rw-r--r-- | examples/async/minimal/minimal.pyproject | 3 | ||||
-rw-r--r-- | examples/async/minimal/requirements.txt | 1 |
5 files changed, 197 insertions, 0 deletions
diff --git a/examples/async/minimal/doc/minimal.png b/examples/async/minimal/doc/minimal.png Binary files differnew file mode 100644 index 000000000..b8a18963f --- /dev/null +++ b/examples/async/minimal/doc/minimal.png diff --git a/examples/async/minimal/doc/minimal.rst b/examples/async/minimal/doc/minimal.rst new file mode 100644 index 000000000..a72e8a73c --- /dev/null +++ b/examples/async/minimal/doc/minimal.rst @@ -0,0 +1,41 @@ +Async examples +============== + +The Python language provides keywords for asynchronous operations, i.e., +"async" to define coroutines or "await" to schedule asynchronous calls in the +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. + +One such package is `trio`. 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. trio 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. + +Based on this functionality, two examples for async usage with Qt have been +implemented: `eratosthenes` and `minimal`: + +.. image:: minimal.png + :alt: Async example: Minimal + +* `eratosthenes` is a more extensive example that visualizes the Sieve of +Eratosthenes algorithm. This algorithm per se is not one that is particularly +suitable for asynchronous operations as it's not I/O-heavy, but synchronizing +coroutines to a configurable tick allows for a good visualization. +* `minimal` is a minimal example featuring a button that triggers an +asynchronous coroutine with a sleep. It is designed to highlight which +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 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 +event loop into a separate class, `minimal` demonstrates that async functions +can be integrated into any class, including subclasses of Qt classes. diff --git a/examples/async/minimal/minimal.py b/examples/async/minimal/minimal.py new file mode 100644 index 000000000..b816f5725 --- /dev/null +++ b/examples/async/minimal/minimal.py @@ -0,0 +1,152 @@ +############################################################################# +## +## Copyright (C) 2022 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt for Python examples of the Qt Toolkit. +## +## $QT_BEGIN_LICENSE:BSD$ +## You may use this file under the terms of the BSD license as follows: +## +## "Redistribution and use in source and binary forms, with or without +## modification, are permitted provided that the following conditions are +## met: +## * Redistributions of source code must retain the above copyright +## notice, this list of conditions and the following disclaimer. +## * Redistributions in binary form must reproduce the above copyright +## notice, this list of conditions and the following disclaimer in +## the documentation and/or other materials provided with the +## distribution. +## * Neither the name of The Qt Company Ltd nor the names of its +## contributors may be used to endorse or promote products derived +## from this software without specific prior written permission. +## +## +## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +## +## $QT_END_LICENSE$ +## +############################################################################# + +from PySide6.QtCore import (Qt, QEvent, QObject, Signal, Slot) +from PySide6.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget) + +import outcome +import signal +import sys +import traceback +import trio + + +class MainWindow(QMainWindow): + + def __init__(self, async_signal): + super().__init__() + + self.async_signal = async_signal + + widget = QWidget() + self.setCentralWidget(widget) + + layout = QVBoxLayout(widget) + + self.text = QLabel("The answer is 42.") + layout.addWidget(self.text, alignment=Qt.AlignmentFlag.AlignCenter) + + async_trigger = QPushButton(text="What is the question?") + async_trigger.clicked.connect(self.async_start) + layout.addWidget(async_trigger, alignment=Qt.AlignmentFlag.AlignCenter) + + @Slot() + def async_start(self): + self.async_signal.emit() + + async def set_text(self): + await trio.sleep(1) + self.text.setText("What do you get if you multiply six by nine?") + + +class AsyncHelper(QObject): + + trigger_signal = Signal() + + class ReenterQtObject(QObject): + """ This is a QObject to which an event will be posted, allowing + Trio to resume when the event is handled. event.fn() is the + next entry point of the Trio event loop. """ + def event(self, event): + if event.type() == QEvent.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 Trio event loop. """ + def __init__(self, fn): + super().__init__(QEvent.Type(QEvent.User + 1)) + self.fn = fn + + def __init__(self, entry=None): + super().__init__() + self.reenter_qt = self.ReenterQtObject() + self.entry = entry + + def set_entry(self, entry): + self.entry = entry + + @Slot() + def launch_guest_run(self): + """ To use Trio and Qt together, one must run the Trio event + loop as a "guest" inside the Qt "host" event loop. """ + if not self.entry: + raise Exception("No entry point for the Trio guest run was set.") + trio.lowlevel.start_guest_run( + self.entry, + run_sync_soon_threadsafe=self.next_guest_run_schedule, + done_callback=self.trio_done_callback, + ) + + def next_guest_run_schedule(self, fn): + """ This function serves to re-schedule the guest (Trio) event + loop inside the host (Qt) event loop. It is called by Trio + at the end of an event loop run in order to relinquish back + to Qt's event loop. By posting an event on the Qt event loop + that contains Trio's next entry point, it ensures that Trio's + event loop will be scheduled again by Qt. """ + QApplication.postEvent(self.reenter_qt, self.ReenterQtEvent(fn)) + + def trio_done_callback(self, outcome_): + """ This function is called by Trio when its event loop has + finished. """ + if isinstance(outcome_, outcome.Error): + error = outcome_.error + traceback.print_exception(type(error), error, error.__traceback__) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + async_helper = AsyncHelper() + main_window = MainWindow(async_helper.trigger_signal) + async_helper.set_entry(main_window.set_text) + + # This establishes the entry point for the Trio guest run. It varies + # depending on how and when its event loop is to be triggered, e.g., + # at a specific moment like a button press (as here) or rather from + # the beginning. + async_helper.trigger_signal.connect(async_helper.launch_guest_run) + + main_window.show() + + signal.signal(signal.SIGINT, signal.SIG_DFL) + app.exec() diff --git a/examples/async/minimal/minimal.pyproject b/examples/async/minimal/minimal.pyproject new file mode 100644 index 000000000..97ff6dbc5 --- /dev/null +++ b/examples/async/minimal/minimal.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["minimal.py"] +} diff --git a/examples/async/minimal/requirements.txt b/examples/async/minimal/requirements.txt new file mode 100644 index 000000000..ae0d704f0 --- /dev/null +++ b/examples/async/minimal/requirements.txt @@ -0,0 +1 @@ +trio |