Skip to main content

Interop between asyncio and Qt for Python

Project description

qtinter — Interop between asyncio and Qt for Python

build docs tests codecov PyPI

qtinter is a Python module that brings together asyncio and Qt for Python, allowing you to use one from the other seamlessly.

Quickstart

Installation

$ pip install qtinter

Using asyncio from Qt

To use asyncio-based libraries in Qt for Python, enclose app.exec() inside context manager qtinter.using_asyncio_from_qt(), and optionally connect Qt signals to coroutine functions using qtinter.asyncslot().

Minimal example (taken from examples/sleep.py):

import asyncio
import qtinter  # <-- import module
from PyQt6 import QtWidgets

async def sleep():
    button.setEnabled(False)
    await asyncio.sleep(1)
    button.setEnabled(True)

if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    button = QtWidgets.QPushButton()
    button.setText('Sleep for one second')
    button.clicked.connect(qtinter.asyncslot(sleep))  # <-- wrap coroutine function
    button.show()

    with qtinter.using_asyncio_from_qt():  # <-- enclose in context manager
        app.exec()

Using Qt from asyncio

To use Qt components from asyncio-based code, enclose the asyncio entry-point inside context manager qtinter.using_qt_from_asyncio(), and optionally wait for Qt signals using qtinter.asyncsignal().

Minimal example (taken from examples/color.py):

import asyncio
import qtinter  # <-- import module
from PyQt6 import QtWidgets

async def choose_color():
    dialog = QtWidgets.QColorDialog()
    dialog.show()
    result = await qtinter.asyncsignal(dialog.finished)  # <-- wait for signal
    if result == QtWidgets.QDialog.DialogCode.Accepted:
        return dialog.selectedColor().name()
    else:
        return None

if __name__ == "__main__":
    app = QtWidgets.QApplication([])
    with qtinter.using_qt_from_asyncio():  # <-- enclose in context manager
        color = asyncio.run(choose_color())
        if color is not None:
            print(color)

Documentation

See full documentation at qtinter.readthedocs.io.

Requirements

qtinter supports the following:

  • Python version: 3.7 or higher
  • Qt binding: PyQt5, PyQt6, PySide2, PySide6
  • Operating system: Linux, MacOS, Windows

Details

qtinter embeds a logical asyncio event loop within a physical Qt event loop (QEventLoop), so that Python libraries written for asyncio can be used by a Python for Qt application.

Running Modes

An QiBaseEventLoop may be run in attached mode or nested mode.

Use QiRunner context manager to run it in attached mode.
This mode only installs the logical asyncio event loop; the physical Qt event loop must still be run as usual, e.g. by app.exec(). This is the preferred workflow as it integrates seamlessly with an existing Qt app.

Call run_forever to run it in nested mode. This starts a (possibly nested) Qt event loop using QEventLoop.exec() and waits until it exits. This is the standard asyncio workflow and is convenient for unit testing, but it is not recommended for integration with an existing Qt app as nested event loops are advised against by Qt.

For either mode, a (global) QCoreApplication (or QApplication / QGuiApplication) instance must exist before running any coroutine, as is required by Qt.

Clean-up

To properly release the resources of the event loop after it stops, you should call shutdown_asyncgens and shutdown_default_executor, followed by close. The first two methods are actually coroutines and therefore must be run from within the event loop.

For attached mode, use the QiRunner context manager, which handles clean-up automatically. Note, however, that it actually runs the first two coroutines in nested mode, i.e. a Qt event loop is started.
Your code should be prepared for this.

For nested mode, asyncio.run() handles clean-up automatically.

The asyncslot Adaptor

qtinter.asyncslot wraps a coroutine function (one defined by async def) to make it usable as a Qt slot. Without wrapping, a coroutine function (whether decorated with QtCore.Slot/PyQt6.pyqtSlot or not) generally cannot be used as a slot because calling it merely returns a coroutine object instead of performing real work.

Under the hood, qtinter.asyncslot calls QiBaseEventLoop.run_task, a custom method which creates a Task wrapping the coroutine and executes it immediately until the first suspension point.

This is designed to work with a common pattern where some work has to be performed immediately in response to a signal. For example, the clicked handler of a "Send Order" button normally disables the button on entry before actually sending the order over network, to avoid sending duplicate orders. For this to work correctly, the code until the first suspension point must be executed synchronously.

An QiBaseEventLoop must be running when a coroutine wrapped by asyncslot is called, or a RuntimeError will be raised.

It is not recommended to decorate a coroutine function with asyncslot as that would make an async def function into a normal function, which is confusing.

Cancellation

To cancel a running coroutine from within itself, raise asyncio.CancelledError.

To retrieve the Task object from within the running coroutine and store it somewhere to be used later, call asyncio.current_task() from within the running coroutine.

Implementation Notes

By embedding a (logical) asyncio event loop inside a (physical) Qt event loop, what's not changed (from the perspective of the asyncio event loop) is that all calls (other than call_soon_threadsafe) are still made from the same thread. This frees us from multi-threading complexities.

What has changed, however, is that in a standalone asyncio event loop, no code can run when the scheduler (specifically, _run_once) is blocked in select(), while in an embedded asyncio event loop, a select() call that would otherwise block yields, allowing any code to run while the loop is "logically" blocked in select.

For example, BaseEventLoop.stop() is implemented by setting the flag _stopping to True, which is then checked before the next iteration of _run_once to stop the loop. This works because stop can only ever be called from a callback, and a callback can only ever be called after select returns and before the next iteration of _run_once. The behavior changes if select yields and stop is called -- the event loop wait not wake up until some IO is available.

We refer to code that runs (from the Qt event loop) after select yields and before _run_once is called again as injected code. We must examine and handle the implications of such code.

We do this by fitting injected code execution into the standalone asyncio event loop model. Specifically, we treat injected code as if they were scheduled with call_soon_threadsafe, which wakes up the selector and executes the code. With some loss of generality, we assume no IO event nor timed callback is ready at the exact same time, so that the scheduler will be put back into blocking select immediately after the code finishes running (unless the code calls stop). This simplification is acceptable because the precise timing of multiple IO or timer events should not be relied upon.

In practice, we cannot actually wake up the asyncio scheduler every time injected code is executed, firstly because there's no way to detect their execution and secondly because doing so would be highly inefficient. Instead, we assume that injected code which does not access the event loop object or its selector is benign enough to be treated as independent from the asyncio event loop ecosystem and may be safely ignored.

This leaves us to just consider injected code that accesses the event loop object or its selector and examine its impact on scheduling. The scheduler depends on three things: the _ready queue for "soon" callbacks, the _scheduled queue for timer callbacks, and _selector for IO events. If the injected code touches any of these things, it needs to be handled.

While the public interface of AbstractEventLoop has numerous methods, the methods that modify those three things boil down to call_soon, call_at, call_later, (arguably) stop, and anything that modifies the selector (proactor). When any of these happens, we physically or logically wake up the selector to simulate a call_soon_threadsafe call.

History

asyncslot is derived from qasync but rewritten from scratch. qasync is derived from asyncqt, which is derived from quamash.

License

BSD License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

qtinter-0.5.0.tar.gz (214.0 kB view hashes)

Uploaded Source

Built Distribution

qtinter-0.5.0-py3-none-any.whl (23.8 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page