Skip to content

Commit e65c342

Browse files
temeddixhosaka
andauthored
Improve examples and README (#102)
* 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]>
1 parent c91a915 commit e65c342

File tree

8 files changed

+281
-71
lines changed

8 files changed

+281
-71
lines changed

README.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,68 @@
99

1010
## Introduction
1111

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

14-
`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.
14+
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`.
1515

16-
#### The future of `qasync`
16+
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`.
1717

18-
`qasync` was created because `asyncqt` and `quamash` are no longer maintained.
18+
### Basic Example
19+
20+
```python
21+
import sys
22+
import asyncio
23+
24+
from qasync import QEventLoop, QApplication
25+
from PySide6.QtWidgets import QWidget, QVBoxLayout
26+
27+
class MainWindow(QWidget):
28+
def __init__(self):
29+
super().__init__()
30+
31+
self.setLayout(QVBoxLayout())
32+
self.lbl_status = QLabel("Idle", self)
33+
self.layout().addWidget(self.lbl_status)
34+
35+
@asyncClose
36+
async def closeEvent(self, event):
37+
pass
38+
39+
@asyncSlot()
40+
async def onMyEvent(self):
41+
pass
42+
43+
44+
if __name__ == "__main__":
45+
app = QApplication(sys.argv)
46+
47+
event_loop = QEventLoop(app)
48+
asyncio.set_event_loop(event_loop)
49+
50+
app_close_event = asyncio.Event()
51+
app.aboutToQuit.connect(app_close_event.set)
52+
53+
main_window = MainWindow()
54+
main_window.show()
55+
56+
with event_loop:
57+
event_loop.run_until_complete(app_close_event.wait())
58+
```
59+
60+
More detailed examples can be found [here](https://github.com/CabbageDevelopment/qasync/tree/master/examples).
61+
62+
### The Future of `qasync`
63+
64+
`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.
1965

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

2268
## Requirements
2369

24-
`qasync` requires Python >= 3.8, and PyQt5/PyQt6 or PySide2/PySide6. The library is tested on Ubuntu, Windows and MacOS.
70+
- Python >= 3.8
71+
- PyQt5/PyQt6 or PySide2/PySide6
72+
73+
`qasync` is tested on Ubuntu, Windows and MacOS.
2574

2675
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.
2776

examples/aiohttp_fetch.py

Lines changed: 42 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,82 @@
11
import asyncio
2-
import functools
32
import sys
43

54
import aiohttp
65

