diff options
author | Adrian Herrmann <adrian.herrmann@qt.io> | 2022-10-06 14:02:52 +0200 |
---|---|---|
committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2022-12-09 20:59:45 +0000 |
commit | 9fc6f33fd5024056d45825836ac037b736f5531e (patch) | |
tree | 4d63494581c92d5ab1fd16b2a1cce54bae726b34 | |
parent | a0b574978e8beae54d617e07d95a35b2aa856bdf (diff) |
examples: Add asyncio versions of async examples
The minimal + eratosthenes examples for async were previously based on
the Trio async package, now there are also versions based on the asyncio
package with minimal changes.
Task-number: PYSIDE-769
Change-Id: I5c1220e1c8ebeefb9a530745e52d8f907e08bb2c
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
(cherry picked from commit 3d036e0aab8fe1f835e18136573ffa8ad0359ff1)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r-- | examples/async/eratosthenes/doc/eratosthenes.rst | 29 | ||||
-rw-r--r-- | examples/async/eratosthenes/eratosthenes.pyproject | 2 | ||||
-rw-r--r-- | examples/async/eratosthenes/eratosthenes_asyncio.py | 203 | ||||
-rw-r--r-- | examples/async/eratosthenes/eratosthenes_trio.py (renamed from examples/async/eratosthenes/eratosthenes.py) | 0 | ||||
-rw-r--r-- | examples/async/minimal/doc/minimal.rst | 29 | ||||
-rw-r--r-- | examples/async/minimal/minimal.pyproject | 2 | ||||
-rw-r--r-- | examples/async/minimal/minimal_asyncio.py | 118 | ||||
-rw-r--r-- | examples/async/minimal/minimal_trio.py (renamed from examples/async/minimal/minimal.py) | 0 |
8 files changed, 363 insertions, 20 deletions
diff --git a/examples/async/eratosthenes/doc/eratosthenes.rst b/examples/async/eratosthenes/doc/eratosthenes.rst index fdb095a91..494a94df3 100644 --- a/examples/async/eratosthenes/doc/eratosthenes.rst +++ b/examples/async/eratosthenes/doc/eratosthenes.rst @@ -6,9 +6,18 @@ 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. -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 +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. + +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 <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 @@ -34,9 +43,11 @@ 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. + 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 +classes. diff --git a/examples/async/eratosthenes/eratosthenes.pyproject b/examples/async/eratosthenes/eratosthenes.pyproject index 8ea189b3c..f0de627ef 100644 --- a/examples/async/eratosthenes/eratosthenes.pyproject +++ b/examples/async/eratosthenes/eratosthenes.pyproject @@ -1,3 +1,3 @@ { - "files": ["eratosthenes.py"] + "files": ["eratosthenes_trio.py", "eratosthenes_asyncio.py"] } diff --git a/examples/async/eratosthenes/eratosthenes_asyncio.py b/examples/async/eratosthenes/eratosthenes_asyncio.py new file mode 100644 index 000000000..87d56e6fa --- /dev/null +++ b/examples/async/eratosthenes/eratosthenes_asyncio.py @@ -0,0 +1,203 @@ +# 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.QtGui import (QColor, QFont, QPalette) +from PySide6.QtWidgets import (QApplication, QGridLayout, QLabel, QMainWindow, QVBoxLayout, QWidget) + +import asyncio +import outcome +import signal +import sys +import traceback +from random import randint + + +class MainWindow(QMainWindow): + + set_num = Signal(int, QColor) + + def __init__(self, rows, cols): + super().__init__() + + self.rows = rows + self.cols = cols + + widget_central = QWidget() + self.setCentralWidget(widget_central) + + layout_outer = QVBoxLayout(widget_central) + + self.widget_outer_text = QLabel() + font = QFont() + font.setPointSize(14) + self.widget_outer_text.setFont(font) + layout_outer.addWidget(self.widget_outer_text, alignment=Qt.AlignmentFlag.AlignCenter) + + widget_inner_grid = QWidget() + layout_outer.addWidget(widget_inner_grid, alignment=Qt.AlignmentFlag.AlignCenter) + + self.layout_inner_grid = QGridLayout(widget_inner_grid) + k = 1 + for i in range(self.rows): + for j in range(self.cols): + box = QLabel(f"{k}") + self.layout_inner_grid.addWidget(box, i, j, Qt.AlignmentFlag.AlignCenter) + k += 1 + + self.set_num.connect(self.set_num_handler) + + @Slot(int, QColor) + def set_num_handler(self, i, color): + row = int((i - 1) / self.cols) + col = (i - 1) - (row * self.cols) + widget = self.layout_inner_grid.itemAtPosition(row, col).widget() + + font = QFont() + font.setWeight(QFont.Bold) + palette = QPalette() + palette.setColor(QPalette.WindowText, color) + widget.setFont(font) + widget.setPalette(palette) + + +class Eratosthenes(): + + """ This Sieve of Eratosthenes runs on a configurable tick (default + 0.1 seconds). At each tick, a new subroutine will be created + that will check multiples of the next prime number. Each of + these subroutines also operates on the same tick. """ + + def __init__(self, num, window, tick=0.1): + self.num = num + self.sieve = [True] * self.num + self.base = 0 + self.window = window + self.tick = tick + self.coroutines = [] + self.done = False + self.loop = None + + def get_tick(self): + return self.loop.time() + self.tick + + async def start(self): + self.loop = asyncio.get_event_loop() + asyncio.create_task(self.update_text()) + while self.base <= self.num / 2: + await asyncio.sleep(self.tick) + for i in range(self.base + 1, self.num): + if self.sieve[i]: + self.base = i + break + asyncio.create_task(self.mark_number(self.base + 1)) + while sum(self.coroutines) > 0: + await asyncio.sleep(self.tick) + self.done = True + + async def mark_number(self, base): + id = len(self.coroutines) + self.coroutines.append(1) + color = QColor(randint(64, 192), randint(64, 192), randint(64, 192)) + for i in range(2 * base, self.num + 1, base): + if self.sieve[i - 1]: + self.sieve[i - 1] = False + self.window.set_num.emit(i, color) + await asyncio.sleep(self.tick) + self.coroutines[id] = 0 + + async def update_text(self): + while not self.done: + await asyncio.sleep(self.tick) + if int(self.loop.time() + self.tick) % 2: + text = "⚙️ ...Calculating prime numbers... ⚙️" + else: + text = "👩💻 ...Hacking the universe... 👩💻" + self.window.widget_outer_text.setText(text) + + self.window.widget_outer_text.setText( + "🥳 Congratulations! You found all the prime numbers and solved mathematics. 🥳" + ) + + +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 + self.loop = asyncio.new_event_loop() + + def set_entry(self, entry): + self.entry = entry + + @Slot() + def launch_guest_run(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.loop.run_forever() + + def continue_loop(self): + """ This function is called by an event posted to the Qt event + loop to restart the asyncio event loop. """ + 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 + num = rows * cols + + app = QApplication(sys.argv) + main_window = MainWindow(rows, cols) + eratosthenes = Eratosthenes(num, main_window) + async_helper = AsyncHelper(entry=eratosthenes.start) + + # 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., + # from the beginning (as here) or rather at a specific moment like + # a button press. + QTimer.singleShot(0, async_helper.launch_guest_run) + + main_window.show() + + signal.signal(signal.SIGINT, signal.SIG_DFL) + app.exec() diff --git a/examples/async/eratosthenes/eratosthenes.py b/examples/async/eratosthenes/eratosthenes_trio.py index 5fb2a35be..5fb2a35be 100644 --- a/examples/async/eratosthenes/eratosthenes.py +++ b/examples/async/eratosthenes/eratosthenes_trio.py diff --git a/examples/async/minimal/doc/minimal.rst b/examples/async/minimal/doc/minimal.rst index dcab2fe29..5a1cf8544 100644 --- a/examples/async/minimal/doc/minimal.rst +++ b/examples/async/minimal/doc/minimal.rst @@ -6,9 +6,18 @@ 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. -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 +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. + +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 <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 @@ -33,9 +42,11 @@ 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. + 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 +classes. diff --git a/examples/async/minimal/minimal.pyproject b/examples/async/minimal/minimal.pyproject index 97ff6dbc5..b4b1a3f80 100644 --- a/examples/async/minimal/minimal.pyproject +++ b/examples/async/minimal/minimal.pyproject @@ -1,3 +1,3 @@ { - "files": ["minimal.py"] + "files": ["minimal_trio.py", "minimal_asyncio.py"] } diff --git a/examples/async/minimal/minimal_asyncio.py b/examples/async/minimal/minimal_asyncio.py new file mode 100644 index 000000000..337a2558d --- /dev/null +++ b/examples/async/minimal/minimal_asyncio.py @@ -0,0 +1,118 @@ +# 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.QtWidgets import (QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget) + +import asyncio +import outcome +import signal +import sys +import traceback + + +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 asyncio.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 + self.loop = asyncio.new_event_loop() + + def set_entry(self, entry): + self.entry = entry + + @Slot() + def launch_guest_run(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.loop.run_forever() + + def continue_loop(self): + """ This function is called by an event posted to the Qt event + loop to restart the asyncio event loop. """ + 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__": + 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.py b/examples/async/minimal/minimal_trio.py index 1769a2dfc..1769a2dfc 100644 --- a/examples/async/minimal/minimal.py +++ b/examples/async/minimal/minimal_trio.py |