Giter Site home page Giter Site logo

qtrio's Introduction

QTrio - a library bringing Qt GUIs together with async and await via Trio

Resources

Documentation Read the Docs Documentation
Chat Gitter Support chatroom
Forum Discourse Support forum
Issues GitHub Issues
Repository GitHub Repository
Tests GitHub Actions Tests
Coverage Codecov Test coverage
Distribution PyPI
Latest distribution version
Supported Python versions
Supported Python interpreters

Introduction

Note:
This library is in early development. It works. It has tests. It has documentation. Expect breaking changes as we explore a clean API. By paying this price you get the privilege to provide feedback via GitHub issues to help shape our future. :]

The QTrio project's goal is to bring the friendly concurrency of Trio using Python's async and await syntax together with the GUI features of Qt to enable more correct code and a more pleasant developer experience. QTrio is permissively licensed to avoid introducing restrictions beyond those of the underlying Python Qt library you choose. Both PySide2 and PyQt5 are supported.

By enabling use of async and await it is possible in some cases to write related code more concisely and clearly than you would get with the signal and slot mechanisms of Qt concurrency. In this set of small examples we will allow the user to input their name then use that input to generate an output message. The user will be able to cancel the input to terminate the program early. In the first example we will do it in the form of a classic "hello" console program. Well, classic plus a bit of boilerplate to allow explicit testing without using special external tooling. Then second, the form of a general Qt program implementing this same activity. And finally, the QTrio way.

# A complete runnable source file with imports and helpers is available in
# either the documentation readme examples or in the repository under
# qtrio/examples/readme/console.py.

def main(
    input_file: typing.TextIO = sys.stdin, output_file: typing.TextIO = sys.stdout
) -> None:
    try:
        output_file.write("What is your name? ")
        output_file.flush()
        name = input_file.readline()[:-1]
        output_file.write(f"Hi {name}, welcome to the team!\n")
    except KeyboardInterrupt:
        pass

Nice and concise, including the cancellation via ctrl+c. This is because we can stay in one scope thus using both local variables and a try/except block. This kind of explodes when you shift into a classic Qt GUI setup.

# A complete runnable source file with imports and helpers is available in
# either the documentation readme examples or in the repository under
# qtrio/examples/readme/qt.py.

class Main:
    def __init__(
        self,
        application: QtWidgets.QApplication,
        input_dialog: typing.Optional[QtWidgets.QInputDialog] = None,
        output_dialog: typing.Optional[QtWidgets.QMessageBox] = None,
    ):
        self.application = application

        if input_dialog is None:  # pragma: no cover
            input_dialog = create_input()

        if output_dialog is None:  # pragma: no cover
            output_dialog = create_output()

        self.input_dialog = input_dialog
        self.output_dialog = output_dialog

    def setup(self) -> None:
        self.input_dialog.accepted.connect(self.input_accepted)
        self.input_dialog.rejected.connect(self.input_rejected)

        self.input_dialog.show()

    def input_accepted(self) -> None:
        name = self.input_dialog.textValue()

        self.output_dialog.setText(f"Hi {name}, welcome to the team!")

        self.output_dialog.finished.connect(self.output_finished)
        self.output_dialog.show()

    def input_rejected(self) -> None:
        self.application.quit()

    def output_finished(self) -> None:
        self.application.quit()

The third example, below, shows how using async and await allows us to return to the more concise and clear description of the sequenced activity. Most of the code is just setup for testability with only the last four lines really containing the activity.

# A complete runnable source file with imports and helpers is available in
# either the documentation readme examples or in the repository under
# qtrio/examples/readme/qtrio_example.py.

async def main(
    *,
    task_status: trio_typing.TaskStatus[Dialogs] = trio.TASK_STATUS_IGNORED,
) -> None:
    dialogs = Dialogs()
    task_status.started(dialogs)

    with contextlib.suppress(qtrio.UserCancelledError):
        name = await dialogs.input.wait()
        dialogs.output.text = f"Hi {name}, welcome to the team!"
        await dialogs.output.wait()

qtrio's People

Contributors

altendky avatar firelightflagboy avatar gordon-epc avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

qtrio's Issues

Tidy `._core.Runner.trio_done()`

It is presently printing a repr() and a traceback. Decide what it should really be doing and implement. Maybe optionally unwrapping?

