From 330724748ce90a1b22165269e260d9ad1d304922 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 02:24:21 +0900 Subject: [PATCH 01/21] Update and organize examples --- examples/aiohttp_fetch.py | 57 ++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 34 deletions(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index 2dc29f2..b036805 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -1,21 +1,19 @@ 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): @@ -45,17 +43,14 @@ def __init__(self): self.btnFetch.clicked.connect(self.on_btnFetch_clicked) self.layout().addWidget(self.btnFetch) - 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() @asyncSlot() - async def on_btnFetch_clicked(self): + async def on_btnFetch_clicked(self): # noqa:N802 self.btnFetch.setEnabled(False) self.lblStatus.setText("Fetching...") @@ -70,29 +65,23 @@ async def on_btnFetch_clicked(self): self.btnFetch.setEnabled(True) -async def main(): - def close_future(future, loop): - loop.call_later(10, future.cancel) - future.cancel() +if __name__ == "__main__": + app = QApplication(sys.argv) - loop = asyncio.get_event_loop() - future = asyncio.Future() + event_loop = QEventLoop(app) + asyncio.set_event_loop(event_loop) + app_close_event = asyncio.Event() - app = QApplication.instance() - if hasattr(app, "aboutToQuit"): - getattr(app, "aboutToQuit").connect( - functools.partial(close_future, future, loop) - ) + main_window = MainWindow() + main_window.show() - mainWindow = MainWindow() - mainWindow.show() + def close_app(): + app_close_event.set() - await future - return True + async def keep_app_lifecycle(): + await app_close_event.wait() + app.aboutToQuit.connect(close_app) -if __name__ == "__main__": - try: - qasync.run(main()) - except asyncio.exceptions.CancelledError: - sys.exit(0) + event_loop.run_until_complete(keep_app_lifecycle()) + event_loop.close() From bc9cc64f56b0bbbbea36d63a824e8b5363884f8d Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 03:00:27 +0900 Subject: [PATCH 02/21] Remove an unused class variable --- examples/aiohttp_fetch.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index b036805..a2f93df 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -22,9 +22,6 @@ class MainWindow(QWidget): _DEF_URL = "https://jsonplaceholder.typicode.com/todos/1" """str: Default URL.""" - _SESSION_TIMEOUT = 1.0 - """float: Session timeout.""" - def __init__(self): super().__init__() From d721a534da58a958789e07691422742a7b3b7d25 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 03:52:25 +0900 Subject: [PATCH 03/21] Use Python's type hint --- examples/aiohttp_fetch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index a2f93df..5aeb397 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -19,8 +19,8 @@ class MainWindow(QWidget): """Main window.""" - _DEF_URL = "https://jsonplaceholder.typicode.com/todos/1" - """str: Default URL.""" + _DEF_URL: str = "https://jsonplaceholder.typicode.com/todos/1" + """Default URL.""" def __init__(self): super().__init__() From 8cbecd6467a5fdb3ffc924b247136e191e8ae5cc Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 04:16:10 +0900 Subject: [PATCH 04/21] Make executor example actually work --- examples/executor_example.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/examples/executor_example.py b/examples/executor_example.py index 9e242a2..5536464 100644 --- a/examples/executor_example.py +++ b/examples/executor_example.py @@ -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 @@ -32,4 +31,9 @@ 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() From 70abac94a8c2eb5e5dfd7cb01eb5acb318049e4b Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 04:16:29 +0900 Subject: [PATCH 05/21] Make the linter happy with tests --- tests/test_qeventloop.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index c1de714..6d9a436 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -151,6 +151,8 @@ async def mycoro(): 'print("Hello async world!")', stdout=subprocess.PIPE, ) + if process.stdout is None: + raise Exception("Output from the process is none") received_stdout = await process.stdout.readexactly(len(b"Hello async world!\n")) await process.wait() assert process.returncode == 0 @@ -589,8 +591,8 @@ def cb3(): loop._add_reader(c_sock.fileno(), cb1) - clent_task = asyncio.ensure_future(client_coro()) - server_task = asyncio.ensure_future(server_coro()) + _clent_task = asyncio.ensure_future(client_coro()) + _server_task = asyncio.ensure_future(server_coro()) both_done = asyncio.gather(client_done, server_done) loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0)) @@ -797,4 +799,5 @@ def teardown_module(module): for logger in loggers: handlers = getattr(logger, "handlers", []) for handler in handlers: - logger.removeHandler(handler) + if not isinstance(logger, logging.PlaceHolder): + logger.removeHandler(handler) From a6048c2906a478397d5b588ec18ade383e49a550 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 04:17:21 +0900 Subject: [PATCH 06/21] Improve code readability --- examples/executor_example.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/executor_example.py b/examples/executor_example.py index 5536464..ab0306e 100644 --- a/examples/executor_example.py +++ b/examples/executor_example.py @@ -33,7 +33,9 @@ def last_50(progress, loop): 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() From e1a50653cd22b4aa3321c49b8627acbc1edec132 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 04:28:22 +0900 Subject: [PATCH 07/21] Fix a wrong comment --- examples/executor_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/executor_example.py b/examples/executor_example.py index ab0306e..f208912 100644 --- a/examples/executor_example.py +++ b/examples/executor_example.py @@ -3,7 +3,7 @@ import time import sys -# from PyQt6.QtWidgets import ( +# from PyQt6.QtWidgets import from PySide6.QtWidgets import QApplication, QProgressBar from qasync import QEventLoop, QThreadExecutor From 7d7d0c96a88913f0f161480c2077ad1028e54082 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 04:37:44 +0900 Subject: [PATCH 08/21] Simplify code --- tests/test_qeventloop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index 6d9a436..69fa72a 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -591,8 +591,8 @@ def cb3(): loop._add_reader(c_sock.fileno(), cb1) - _clent_task = asyncio.ensure_future(client_coro()) - _server_task = asyncio.ensure_future(server_coro()) + asyncio.ensure_future(client_coro()) + asyncio.ensure_future(server_coro()) both_done = asyncio.gather(client_done, server_done) loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0)) From 80e4ef1dd8feabfaf16e7df94952ed80d4847f65 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 04:40:40 +0900 Subject: [PATCH 09/21] Simplify code --- tests/test_qeventloop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index 69fa72a..17db208 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -799,5 +799,5 @@ def teardown_module(module): for logger in loggers: handlers = getattr(logger, "handlers", []) for handler in handlers: - if not isinstance(logger, logging.PlaceHolder): + if isinstance(logger, logging.Logger): logger.removeHandler(handler) From c1dccec71b43b7d050ca5a254cb9cd86a42d3cf3 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 13:12:39 +0900 Subject: [PATCH 10/21] Fix variable names --- examples/aiohttp_fetch.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index 5aeb397..fc6e305 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -27,18 +27,18 @@ def __init__(self): 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() @@ -47,19 +47,19 @@ async def closeEvent(self, event): # noqa:N802 await self.session.close() @asyncSlot() - async def on_btnFetch_clicked(self): # noqa:N802 - 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) + self.btn_fetch.setEnabled(True) if __name__ == "__main__": From c32de2a593eb5b754bfb385e33293350969c516c Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 13:29:18 +0900 Subject: [PATCH 11/21] Fix the warning from session creation --- examples/aiohttp_fetch.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index fc6e305..7ae5cb2 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -40,12 +40,15 @@ def __init__(self): self.btn_fetch.clicked.connect(self.on_btn_fetch_clicked) self.layout().addWidget(self.btn_fetch) - self.session = aiohttp.ClientSession() + self.session: aiohttp.ClientSession @asyncClose async def closeEvent(self, event): # noqa:N802 await self.session.close() + async def boot(self): + self.session = aiohttp.ClientSession() + @asyncSlot() async def on_btn_fetch_clicked(self): self.btn_fetch.setEnabled(False) @@ -80,5 +83,6 @@ async def keep_app_lifecycle(): app.aboutToQuit.connect(close_app) + event_loop.create_task(main_window.boot()) event_loop.run_until_complete(keep_app_lifecycle()) event_loop.close() From 25fe176bb2d9136bb3a064c33baf66594dc0a573 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 16:19:05 +0900 Subject: [PATCH 12/21] Simplify code --- examples/aiohttp_fetch.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index 7ae5cb2..2e988e0 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -75,13 +75,10 @@ async def on_btn_fetch_clicked(self): main_window = MainWindow() main_window.show() - def close_app(): - app_close_event.set() - async def keep_app_lifecycle(): await app_close_event.wait() - app.aboutToQuit.connect(close_app) + app.aboutToQuit.connect(app_close_event.set) event_loop.create_task(main_window.boot()) event_loop.run_until_complete(keep_app_lifecycle()) From 8e89a91bc9a1e5223e161c2c9f527c26e9d8b05d Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 16:57:02 +0900 Subject: [PATCH 13/21] Organize code --- examples/aiohttp_fetch.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index 2e988e0..6011702 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -72,14 +72,10 @@ async def on_btn_fetch_clicked(self): asyncio.set_event_loop(event_loop) app_close_event = asyncio.Event() + app.aboutToQuit.connect(app_close_event.set) main_window = MainWindow() main_window.show() - async def keep_app_lifecycle(): - await app_close_event.wait() - - app.aboutToQuit.connect(app_close_event.set) - event_loop.create_task(main_window.boot()) - event_loop.run_until_complete(keep_app_lifecycle()) + event_loop.run_until_complete(asyncio.wait_for(app_close_event.wait(), None)) event_loop.close() From 26c9a6150f547a971decfd067b1fc098050d632e Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Wed, 15 Nov 2023 17:44:25 +0900 Subject: [PATCH 14/21] Restore some variables for compatibility --- tests/test_qeventloop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_qeventloop.py b/tests/test_qeventloop.py index 17db208..68ba88b 100644 --- a/tests/test_qeventloop.py +++ b/tests/test_qeventloop.py @@ -591,8 +591,8 @@ def cb3(): loop._add_reader(c_sock.fileno(), cb1) - asyncio.ensure_future(client_coro()) - asyncio.ensure_future(server_coro()) + _client_task = asyncio.ensure_future(client_coro()) + _server_task = asyncio.ensure_future(server_coro()) both_done = asyncio.gather(client_done, server_done) loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0)) From 6b6a76ce650aa087087e65d779092b7f6603c886 Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Thu, 16 Nov 2023 00:11:22 +0900 Subject: [PATCH 15/21] Improve README --- README.md | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 880b411..cc578e7 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,44 @@ `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`, `QThread`, etc. Because of Python's GIL, making your app single-threaded does not inherently make it slower, but only safer. -#### 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 basically identical to that of `asyncio`. -`qasync` was created because `asyncqt` and `quamash` are no longer maintained. +### Basic Example + +```python +class MainWindow(QWidget): + def __init__(self): + super().__init__() + + self.setLayout(QVBoxLayout()) + + self.lbl_status = QLabel("Idle", self) + self.layout().addWidget(self.lbl_status) + + +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() + + event_loop.create_task(main_window.boot()) + event_loop.run_until_complete(asyncio.wait_for(app_close_event.wait(), None)) + event_loop.close() +``` + +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.** From 680b61de1e3fa975f34b407b20cd63939fd40c1b Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Thu, 16 Nov 2023 01:57:23 +0900 Subject: [PATCH 16/21] Improve guides --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cc578e7..9724344 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ `qasync` allows coroutines to be used in PyQt/PySide applications by providing an implementation of the `PEP 3156` event-loop. -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`, `QThread`, etc. Because of Python's GIL, making your app single-threaded does not inherently make it slower, but only safer. +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`. Single-threaded concurrency does not inherently make the app slower, but only safer, because of Python's GIL. The concept of single-threaded app is also adopted by Flutter and JavaScript due to its benefits. If you need some CPU-intensive tasks to be executed in parallel, `qasync` also got that covered, providing `QEventLoop.run_in_executor` which is basically identical to that of `asyncio`. From ef80414ce433cc41879b78550c543c7df60ea04e Mon Sep 17 00:00:00 2001 From: Donghyun Kim Date: Thu, 16 Nov 2023 01:58:59 +0900 Subject: [PATCH 17/21] Fix a title --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9724344..6bbc6f1 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ if __name__ == "__main__": More detailed examples can be found [here](https://github.com/CabbageDevelopment/qasync/tree/master/examples). -### The future of `qasync` +### 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. From bd0c1a316fcdc77ef207fb630c1c175cd27e291b Mon Sep 17 00:00:00 2001 From: DongHyun Kim Date: Fri, 17 Nov 2023 12:48:47 +0900 Subject: [PATCH 18/21] Improve details --- README.md | 25 +++++++++++++++++-------- examples/aiohttp_fetch.py | 5 +++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 6bbc6f1..be27d78 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,27 @@ `qasync` allows coroutines to be used in PyQt/PySide applications by providing an implementation of the `PEP 3156` event-loop. -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`. Single-threaded concurrency does not inherently make the app slower, but only safer, because of Python's GIL. The concept of single-threaded app is also adopted by Flutter and JavaScript due to its benefits. +With `qasync`, you can use `asyncio` functionalities directly inside Qt app's event loop, in the main thread. The concept of single-threaded app is also adopted by Flutter and JavaScript due to its benefits. Using async functions for Python tasks can be much easier and cleaner than using `threading.Thread` or `QThread`. Single-threaded concurrency does not inherently make the app slower, but only safer, because of Python's GIL. If you need some CPU-intensive tasks to be executed in parallel, `qasync` also got that covered, providing `QEventLoop.run_in_executor` which is basically identical to that of `asyncio`. ### Basic Example ```python +from qasync import QEventLoop, asyncClose, asyncSlot + + class MainWindow(QWidget): def __init__(self): super().__init__() - self.setLayout(QVBoxLayout()) + @asyncClose + async def closeEvent(self, event): # noqa:N802 + pass - self.lbl_status = QLabel("Idle", self) - self.layout().addWidget(self.lbl_status) + @asyncSlot() + async def on_my_event(self): + pass if __name__ == "__main__": @@ -33,14 +39,14 @@ if __name__ == "__main__": event_loop = QEventLoop(app) asyncio.set_event_loop(event_loop) - app_close_event = asyncio.Event() + app_close_event = asyncio.Event() app.aboutToQuit.connect(app_close_event.set) + main_window = MainWindow() main_window.show() - event_loop.create_task(main_window.boot()) - event_loop.run_until_complete(asyncio.wait_for(app_close_event.wait(), None)) + event_loop.run_until_complete(app_close_event.wait()) event_loop.close() ``` @@ -54,7 +60,10 @@ More detailed examples can be found [here](https://github.com/CabbageDevelopment ## 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. diff --git a/examples/aiohttp_fetch.py b/examples/aiohttp_fetch.py index 6011702..1fdcc4a 100644 --- a/examples/aiohttp_fetch.py +++ b/examples/aiohttp_fetch.py @@ -70,12 +70,13 @@ async def on_btn_fetch_clicked(self): event_loop = QEventLoop(app) asyncio.set_event_loop(event_loop) - app_close_event = asyncio.Event() + app_close_event = asyncio.Event() app.aboutToQuit.connect(app_close_event.set) + main_window = MainWindow() main_window.show() event_loop.create_task(main_window.boot()) - event_loop.run_until_complete(asyncio.wait_for(app_close_event.wait(), None)) + event_loop.run_until_complete(app_close_event.wait()) event_loop.close() From 429df504b96726388b6800844333b1a8a030d114 Mon Sep 17 00:00:00 2001 From: Alex March Date: Fri, 17 Nov 2023 16:54:23 +0900 Subject: [PATCH 19/21] Make the basic example copy paste complete, tidy up the intro --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index be27d78..d8867ef 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,20 @@ ## 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. -With `qasync`, you can use `asyncio` functionalities directly inside Qt app's event loop, in the main thread. The concept of single-threaded app is also adopted by Flutter and JavaScript due to its benefits. Using async functions for Python tasks can be much easier and cleaner than using `threading.Thread` or `QThread`. Single-threaded concurrency does not inherently make the app slower, but only safer, because of Python's GIL. +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`. -If you need some CPU-intensive tasks to be executed in parallel, `qasync` also got that covered, providing `QEventLoop.run_in_executor` which is basically identical to that of `asyncio`. +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`. ### Basic Example ```python -from qasync import QEventLoop, asyncClose, asyncSlot +import sys +import asyncio +from qasync import QEventLoop, QApplication +from PySide6.QtWidgets import QWidget, QVBoxLayout class MainWindow(QWidget): def __init__(self): @@ -39,15 +42,15 @@ if __name__ == "__main__": 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() - event_loop.run_until_complete(app_close_event.wait()) - event_loop.close() + 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). From a4bef242a42b957660b4a5ca58e0fbfb06d94e7d Mon Sep 17 00:00:00 2001 From: Alex March Date: Fri, 17 Nov 2023 16:54:53 +0900 Subject: [PATCH 20/21] Add an example using QML --- README.md | 2 +- examples/qml_httpx/app.py | 41 +++++++++++++++++++++ examples/qml_httpx/qml/Main.qml | 18 +++++++++ examples/qml_httpx/qml/Page.qml | 65 +++++++++++++++++++++++++++++++++ examples/qml_httpx/service.py | 44 ++++++++++++++++++++++ 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 examples/qml_httpx/app.py create mode 100644 examples/qml_httpx/qml/Main.qml create mode 100644 examples/qml_httpx/qml/Page.qml create mode 100644 examples/qml_httpx/service.py diff --git a/README.md b/README.md index d8867ef..92e672f 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ if __name__ == "__main__": 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() diff --git a/examples/qml_httpx/app.py b/examples/qml_httpx/app.py new file mode 100644 index 0000000..f544cd9 --- /dev/null +++ b/examples/qml_httpx/app.py @@ -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()) diff --git a/examples/qml_httpx/qml/Main.qml b/examples/qml_httpx/qml/Main.qml new file mode 100644 index 0000000..85cf796 --- /dev/null +++ b/examples/qml_httpx/qml/Main.qml @@ -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" + } +} diff --git a/examples/qml_httpx/qml/Page.qml b/examples/qml_httpx/qml/Page.qml new file mode 100644 index 0000000..1267da8 --- /dev/null +++ b/examples/qml_httpx/qml/Page.qml @@ -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 + } + } + +} diff --git a/examples/qml_httpx/service.py b/examples/qml_httpx/service.py new file mode 100644 index 0000000..3c2604f --- /dev/null +++ b/examples/qml_httpx/service.py @@ -0,0 +1,44 @@ +import httpx + +from qasync import asyncSlot +from PySide6.QtCore import QObject, Signal, Property, Slot + + +class ExampleService(QObject): + valueChanged = Signal(str, arguments=["value"]) + loadingChanged = Signal(bool, arguments=["loading"]) + + def __init__(self, parent=None): + QObject.__init__(self, parent) + + self._value = None + self._loading = False + + def _set_value(self, value): + if self._value != value: + self._value = value + self.valueChanged.emit(value) + + def _set_loading(self, value): + if self._loading != value: + self._loading = value + self.loadingChanged.emit(value) + + @Property(str, notify=valueChanged) + def value(self) -> str: + return self._value + + @Property(bool, notify=loadingChanged) + def isLoading(self) -> bool: + return self._loading + + @asyncSlot(str) + async def fetch(self, endpoint: str): + if not endpoint: + return + + self._set_loading(True) + async with httpx.AsyncClient() as client: + resp = await client.get(endpoint) + self._set_value(resp.text) + self._set_loading(False) From cbbedb4f1399085a11ed8136328fb0a448867dd0 Mon Sep 17 00:00:00 2001 From: Alex March Date: Fri, 17 Nov 2023 17:09:06 +0900 Subject: [PATCH 21/21] Revert accidentally removed hunk --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92e672f..cd39b52 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,16 @@ 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): # noqa:N802 + async def closeEvent(self, event): pass @asyncSlot() - async def on_my_event(self): + async def onMyEvent(self): pass