From 4a68220ed2f431a25c8fb5897dfe1b9595fd3102 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Thu, 10 Jul 2025 19:10:57 -0500 Subject: [PATCH 01/16] Remove references to event loop policies. --- qasync/__init__.py | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index 3363e97..ef8535e 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -1,6 +1,13 @@ """ Implementation of the PEP 3156 Event-Loop with Qt. +This package is originally at https://github.com/CabbageDevelopment/qasync, +licensed under the BSD license; +It is modified since the original was unmaintained for two years. References +to Event Loop Policies was removed, since it's scheduled to be deprecated in +Python 3.16. + +Copyright (c) 2025 Xinyuan Zhou Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen @@ -13,6 +20,7 @@ "Gerard Marull-Paretas , " "Mark Harviston , " "Arve Knudsen ", + "Xinyuan Zhou ", ) __all__ = ["QEventLoop", "QThreadExecutor", "asyncSlot", "asyncClose"] @@ -812,29 +820,23 @@ def wrapper(*args, **kwargs): return outer_decorator - -class QEventLoopPolicyMixin: - def new_event_loop(self): - return QEventLoop(QApplication.instance() or QApplication(sys.argv)) - - -class DefaultQEventLoopPolicy( - QEventLoopPolicyMixin, - asyncio.DefaultEventLoopPolicy, -): - pass - - @contextlib.contextmanager -def _set_event_loop_policy(policy): - old_policy = asyncio.get_event_loop_policy() - asyncio.set_event_loop_policy(policy) +def _use_qeventloop(): + app = QApplication.instance() or QApplication([]) + loop = QEventLoop(app) + old_loop = asyncio.get_event_loop() + asyncio.set_event_loop(loop) try: - yield + yield loop finally: - asyncio.set_event_loop_policy(old_policy) + loop.close() + asyncio.set_event_loop(old_loop) + +def run(main, *args, **kwargs): + """ + Run the given coroutine using a QEventLoop without setting a global policy. + """ + with _use_qeventloop() as loop: + return loop.run_until_complete(main(*args, **kwargs)) -def run(*args, **kwargs): - with _set_event_loop_policy(DefaultQEventLoopPolicy()): - return asyncio.run(*args, **kwargs) From 529b9a7575a03e377dfd6566147fdb4fd8952631 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 13 Jul 2025 22:27:15 -0500 Subject: [PATCH 02/16] Apply suggestions from code review --- qasync/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index ef8535e..bae6c07 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -1,13 +1,6 @@ """ Implementation of the PEP 3156 Event-Loop with Qt. -This package is originally at https://github.com/CabbageDevelopment/qasync, -licensed under the BSD license; -It is modified since the original was unmaintained for two years. References -to Event Loop Policies was removed, since it's scheduled to be deprecated in -Python 3.16. - -Copyright (c) 2025 Xinyuan Zhou Copyright (c) 2018 Gerard Marull-Paretas Copyright (c) 2014 Mark Harviston Copyright (c) 2014 Arve Knudsen @@ -20,7 +13,6 @@ "Gerard Marull-Paretas , " "Mark Harviston , " "Arve Knudsen ", - "Xinyuan Zhou ", ) __all__ = ["QEventLoop", "QThreadExecutor", "asyncSlot", "asyncClose"] From 79911bce379ec2b123f9e5563ef1a238a1716b9a Mon Sep 17 00:00:00 2001 From: John Date: Mon, 14 Jul 2025 10:23:25 -0500 Subject: [PATCH 03/16] Update __init__.py --- qasync/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index bae6c07..a904b78 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -814,7 +814,7 @@ def wrapper(*args, **kwargs): @contextlib.contextmanager def _use_qeventloop(): - app = QApplication.instance() or QApplication([]) + app = QApplication.instance() or QApplication([sys.argv]) loop = QEventLoop(app) old_loop = asyncio.get_event_loop() asyncio.set_event_loop(loop) @@ -825,10 +825,10 @@ def _use_qeventloop(): asyncio.set_event_loop(old_loop) -def run(main, *args, **kwargs): +def run(future): """ - Run the given coroutine using a QEventLoop without setting a global policy. + Run the given coroutine using a QEventLoop. """ with _use_qeventloop() as loop: - return loop.run_until_complete(main(*args, **kwargs)) + return loop.run_until_complete(future) From 5a58db2c495697f7731e5a0b9db39b4e6ebb5b40 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Mon, 14 Jul 2025 13:50:49 -0500 Subject: [PATCH 04/16] fix remaining issues --- qasync/__init__.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index a904b78..4a270d9 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -813,9 +813,12 @@ def wrapper(*args, **kwargs): return outer_decorator @contextlib.contextmanager -def _use_qeventloop(): +def _use_qeventloop(loop_factory): app = QApplication.instance() or QApplication([sys.argv]) - loop = QEventLoop(app) + if loop_factory is None: + loop = QEventLoop(app) + else: + loop = loop_factory(app) old_loop = asyncio.get_event_loop() asyncio.set_event_loop(loop) try: @@ -824,11 +827,12 @@ def _use_qeventloop(): loop.close() asyncio.set_event_loop(old_loop) - -def run(future): +# A run function matching the signature of asyncio.run +def run(main_coro, *, debug=None, loop_factory=None): """ Run the given coroutine using a QEventLoop. """ - with _use_qeventloop() as loop: - return loop.run_until_complete(future) - + with _use_qeventloop(loop_factory) as loop: + if debug is not None: + loop.set_debug(debug) + return loop.run_until_complete(main_coro) From d3b4897a508850a10c0ed07c03f6c3e59d34fa3f Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jul 2025 21:15:13 -0500 Subject: [PATCH 05/16] CI Changes --- .github/workflows/main.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 57a6a11..5141de3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [Ubuntu, Windows, MacOS] - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] qt-version: ["pyside2", "pyside6", "pyqt5", "pyqt6"] include: - os: Ubuntu @@ -29,6 +29,9 @@ jobs: image: windows-2022 - os: MacOS image: macos-12 + exclude: + - os: MacOS + qt-version: pyside2 fail-fast: false defaults: run: From a1c9df200be192079b600cc2654903365d30f9e6 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jul 2025 21:18:25 -0500 Subject: [PATCH 06/16] Create test_run.py --- tests/test_run.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/test_run.py diff --git a/tests/test_run.py b/tests/test_run.py new file mode 100644 index 0000000..e10bdd7 --- /dev/null +++ b/tests/test_run.py @@ -0,0 +1,13 @@ +import qasync +import asyncio + +def test_run_with_contextmanager(): + async def coro(): + event_loop = asyncio.get_event_loop() + assert type(event_loop).__name__ == "QSelectorEventLoop" + await asyncio.sleep(0) + + qasync.run(coro()) + + event_loop = asyncio.get_event_loop() + assert type(event_loop).__name__ != "QSelectorEventLoop" From 4859dd64248a7340a7751e7471b1ace350524aa6 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jul 2025 21:20:42 -0500 Subject: [PATCH 07/16] Update test_run.py --- tests/test_run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_run.py b/tests/test_run.py index e10bdd7..f81287a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -7,6 +7,7 @@ async def coro(): assert type(event_loop).__name__ == "QSelectorEventLoop" await asyncio.sleep(0) + app = QApplication([]) qasync.run(coro()) event_loop = asyncio.get_event_loop() From 3ed12df89726da0aecb8c291f8fcb603505eaaa5 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jul 2025 21:22:32 -0500 Subject: [PATCH 08/16] Update test_run.py --- tests/test_run.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_run.py b/tests/test_run.py index f81287a..81da3d8 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -1,13 +1,12 @@ import qasync import asyncio -def test_run_with_contextmanager(): +def test_run_with_contextmanager(application): async def coro(): event_loop = asyncio.get_event_loop() assert type(event_loop).__name__ == "QSelectorEventLoop" await asyncio.sleep(0) - app = QApplication([]) qasync.run(coro()) event_loop = asyncio.get_event_loop() From a544396e7e1edb1305e72ddda170c0dc79543430 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Mon, 21 Jul 2025 21:24:40 -0500 Subject: [PATCH 09/16] test ru --- tests/test_run.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_run.py b/tests/test_run.py index 81da3d8..11b4f3a 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -7,6 +7,7 @@ async def coro(): assert type(event_loop).__name__ == "QSelectorEventLoop" await asyncio.sleep(0) + _ = application qasync.run(coro()) event_loop = asyncio.get_event_loop() From 149ad9dfd670d3c81322d8bcb41daf726648e068 Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jul 2025 21:28:52 -0500 Subject: [PATCH 10/16] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7f23007..99358c1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -29,9 +29,9 @@ jobs: image: windows-2022 - os: MacOS image: macos-14 - exclude: - os: MacOS qt-version: pyside2 + image: macos-13 fail-fast: false defaults: run: From f5b819b5e733a4d9c77f92927ab1043437034f9b Mon Sep 17 00:00:00 2001 From: John Zhou Date: Mon, 21 Jul 2025 22:23:36 -0500 Subject: [PATCH 11/16] fix failure --- qasync/__init__.py | 5 ++++- tests/test_run.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index 7f014c2..4e5bbea 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -824,7 +824,10 @@ def _use_qeventloop(loop_factory): loop = QEventLoop(app) else: loop = loop_factory(app) - old_loop = asyncio.get_event_loop() + try: + old_loop = asyncio.get_event_loop() + except RuntimeError: # No current event loop + old_loop = None asyncio.set_event_loop(loop) try: yield loop diff --git a/tests/test_run.py b/tests/test_run.py index 11b4f3a..22d5f64 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -1,5 +1,6 @@ import qasync import asyncio +from qasync import QApplication def test_run_with_contextmanager(application): async def coro(): @@ -7,8 +8,10 @@ async def coro(): assert type(event_loop).__name__ == "QSelectorEventLoop" await asyncio.sleep(0) - _ = application qasync.run(coro()) - event_loop = asyncio.get_event_loop() + try: + event_loop = asyncio.get_event_loop() + except: + event_loop = None assert type(event_loop).__name__ != "QSelectorEventLoop" From 785bcc00079f0c5dff6e9713e980ac80f0605bff Mon Sep 17 00:00:00 2001 From: John Zhou Date: Mon, 21 Jul 2025 22:25:52 -0500 Subject: [PATCH 12/16] safer --- qasync/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index 4e5bbea..7ae4d48 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -833,7 +833,8 @@ def _use_qeventloop(loop_factory): yield loop finally: loop.close() - asyncio.set_event_loop(old_loop) + if old_loop is not None: + asyncio.set_event_loop(old_loop) # A run function matching the signature of asyncio.run def run(main_coro, *, debug=None, loop_factory=None): From e8ba2cd111155fdb38f520ce501d30e80527a848 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Mon, 21 Jul 2025 22:52:20 -0500 Subject: [PATCH 13/16] fixup --- qasync/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qasync/__init__.py b/qasync/__init__.py index 7ae4d48..4e5bbea 100644 --- a/qasync/__init__.py +++ b/qasync/__init__.py @@ -833,8 +833,7 @@ def _use_qeventloop(loop_factory): yield loop finally: loop.close() - if old_loop is not None: - asyncio.set_event_loop(old_loop) + asyncio.set_event_loop(old_loop) # A run function matching the signature of asyncio.run def run(main_coro, *, debug=None, loop_factory=None): From 03f843fb1abf2dedcf94d1b5554e6d70b05d823e Mon Sep 17 00:00:00 2001 From: John Date: Mon, 21 Jul 2025 23:15:25 -0500 Subject: [PATCH 14/16] Update main.yml --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 99358c1..4e714bb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [Ubuntu, Windows, MacOS] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14-dev"] qt-version: ["pyside2", "pyside6", "pyqt5", "pyqt6"] include: - os: Ubuntu From 994d4117ae65541e1f31e88a45093bf248b07f64 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Tue, 22 Jul 2025 09:50:12 -0500 Subject: [PATCH 15/16] fixup --- tests/test_run.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_run.py b/tests/test_run.py index 22d5f64..dd05170 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -1,11 +1,12 @@ import qasync import asyncio +import os from qasync import QApplication def test_run_with_contextmanager(application): async def coro(): event_loop = asyncio.get_event_loop() - assert type(event_loop).__name__ == "QSelectorEventLoop" + assert type(event_loop).__name__ == "QIOCPEventLoop" if os.name == 'nt' else "QSelectorEventLoop" await asyncio.sleep(0) qasync.run(coro()) From 3c4c00186cc01acda2115e215a3cb524eea4a9f8 Mon Sep 17 00:00:00 2001 From: John Zhou Date: Tue, 22 Jul 2025 10:08:56 -0500 Subject: [PATCH 16/16] fixup --- tests/test_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_run.py b/tests/test_run.py index dd05170..b7ed047 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -15,4 +15,4 @@ async def coro(): event_loop = asyncio.get_event_loop() except: event_loop = None - assert type(event_loop).__name__ != "QSelectorEventLoop" + assert type(event_loop).__name__ != "QIOCPEventLoop" if os.name == 'nt' else "QSelectorEventLoop"