7-
# from PyQt5.QtWidgets import (
8-
from PySide2.QtWidgets import (
9-
QWidget,
6+
# from PyQt6.QtWidgets import (
7+
from PySide6.QtWidgets import (
8+
QApplication,
109
QLabel,
1110
QLineEdit,
12-
QTextEdit,
1311
QPushButton,
12+
QTextEdit,
1413
QVBoxLayout,
14+
QWidget,
1515
)
16-
17-
import qasync
18-
from qasync import asyncSlot, asyncClose, QApplication
16+
from qasync import QEventLoop, asyncClose, asyncSlot
1917

2018

2119
class MainWindow(QWidget):
2220
"""Main window."""
2321

24-
_DEF_URL = "https://jsonplaceholder.typicode.com/todos/1"
25-
"""str: Default URL."""
26-
27-
_SESSION_TIMEOUT = 1.0
28-
"""float: Session timeout."""
22+
_DEF_URL: str = "https://jsonplaceholder.typicode.com/todos/1"
23+
"""Default URL."""
2924

3025
def __init__(self):
3126
super().__init__()
3227

3328
self.setLayout(QVBoxLayout())
3429

35-
self.lblStatus = QLabel("Idle", self)
36-
self.layout().addWidget(self.lblStatus)
30+
self.lbl_status = QLabel("Idle", self)
31+
self.layout().addWidget(self.lbl_status)
3732

38-
self.editUrl = QLineEdit(self._DEF_URL, self)
39-
self.layout().addWidget(self.editUrl)
33+
self.edit_url = QLineEdit(self._DEF_URL, self)
34+
self.layout().addWidget(self.edit_url)
4035

41-
self.editResponse = QTextEdit("", self)
42-
self.layout().addWidget(self.editResponse)
36+
self.edit_response = QTextEdit("", self)
37+
self.layout().addWidget(self.edit_response)
4338

44-
self.btnFetch = QPushButton("Fetch", self)
45-
self.btnFetch.clicked.connect(self.on_btnFetch_clicked)
46-
self.layout().addWidget(self.btnFetch)
39+
self.btn_fetch = QPushButton("Fetch", self)
40+
self.btn_fetch.clicked.connect(self.on_btn_fetch_clicked)
41+
self.layout().addWidget(self.btn_fetch)
4742

48-
self.session = aiohttp.ClientSession(
49-
loop=asyncio.get_event_loop(),
50-
timeout=aiohttp.ClientTimeout(total=self._SESSION_TIMEOUT),
51-
)
43+
self.session: aiohttp.ClientSession
5244

5345
@asyncClose
54-
async def closeEvent(self, event):
46+
async def closeEvent(self, event): # noqa:N802
5547
await self.session.close()
5648

49+
async def boot(self):
50+
self.session = aiohttp.ClientSession()
51+
5752
@asyncSlot()
58-
async def on_btnFetch_clicked(self):
59-
self.btnFetch.setEnabled(False)
60-
self.lblStatus.setText("Fetching...")
53+
async def on_btn_fetch_clicked(self):
54+
self.btn_fetch.setEnabled(False)
55+
self.lbl_status.setText("Fetching...")
6156

6257
try:
63-
async with self.session.get(self.editUrl.text()) as r:
64-
self.editResponse.setText(await r.text())
58+
async with self.session.get(self.edit_url.text()) as r:
59+
self.edit_response.setText(await r.text())
6560
except Exception as exc:
66-
self.lblStatus.setText("Error: {}".format(exc))
61+
self.lbl_status.setText("Error: {}".format(exc))
6762
else:
68-
self.lblStatus.setText("Finished!")
63+
self.lbl_status.setText("Finished!")
6964
finally:
70-
self.btnFetch.setEnabled(True)
71-
72-
73-
async def main():
74-
def close_future(future, loop):
75-
loop.call_later(10, future.cancel)
76-
future.cancel()
65+
self.btn_fetch.setEnabled(True)
7766

78-
loop = asyncio.get_event_loop()
79-
future = asyncio.Future()
8067

81-
app = QApplication.instance()
82-
if hasattr(app, "aboutToQuit"):
83-
getattr(app, "aboutToQuit").connect(
84-
functools.partial(close_future, future, loop)
85-
)
86-
87-
mainWindow = MainWindow()
88-
mainWindow.show()
68+
if __name__ == "__main__":
69+
app = QApplication(sys.argv)
8970

90-
await future
91-
return True
71+
event_loop = QEventLoop(app)
72+
asyncio.set_event_loop(event_loop)
9273

74+
app_close_event = asyncio.Event()
75+
app.aboutToQuit.connect(app_close_event.set)
76+
77+
main_window = MainWindow()
78+
main_window.show()
9379

94-
if __name__ == "__main__":
95-
try:
96-
qasync.run(main())
97-
except asyncio.exceptions.CancelledError:
98-
sys.exit(0)
80+
event_loop.create_task(main_window.boot())
81+
event_loop.run_until_complete(app_close_event.wait())
82+
event_loop.close()

examples/executor_example.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import functools
2-
import sys
32
import asyncio
43
import time
5-
import qasync
4+
import sys
65

7-
# from PyQt5.QtWidgets import (
8-
from PySide2.QtWidgets import QApplication, QProgressBar
6+
# from PyQt6.QtWidgets import
7+
from PySide6.QtWidgets import QApplication, QProgressBar
98
from qasync import QEventLoop, QThreadExecutor
109

1110

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

3433

35-
qasync.run(master())
34+
if __name__ == "__main__":
35+
app = QApplication(sys.argv)
36+
37+
event_loop = QEventLoop(app)
38+
asyncio.set_event_loop(event_loop)
39+
40+
event_loop.run_until_complete(master())
41+
event_loop.close()

examples/qml_httpx/app.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import sys
2+
import asyncio
3+
from pathlib import Path
4+
5+
from qasync import QEventLoop, QApplication
6+
from PySide6.QtCore import QUrl
7+
from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType
8+
9+
from service import ExampleService
10+
11+
QML_PATH = Path(__file__).parent.absolute().joinpath("qml")
12+
13+
14+
if __name__ == "__main__":
15+
app = QApplication(sys.argv)
16+
17+
engine = QQmlApplicationEngine()
18+
engine.addImportPath(QML_PATH)
19+
20+
app.aboutToQuit.connect(engine.deleteLater)
21+
engine.quit.connect(app.quit)
22+
23+
# register our service, making it usable directly in QML
24+
qmlRegisterType(ExampleService, "qasync", 1, 0, ExampleService.__name__)
25+
26+
# alternatively, instantiate the service and inject it into the QML engine
27+
# service = ExampleService()
28+
# engine.rootContext().setContextProperty("service", service)
29+
30+
event_loop = QEventLoop(app)
31+
asyncio.set_event_loop(event_loop)
32+
33+
app_close_event = asyncio.Event()
34+
app.aboutToQuit.connect(app_close_event.set)
35+
engine.quit.connect(app_close_event.set)
36+
37+
qml_entry = QUrl.fromLocalFile(str(QML_PATH.joinpath("Main.qml")))
38+
engine.load(qml_entry)
39+
40+
with event_loop:
41+
event_loop.run_until_complete(app_close_event.wait())

examples/qml_httpx/qml/Main.qml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import QtQuick 2.15
2+
import QtQuick.Controls 2.15
3+
import QtQuick.Layouts 1.15
4+
import QtQuick.Window 2.15
5+
6+
ApplicationWindow {
7+
id: root
8+
title: "qasync"
9+
visible: true
10+
width: 420
11+
height: 240
12+
13+
Loader {
14+
id: mainLoader
15+
anchors.fill: parent
16+
source: "Page.qml"
17+
}
18+
}

examples/qml_httpx/qml/Page.qml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import QtQuick 2.15
2+
import QtQuick.Controls 2.15
3+
import QtQuick.Controls.Material 2.15
4+
import QtQuick.Layouts 1.15
5+
6+
Item {
7+
ExampleService {
8+
id: service
9+
10+
// handle value changes inside the service object
11+
onValueChanged: {
12+
// use value
13+
}
14+
}
15+
16+
Connections {
17+
target: service
18+
19+
// handle value changes with an external Connection
20+
function onValueChanged(value) {
21+
// use value
22+
}
23+
}
24+
25+
ColumnLayout {
26+
anchors {
27+
fill: parent
28+
margins: 10
29+
}
30+
31+
RowLayout {
32+
Layout.fillWidth: true
33+
34+
Button {
35+
id: button
36+
Layout.preferredWidth: 100
37+
enabled: !service.isLoading
38+
39+
text: {
40+
return service.isLoading ? qsTr("Loading...") : qsTr("Fetch")
41+
}
42+
onClicked: function() {
43+
service.fetch(url.text)
44+
}
45+
}
46+
47+
TextField {
48+
id: url
49+
Layout.fillWidth: true
50+
enabled: !service.isLoading
51+
text: qsTr("https://jsonplaceholder.typicode.com/todos/1")
52+
}
53+
}
54+
55+
TextEdit {
56+
id: text
57+
Layout.fillHeight: true
58+
Layout.fillWidth: true
59+
60+
// react to value changes from other widgets
61+
text: service.value
62+
}
63+
}
64+
65+
}

0 commit comments

Comments
 (0)