Getting Started

This tutorial introduces usage of QTrio to enable integration of Qt into a Trio async application. For help with relevant async concepts and usage of Trio itself see the Trio tutorial.

I know, I know… we are supposed to do one thing well. But QTrio presently targets three distinct development tools. In time perhaps pieces will be spun off but for now they provide increasing layers you can use or not as they interest you.

Installation

While the general aspects of installation using pip belong elsewhere, it is recommended to work in a virtual environment such as you can create with the venv module (see also Python Virtual Environments in Five Minutes ).

Somewhat more specific to QTrio, several extras are available for installing optional dependencies or applying version constraints.

  • cli - For CLI usage, presently just examples.

  • examples - For running examples.

  • pyqt5 - For running with PyQt5, primarily to apply any version constraints.

  • pyside2 - For running with PySide2, primarily to apply any version constraints.

A normal installation might look like:

$ myenv/bin/pip install qtrio[pyside2]

Overview

The first layer allows you to run Trio tasks in the same thread as the Qt event loop. This is valuable as it let’s the tasks safely interact directly with the Qt GUI objects. It is a small wrapper around Trio’s guest mode. This layer is exposed directly under the qtrio package.

Now that Qt and Trio are friends we can focus on making the relationship smoother. This second layer of QTrio is also available directly in the qtrio package and allows for awaiting signals and iterating over the emissions of signals. This avoids the normal callback design of GUI systems in favor of Trio’s structured concurrency allowing GUI responses to be handled where you want within the task tree.

Not everything Qt provides will be easily integrated into this structure. The rest of QTrio will grow to contain helpers and wrappers to address these cases.

In addition to the above three layers there is also adjacent support for testing.

Layer 1 - Crossing Paths

With one extra character you can turn trio.run() into qtrio.run(). This gets you the Trio guest mode hosted by a Qt QApplication. Note how there is only one function and you are able to asynchronously sleep in it to avoid blocking the GUI. By default, when you leave your main function the Qt application will be exited.

import typing

import qtrio
from qtpy import QtWidgets
import trio


async def main(
    label: typing.Optional[QtWidgets.QWidget] = None,
    message: str = "Hello world.",
    change_delay: float = 0.5,
    close_delay: float = 3,
) -> None:
    if label is None:  # pragma: no cover
        label = QtWidgets.QLabel()
    # start big enough to fit the whole message
    label.setText(message)
    label.show()
    label.setText("")

    for i in range(len(message)):
        await trio.sleep(change_delay)
        label.setText(message[: i + 1])

    await trio.sleep(close_delay)


if __name__ == "__main__":  # pragma: no cover
    qtrio.run(main)

Layer 2 - Building Respect

A good relationship goes both ways. Above, Trio did all the talking and Qt just listened. Now let’s have Trio listen to Qt. Emissions from Qt signals can be made available in a trio.MemoryReceiveChannel. You can either trio.MemoryReceiveChannel.receive() them one at a time or asynchronously iterate over them for longer lasting activities. The received object is a qtrio.Emission and contains both the originating signal and the arguments.

import typing

import attr
import qtrio
from qtpy import QtWidgets


@attr.s(auto_attribs=True)
class Widget:
    widget: QtWidgets.QWidget = attr.ib(factory=QtWidgets.QWidget)
    layout: QtWidgets.QLayout = attr.ib(factory=QtWidgets.QVBoxLayout)
    button: QtWidgets.QPushButton = attr.ib(factory=QtWidgets.QPushButton)
    label: QtWidgets.QWidget = attr.ib(factory=QtWidgets.QLabel)

    def setup(self, message: str) -> None:
        self.button.setText("More")

        # start big enough to fit the whole message
        self.label.setText(message)

        self.layout.addWidget(self.button)
        self.layout.addWidget(self.label)
        self.widget.setLayout(self.layout)

    def show(self) -> None:
        self.widget.show()
        self.label.setText("")


async def main(
    widget: typing.Optional[Widget] = None,
    message: str = "Hello world.",
) -> None:
    if widget is None:  # pragma: no cover
        widget = Widget()

    widget.setup(message=message)

    async with qtrio.enter_emissions_channel(
        signals=[widget.button.clicked]
    ) as emissions:
        i = 1
        widget.show()

        async for _ in emissions.channel:  # pragma: no branch
            widget.label.setText(message[:i])
            i += 1

            if i > len(message):
                break

        # wait for another click to finish
        await emissions.channel.receive()


if __name__ == "__main__":  # pragma: no cover
    qtrio.run(main)

Layer 3 - Best Friends

This space intentionally left blank.

(for now… sorry)