aboutsummaryrefslogtreecommitdiffstats
path: root/examples
diff options
context:
space:
mode:
authorAdrian Herrmann <adrian.herrmann@qt.io>2022-10-06 14:02:52 +0200
committerAdrian Herrmann <adrian.herrmann@qt.io>2022-12-09 20:07:36 +0100
commit3d036e0aab8fe1f835e18136573ffa8ad0359ff1 (patch)
treeb3e5c75775f4a17b5bcedb58e6de92214663844f /examples
parent7fcf5d83d7035fdcdf9d60670cd0bce64ae32e64 (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 Pick-to: 6.3 6.4 Change-Id: I5c1220e1c8ebeefb9a530745e52d8f907e08bb2c Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'examples')
-rw-r--r--examples/async/eratosthenes/doc/eratosthenes.rst29
-rw-r--r--examples/async/eratosthenes/eratosthenes.pyproject2
-rw-r--r--examples/async/eratosthenes/eratosthenes_asyncio.py203
-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.rst29
-rw-r--r--examples/async/minimal/minimal.pyproject2
-rw-r--r--examples/async/minimal/minimal_asyncio.py118
-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