Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve examples and README #102

Merged
merged 21 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
59 changes: 54 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,68 @@

## Introduction

`qasync` allows coroutines to be used in PyQt/PySide applications by providing an implementation of the `PEP 3156` event-loop.
`qasync` allows coroutines to be used in PyQt/PySide applications by providing an implementation of the `PEP 3156` event loop.

`qasync` is a fork of [asyncqt](https://github.com/gmarull/asyncqt), which is a fork of [quamash](https://github.com/harvimt/quamash). May it live longer than its predecessors.
With `qasync`, you can use `asyncio` functionalities directly inside Qt app's event loop, in the main thread. Using async functions for Python tasks can be much easier and cleaner than using `threading.Thread` or `QThread`.

#### The future of `qasync`
If you need some CPU-intensive tasks to be executed in parallel, `qasync` also got that covered, providing `QEventLoop.run_in_executor` which is functionally identical to that of `asyncio`.

`qasync` was created because `asyncqt` and `quamash` are no longer maintained.
### Basic Example

```python
import sys
import asyncio

from qasync import QEventLoop, QApplication
from PySide6.QtWidgets import QWidget, QVBoxLayout

class MainWindow(QWidget):
def __init__(self):
super().__init__()

self.setLayout(QVBoxLayout())
self.lbl_status = QLabel("Idle", self)
self.layout().addWidget(self.lbl_status)

@asyncClose
async def closeEvent(self, event):
pass

@asyncSlot()
async def onMyEvent(self):
pass


if __name__ == "__main__":
app = QApplication(sys.argv)

event_loop = QEventLoop(app)
asyncio.set_event_loop(event_loop)

app_close_event = asyncio.Event()
app.aboutToQuit.connect(app_close_event.set)

main_window = MainWindow()
main_window.show()

with event_loop:
event_loop.run_until_complete(app_close_event.wait())
```

More detailed examples can be found [here](https://github.com/CabbageDevelopment/qasync/tree/master/examples).

### The Future of `qasync`

`qasync` is a fork of [asyncqt](https://github.com/gmarull/asyncqt), which is a fork of [quamash](https://github.com/harvimt/quamash). `qasync` was created because those are no longer maintained. May it live longer than its predecessors.

**`qasync` will continue to be maintained, and will still be accepting pull requests.**

## Requirements

`qasync` requires Python >= 3.8, and PyQt5/PyQt6 or PySide2/PySide6. The library is tested on Ubuntu, Windows and MacOS.
- Python >= 3.8
- PyQt5/PyQt6 or PySide2/PySide6

`qasync` is tested on Ubuntu, Windows and MacOS.

If you need Python 3.6 or 3.7 support, use the [v0.25.0](https://github.com/CabbageDevelopment/qasync/releases/tag/v0.25.0) tag/release.

Expand Down
100 changes: 42 additions & 58 deletions examples/aiohttp_fetch.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,82 @@
import asyncio
import functools
import sys

import aiohttp

# from PyQt5.QtWidgets import (
from PySide2.QtWidgets import (
QWidget,
# from PyQt6.QtWidgets import (
from PySide6.QtWidgets import (
QApplication,
QLabel,
QLineEdit,
QTextEdit,
QPushButton,
QTextEdit,
QVBoxLayout,
QWidget,
)

import qasync
from qasync import asyncSlot, asyncClose, QApplication
from qasync import QEventLoop, asyncClose, asyncSlot


class MainWindow(QWidget):
"""Main window."""

_DEF_URL = "https://jsonplaceholder.typicode.com/todos/1"
"""str: Default URL."""

_SESSION_TIMEOUT = 1.0
"""float: Session timeout."""
_DEF_URL: str = "https://jsonplaceholder.typicode.com/todos/1"
"""Default URL."""

def __init__(self):
super().__init__()

self.setLayout(QVBoxLayout())

self.lblStatus = QLabel("Idle", self)
self.layout().addWidget(self.lblStatus)
self.lbl_status = QLabel("Idle", self)
self.layout().addWidget(self.lbl_status)

self.editUrl = QLineEdit(self._DEF_URL, self)
self.layout().addWidget(self.editUrl)
self.edit_url = QLineEdit(self._DEF_URL, self)
self.layout().addWidget(self.edit_url)

self.editResponse = QTextEdit("", self)
self.layout().addWidget(self.editResponse)
self.edit_response = QTextEdit("", self)
self.layout().addWidget(self.edit_response)

self.btnFetch = QPushButton("Fetch", self)
self.btnFetch.clicked.connect(self.on_btnFetch_clicked)
self.layout().addWidget(self.btnFetch)
self.btn_fetch = QPushButton("Fetch", self)
self.btn_fetch.clicked.connect(self.on_btn_fetch_clicked)
self.layout().addWidget(self.btn_fetch)

self.session = aiohttp.ClientSession(
loop=asyncio.get_event_loop(),
timeout=aiohttp.ClientTimeout(total=self._SESSION_TIMEOUT),
)
self.session: aiohttp.ClientSession

@asyncClose
async def closeEvent(self, event):
async def closeEvent(self, event): # noqa:N802
await self.session.close()

async def boot(self):
self.session = aiohttp.ClientSession()

@asyncSlot()
async def on_btnFetch_clicked(self):
self.btnFetch.setEnabled(False)
self.lblStatus.setText("Fetching...")
async def on_btn_fetch_clicked(self):
self.btn_fetch.setEnabled(False)
self.lbl_status.setText("Fetching...")

try:
async with self.session.get(self.editUrl.text()) as r:
self.editResponse.setText(await r.text())
async with self.session.get(self.edit_url.text()) as r:
self.edit_response.setText(await r.text())
except Exception as exc:
self.lblStatus.setText("Error: {}".format(exc))
self.lbl_status.setText("Error: {}".format(exc))
else:
self.lblStatus.setText("Finished!")
self.lbl_status.setText("Finished!")
finally:
self.btnFetch.setEnabled(True)


async def main():
def close_future(future, loop):
loop.call_later(10, future.cancel)
future.cancel()
self.btn_fetch.setEnabled(True)

loop = asyncio.get_event_loop()
future = asyncio.Future()

app = QApplication.instance()
if hasattr(app, "aboutToQuit"):
getattr(app, "aboutToQuit").connect(
functools.partial(close_future, future, loop)
)

mainWindow = MainWindow()
mainWindow.show()
if __name__ == "__main__":
app = QApplication(sys.argv)

await future
return True
event_loop = QEventLoop(app)
asyncio.set_event_loop(event_loop)

app_close_event = asyncio.Event()
app.aboutToQuit.connect(app_close_event.set)

main_window = MainWindow()
main_window.show()

if __name__ == "__main__":
try:
qasync.run(main())
except asyncio.exceptions.CancelledError:
sys.exit(0)
event_loop.create_task(main_window.boot())
event_loop.run_until_complete(app_close_event.wait())
event_loop.close()
16 changes: 11 additions & 5 deletions examples/executor_example.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import functools
import sys
import asyncio
import time
import qasync
import sys

# from PyQt5.QtWidgets import (
from PySide2.QtWidgets import QApplication, QProgressBar
# from PyQt6.QtWidgets import
from PySide6.QtWidgets import QApplication, QProgressBar
from qasync import QEventLoop, QThreadExecutor


Expand All @@ -32,4 +31,11 @@ def last_50(progress, loop):
time.sleep(0.1)


qasync.run(master())
if __name__ == "__main__":
app = QApplication(sys.argv)

event_loop = QEventLoop(app)
asyncio.set_event_loop(event_loop)

event_loop.run_until_complete(master())
event_loop.close()
41 changes: 41 additions & 0 deletions examples/qml_httpx/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import sys
import asyncio
from pathlib import Path

from qasync import QEventLoop, QApplication
from PySide6.QtCore import QUrl
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType

from service import ExampleService

QML_PATH = Path(__file__).parent.absolute().joinpath("qml")


if __name__ == "__main__":
app = QApplication(sys.argv)

engine = QQmlApplicationEngine()
engine.addImportPath(QML_PATH)

app.aboutToQuit.connect(engine.deleteLater)
engine.quit.connect(app.quit)

# register our service, making it usable directly in QML
qmlRegisterType(ExampleService, "qasync", 1, 0, ExampleService.__name__)

# alternatively, instantiate the service and inject it into the QML engine
# service = ExampleService()
# engine.rootContext().setContextProperty("service", service)

event_loop = QEventLoop(app)
asyncio.set_event_loop(event_loop)

app_close_event = asyncio.Event()
app.aboutToQuit.connect(app_close_event.set)
engine.quit.connect(app_close_event.set)

qml_entry = QUrl.fromLocalFile(str(QML_PATH.joinpath("Main.qml")))
engine.load(qml_entry)

with event_loop:
event_loop.run_until_complete(app_close_event.wait())
18 changes: 18 additions & 0 deletions examples/qml_httpx/qml/Main.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15

ApplicationWindow {
id: root
title: "qasync"
visible: true
width: 420
height: 240

Loader {
id: mainLoader
anchors.fill: parent
source: "Page.qml"
}
}
65 changes: 65 additions & 0 deletions examples/qml_httpx/qml/Page.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.15
import QtQuick.Layouts 1.15

Item {
ExampleService {
id: service

// handle value changes inside the service object
onValueChanged: {
// use value
}
}

Connections {
target: service

// handle value changes with an external Connection
function onValueChanged(value) {
// use value
}
}

ColumnLayout {
anchors {
fill: parent
margins: 10
}

RowLayout {
Layout.fillWidth: true

Button {
id: button
Layout.preferredWidth: 100
enabled: !service.isLoading

text: {
return service.isLoading ? qsTr("Loading...") : qsTr("Fetch")
}
onClicked: function() {
service.fetch(url.text)
}
}

TextField {
id: url
Layout.fillWidth: true
enabled: !service.isLoading
text: qsTr("https://jsonplaceholder.typicode.com/todos/1")
}
}

TextEdit {
id: text
Layout.fillHeight: true
Layout.fillWidth: true

// react to value changes from other widgets
text: service.value
}
}

}