Skip to content

Commit 16f3f7e

Browse files
johnzhou721hosaka
andauthored
Use asyncio loop_factory instead of policies for python>=3.12 (#140)
* Move run related tests to test_run.py * Skip policy related test * Increase some timeouts --------- Co-authored-by: Alex March <alexmarch@fastmail.com>
1 parent 3b78c45 commit 16f3f7e

3 files changed

Lines changed: 99 additions & 123 deletions

File tree

qasync/__init__.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -849,28 +849,33 @@ def wrapper(*args, **kwargs):
849849
return outer_decorator
850850

851851

852-
class QEventLoopPolicyMixin:
853-
def new_event_loop(self):
854-
return QEventLoop(QApplication.instance() or QApplication(sys.argv))
852+
def _get_qevent_loop():
853+
return QEventLoop(QApplication.instance() or QApplication(sys.argv))
855854

856855

857-
class DefaultQEventLoopPolicy(
858-
QEventLoopPolicyMixin,
859-
asyncio.DefaultEventLoopPolicy,
860-
):
861-
pass
862-
863-
864-
@contextlib.contextmanager
865-
def _set_event_loop_policy(policy):
866-
old_policy = asyncio.get_event_loop_policy()
867-
asyncio.set_event_loop_policy(policy)
868-
try:
869-
yield
870-
finally:
871-
asyncio.set_event_loop_policy(old_policy)
856+
if sys.version_info >= (3, 12):
872857

858+
def run(*args, **kwargs):
859+
return asyncio.run(
860+
*args,
861+
**kwargs,
862+
loop_factory=_get_qevent_loop,
863+
)
864+
else:
865+
# backwards compatibility with event loop policies
866+
class DefaultQEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
867+
def new_event_loop(self):
868+
return _get_qevent_loop()
869+
870+
@contextlib.contextmanager
871+
def _set_event_loop_policy(policy):
872+
old_policy = asyncio.get_event_loop_policy()
873+
asyncio.set_event_loop_policy(policy)
874+
try:
875+
yield
876+
finally:
877+
asyncio.set_event_loop_policy(old_policy)
873878

874-
def run(*args, **kwargs):
875-
with _set_event_loop_policy(DefaultQEventLoopPolicy()):
876-
return asyncio.run(*args, **kwargs)
879+
def run(*args, **kwargs):
880+
with _set_event_loop_policy(DefaultQEventLoopPolicy()):
881+
return asyncio.run(*args, **kwargs)

tests/test_qeventloop.py

Lines changed: 5 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def test_can_handle_exception_in_executor(self, loop, executor):
105105
loop.run_until_complete(
106106
asyncio.wait_for(
107107
loop.run_in_executor(executor, self.blocking_failure),
108-
timeout=3.0,
108+
timeout=10.0,
109109
)
110110
)
111111

@@ -126,7 +126,7 @@ def blocking_func(self, was_invoked):
126126
async def blocking_task(self, loop, executor, was_invoked):
127127
logging.debug("start blocking task()")
128128
fut = loop.run_in_executor(executor, self.blocking_func, was_invoked)
129-
await asyncio.wait_for(fut, timeout=5.0)
129+
await asyncio.wait_for(fut, timeout=10.0)
130130
logging.debug("start blocking task()")
131131

132132

@@ -140,7 +140,7 @@ async def mycoro():
140140
await process.wait()
141141
assert process.returncode == 5
142142

143-
loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3))
143+
loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=10.0))
144144

145145

146146
def test_can_read_subprocess(loop):
@@ -160,7 +160,7 @@ async def mycoro():
160160
assert process.returncode == 0
161161
assert received_stdout.strip() == b"Hello async world!"
162162

163-
loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3))
163+
loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=10.0))
164164

165165

166166
def test_can_communicate_subprocess(loop):
@@ -181,7 +181,7 @@ async def mycoro():
181181
assert process.returncode == 0
182182
assert received_stdout.strip() == b"Hello async world!"
183183

184-
loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=3))
184+
loop.run_until_complete(asyncio.wait_for(mycoro(), timeout=10.0))
185185

186186

187187
def test_can_terminate_subprocess(loop):
@@ -809,103 +809,6 @@ async def mycoro():
809809
assert "seconds" in msg
810810

811811