Using duck-typed signals (non-Qt) - can this cause problems?

Along with Qt Signal, I also use my own homegrown version which is api-compatible (connect/disconnect/emit), but does not use Qt code. Such signals are handy because some controllers can be used in a CLI-only applications (code reuse).

Not being very familiar with type hints, just getting a bit worried. My understanding is that runtime nothing bad can happen. But maybe UI (PyCharm?) or static type checkers will flag errors?

Question: do you see any problem with duck-typed non-Qt signals?

Contribution discussion

Docs. Readme too?

I am reasonably set on the direction of Trio first and Qt being cut down from an application framework to more of a GUI-as-I/O library. Beyond that, most things are fair game for debate. Names, tag line, implementations, etc. And even within the main direction, I would still expect to support callbacks in some form, probably, even though not as the primary intended usage.

Sometimes have open_emissions_channel manage the read channel context

async with qtrio.open_emissions_channel(signals=[a_signal]) as emissions:
    async with emissions.channel:
        async for emissions in emissions.channel:
            print(emissions)

would often rather be just

async with qtrio.open_emissions_channel(signals=[a_signal]) as emissions:
    async for emissions in emissions.channel:
        print(emissions)

Maybe just separate function names to do each? Or even...

async for emissions in qtrio.open_emissions_channel(signals=[a_signal]):
    print(emissions)

Deal properly with readme examples...

The examples in the readme can't be copied and pasted and run. They do not include imports, helpers, launchers... I think this may be ok, but it should be flagrantly obvious how to get an actually _runnable_ version. They are already included in the docs under examples but we need links at least. At the moment I tend to think that including the full file content in the readme is excessive? But I do also like complete examples so maybe someone can talk me into it.

So, how do we get there... it seems trivial but... let's take a little related detour and come back.

The readme examples are a little smelly right now in that they are just manually copied from the example .py files. The other entries in the docs use the reStructuredText include directive so they don't get out of date. GitHub declines to support this though in GitHub .rst rendering so the readme there can't use the include directive. I don't know how it works but there's similar consideration for the PyPI page. There's no indication the GitHub issue will be resolved... it's from 2012 and rejected.

So the basic difficulty is in how to get functional links in each of GitHub, PyPI and Read the Docs. A few months back there was an exploration of having the readme be generated from documentation using Sphinx with .rst output but we ran into various issues with the associated plugins, or something... #221

tl;dr doing this right should be simple, but isn't given the requirements.

test_overrunning_test_times_out is flaky

FAILED C:\hostedtoolcache\windows\Python\3.6.8\x86\lib\site-packages\qtrio_tests\test_pytest.py::test_overrunning_test_times_out

https://github.com/altendky/qtrio/pull/50/checks?check_run_id=828834279

2020-07-02T01:11:11.5061202Z E               subprocess.TimeoutExpired: Command '('c:\\hostedtoolcache\\windows\\python\\3.6.8\\x86\\python.exe', '-mpytest', '--basetemp=C:\\Users\\runneradmin\\AppData\\Local\\Temp\\pytest-of-runneradmin\\pytest-0\\test_overrunning_test_times_out0\\runpytest-0')' timed out after 6 seconds

At first glance it looks like @qtrio.host isn't timing out properly thus resulting in a subprocess timeout instead. It doesn't seem like the subprocess launch time should be sufficient to cause this but... usually it doesn't. So, maybe that's it and the subprocess timeout just (appropriately) needs a significant bump to reduce flakiness. Unless maybe we can come up with a better way.

def test_overrunning_test_times_out(testdir):
"""The overrunning test is timed out."""
test_file = rf"""
import qtrio
import trio
@qtrio.host
async def test(request):
await trio.sleep({2 * qtrio._pytest.timeout})
"""
testdir.makepyfile(test_file)
timeout = qtrio._pytest.timeout
result = testdir.runpytest_subprocess(timeout=2 * qtrio._pytest.timeout)
result.assert_outcomes(failed=1)
result.stdout.re_match_lines(
lines2=[f"E AssertionError: test not finished within {timeout} seconds"],
)

Figure out what tests to subprocess

