Skip to content

Commit

Permalink
Improve examples and README (#102)
Browse files Browse the repository at this point in the history
* Update and organize examples

* Remove an unused class variable

* Use Python's type hint

* Make executor example actually work

* Make the linter happy with tests

* Improve code readability

* Fix a wrong comment

* Simplify code

* Simplify code

* Fix variable names

* Fix the warning from session creation

* Simplify code

* Organize code

* Restore some variables for compatibility

* Improve README

* Improve guides

* Fix a title

* Improve details

* Make the basic example copy paste complete, tidy up the intro

* Add an example using QML

* Revert accidentally removed hunk

---------

Co-authored-by: Alex March <[email protected]>
  • Loading branch information
temeddix and hosaka authored Nov 17, 2023
1 parent c91a915 commit e65c342
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 71 deletions.
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
}
}

}
Loading

0 comments on commit e65c342

Please sign in to comment.