812-
@pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires Python 3.12+")
813-
def test_asyncio_run(application):
814-
"""Test that QEventLoop is compatible with asyncio.run()"""
815-
done = False
816-
loop = None
817-
818-
async def main():
819-
nonlocal done, loop
820-
assert loop.is_running()
821-
assert asyncio.get_running_loop() is loop
822-
await asyncio.sleep(0.01)
823-
done = True
824-
825-
def factory():
826-
nonlocal loop
827-
loop = qasync.QEventLoop(application)
828-
return loop
829-
830-
asyncio.run(main(), loop_factory=factory)
831-
assert done
832-
assert loop.is_closed()
833-
assert not loop.is_running()
834-
835-
836-
@pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires Python 3.12+")
837-
def test_asyncio_run_cleanup(application):
838-
"""Test that running tasks are cleaned up"""
839-
task = None
840-
cancelled = False
841-
842-
async def main():
843-
nonlocal task, cancelled
844-
845-
async def long_task():
846-
nonlocal cancelled
847-
try:
848-
await asyncio.sleep(10)
849-
except asyncio.CancelledError:
850-
cancelled = True
851-
852-
task = asyncio.create_task(long_task())
853-
await asyncio.sleep(0.01)
854-
855-
asyncio.run(main(), loop_factory=lambda: qasync.QEventLoop(application))
856-
assert cancelled
857-
858-
859-
def test_qasync_run(application):
860-
"""Test running with qasync.run()"""
861-
done = False
862-
loop = None
863-
864-
async def main():
865-
nonlocal done, loop
866-
loop = asyncio.get_running_loop()
867-
assert loop.is_running()
868-
await asyncio.sleep(0.01)
869-
done = True
870-
871-
# qasync.run uses an EventLoopPolicy to create the loop
872-
qasync.run(main())
873-
assert done
874-
assert loop.is_closed()
875-
assert not loop.is_running()
876-
877-
878-
def test_qeventloop_in_qthread():
879-
class CoroutineExecutorThread(qasync.QtCore.QThread):
880-
def __init__(self, coro):
881-
super().__init__()
882-
self.coro = coro
883-
self.loop = None
884-
885-
def run(self):
886-
self.loop = qasync.QEventLoop(self)
887-
asyncio.set_event_loop(self.loop)
888-
asyncio.run(self.coro)
889-
890-
def join(self):
891-
self.loop.stop()
892-
self.loop.close()
893-
self.wait()
894-
895-
event = threading.Event()
896-
897-
async def coro():
898-
await asyncio.sleep(0.1)
899-
event.set()
900-
901-
thread = CoroutineExecutorThread(coro())
902-
thread.start()
903-
904-
assert event.wait(timeout=1), "Coroutine did not execute successfully"
905-
906-
thread.join() # Ensure thread cleanup
907-
908-
909812
def teardown_module(module):
910813
"""
911814
Remove handlers from all loggers

tests/test_run.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import asyncio
2+
import sys
23
from unittest.mock import ANY
34

45
import pytest
@@ -25,6 +26,7 @@ def test_qasync_run_restores_loop(get_event_loop_coro):
2526
_ = asyncio.get_event_loop()
2627

2728

29+
@pytest.mark.skipif(sys.version_info >= (3, 14), reason="Deprecated since Python 3.14")
2830
def test_qasync_run_restores_policy(get_event_loop_coro):
2931
old_policy = asyncio.get_event_loop_policy()
3032
qasync.run(get_event_loop_coro(ANY))
@@ -35,3 +37,69 @@ def test_qasync_run_restores_policy(get_event_loop_coro):
3537
def test_qasync_run_with_debug_args(get_event_loop_coro):
3638
qasync.run(get_event_loop_coro(True), debug=True)
3739
qasync.run(get_event_loop_coro(False), debug=False)
40+
41+
42+
@pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires Python 3.12+")
43+
def test_asyncio_run(application):
44+
"""Test that QEventLoop is compatible with asyncio.run()"""
45+
done = False
46+
loop = None
47+
48+
async def main():
49+
nonlocal done, loop
50+
assert loop.is_running()
51+
assert asyncio.get_running_loop() is loop
52+
await asyncio.sleep(0.01)
53+
done = True
54+
55+
def factory():
56+
nonlocal loop
57+
loop = qasync.QEventLoop(application)
58+
return loop
59+
60+
asyncio.run(main(), loop_factory=factory)
61+
assert done
62+
assert loop.is_closed()
63+
assert not loop.is_running()
64+
65+
66+
@pytest.mark.skipif(sys.version_info < (3, 12), reason="Requires Python 3.12+")
67+
def test_asyncio_run_cleanup(application):
68+
"""Test that running tasks are cleaned up"""
69+
task = None
70+
cancelled = False
71+
72+
async def main():
73+
nonlocal task, cancelled
74+
75+
async def long_task():
76+
nonlocal cancelled
77+
try:
78+
await asyncio.sleep(10)
79+
except asyncio.CancelledError:
80+
cancelled = True
81+
82+
task = asyncio.create_task(long_task())
83+
await asyncio.sleep(0.01)
84+
85+
asyncio.run(main(), loop_factory=lambda: qasync.QEventLoop(application))
86+
assert cancelled
87+
88+
89+
def test_qasync_run(application):
90+
"""Test running with qasync.run()"""
91+
done = False
92+
loop = None
93+
94+
async def main():
95+
nonlocal done, loop
96+
loop = asyncio.get_running_loop()
97+
assert loop.is_running()
98+
await asyncio.sleep(0.01)
99+
done = True
100+
101+
# qasync.run uses an EventLoopPolicy to create the loop
102+
qasync.run(main())
103+
assert done
104+
assert loop.is_closed()
105+
assert not loop.is_running()

0 commit comments

Comments
 (0)