def test_hosted_assertion_failure_fails(testdir):
"""QTrio hosted test which fails an assertion fails the test."""
test_file = r"""
import qtrio
@qtrio.host
async def test(request):
assert False
"""
testdir.makepyfile(test_file)
result = testdir.runpytest_subprocess(timeout=10)
result.assert_outcomes(failed=1)

Various tests are written to subprocess to the real test. I think there are places this makes sense and more that it probably doesn't. I think that the single-repo nature here is part of the problem. We create the basic qtrio.Runner feature, then use it in qtrio.host() and then use that for running other tests. The tests of qtrio.host() seem likely to belong in subprocesses. Perhaps they should get a separate run first to make sure they work before running the main test suite and then not subprocess any of the rest of the tests? (well, unless they have another reason to be subprocessed)

Support inside-out guest mode

python-trio/trio#1652

It almost feels like this should be the normal approach to starting. Then it really is more like a regular old Trio application that just happens to kick off some Qt compatibility thing-a-ma-jig on the side.

qtrio.RegisterEventTypeError is problematic related to being raised at import time

qtrio/qtrio/_core.py

Lines 34 to 40 in 4fa1177

REENTER_EVENT_HINT: int = QtCore.QEvent.registerEventType()
if REENTER_EVENT_HINT == -1:
message = (
"Failed to register the event hint, either no available hints remain or the"
+ " program is shutting down."
)
raise qtrio.RegisterEventTypeError(message)

I don't know... It's nice not to have to register the type explicitly but it is also an import time side effect which isn't the coolest thing ever.

def wrapper(f, *args, wrapper_arg1) vs. def wrapper(f, args, wrapper_arg1)

The parts of Trio that I have seen use a *args approach for forwarded arguments and leave it to the caller to use functools.partial() if they want keyword arguments. So far I've been trying to stick with that but it has the limitation regarding keyword args and also can make typing hinting a bit weird. If instead positional arguments are taken in a sequence and keyword arguments in a map then they are two well defined parameters that can be passed positionally or by keyword and type hinted well.

So, how far should I stick to being Trio-like?

Came up while looking at this and trying to hint it.
https://github.com/altendky/qtrio/pull/90/files#diff-aca67cae4ab3db4c745d891483802bf2R230-R235.

Document review guidelines

If possible this would be followed by automatic checks. Maybe there already exists a good reference for this.

  • docstrings
    • modules
    • functions
    • classes
    • methods
    • tests
  • news fragment

Is qtrio._core.Outcomes.unwrap() excessive or useful?

qtrio/qtrio/_core.py

Lines 103 to 124 in 4fa1177

def unwrap(self):
"""Unwrap either the Trio or Qt outcome. First, errors are given priority over
success values. Second, the Trio outcome gets priority over the Qt outcome. If
both are still None a :class:`NoOutcomesError` is raised.
"""
if self.trio is not None:
# highest priority to the Trio outcome, if it is an error we are done
result = self.trio.unwrap()
# since a Trio result is higher priority, we only care if Qt gave an error
if self.qt is not None:
self.qt.unwrap()
# no Qt error so go ahead and return the Trio result
return result
elif self.qt is not None:
# either it is a value that gets returned or an error that gets raised
return self.qt.unwrap()
# neither Trio nor Qt outcomes have been set so we have nothing to unwrap()
raise qtrio.NoOutcomesError()

macOS native dialogs don't support setting directory

Need to report this to Qt. Non-native dialog works.

1197:preqtrio administrator$ venv/bin/python x.py
/Users/administrator/preqtrio/blue

image

import os
import pathlib

from PyQt5 import QtWidgets

p = (pathlib.Path('blue') / 'red').resolve()
parent = p.parent
parent.mkdir(parents=True, exist_ok=True)

app = QtWidgets.QApplication([])
if not parent.is_dir():
    raise Exception('parent is not a directory')
s = os.fspath(parent)
print(s)
dialog = QtWidgets.QFileDialog(directory=s)
dialog.selectFile(os.fspath(p))
dialog.show()
app.exec_()

Support Qt6

As of just over a month ago, Qt6 came out. How much work is necessary to add support for Qt6?

What about QML

At least explore what QML looks like and how it would be supported. Even if it just works as is there should be an example.

Explain README example purpose

The first example here shows classic pure Qt code.

It would be nice to have a short explanation of what the example is trying to accomplish. For those unfamiliar with Qt :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.