Skip to content

Commit 4338e01

Browse files
authored
Merge branch 'master' into sync
2 parents b263218 + e48ed41 commit 4338e01

8 files changed

Lines changed: 297 additions & 57 deletions

File tree

README.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,15 @@ class MainWindow(QWidget):
4646
if __name__ == "__main__":
4747
app = QApplication(sys.argv)
4848

49-
event_loop = QEventLoop(app)
50-
asyncio.set_event_loop(event_loop)
51-
5249
app_close_event = asyncio.Event()
5350
app.aboutToQuit.connect(app_close_event.set)
5451

5552
main_window = MainWindow()
5653
main_window.show()
5754

58-
with event_loop:
59-
event_loop.run_until_complete(app_close_event.wait())
55+
# for 3.11 or older use qasync.run instead of asyncio.run
56+
# qasync.run(app_close_event.wait())
57+
asyncio.run(app_close_event.wait(), loop_factory=QEventLoop)
6058
```
6159

6260
More detailed examples can be found [here](https://github.com/CabbageDevelopment/qasync/tree/master/examples).

examples/aiohttp_fetch.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
QVBoxLayout,
1414
QWidget,
1515
)
16+
1617
from qasync import QEventLoop, asyncClose, asyncSlot
1718

1819

@@ -68,15 +69,16 @@ async def on_btn_fetch_clicked(self):
6869
if __name__ == "__main__":
6970
app = QApplication(sys.argv)
7071

71-
event_loop = QEventLoop(app)
72-
asyncio.set_event_loop(event_loop)
73-
7472
app_close_event = asyncio.Event()
7573
app.aboutToQuit.connect(app_close_event.set)
76-
74+
7775
main_window = MainWindow()
7876
main_window.show()
7977

80-
event_loop.create_task(main_window.boot())
81-
event_loop.run_until_complete(app_close_event.wait())
82-
event_loop.close()
78+
async def async_main():
79+
asyncio.create_task(main_window.boot())
80+
await app_close_event.wait()
81+
82+
# for 3.11 or older use qasync.run instead of asyncio.run
83+
# qasync.run(async_main())
84+
asyncio.run(async_main(), loop_factory=QEventLoop)

examples/executor_example.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import functools
21
import asyncio
3-
import time
2+
import functools
43
import sys
4+
import time
55

66
# from PyQt6.QtWidgets import
77
from PySide6.QtWidgets import QApplication, QProgressBar
8+
89
from qasync import QEventLoop, QThreadExecutor
910

1011

@@ -34,8 +35,6 @@ def last_50(progress, loop):
3435
if __name__ == "__main__":
3536
app = QApplication(sys.argv)
3637

37-
event_loop = QEventLoop(app)
38-
asyncio.set_event_loop(event_loop)
39-
40-
event_loop.run_until_complete(master())
41-
event_loop.close()
38+
# for 3.11 or older use qasync.run instead of asyncio.run
39+
# qasync.run(master())
40+
asyncio.run(master(), loop_factory=QEventLoop)

qasync/__init__.py

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,28 @@
7878
from PyQt5.QtCore import pyqtSlot as Slot
7979

8080
QApplication = QtWidgets.QApplication
81+
AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00)
8182

8283
elif QtModuleName == "PyQt6":
8384
from PyQt6 import QtWidgets
8485
from PyQt6.QtCore import pyqtSlot as Slot
8586

8687
QApplication = QtWidgets.QApplication
88+
AllEvents = QtCore.QEventLoop.ProcessEventsFlag(0x00)
8789

8890
elif QtModuleName == "PySide2":
8991
from PySide2 import QtWidgets
9092
from PySide2.QtCore import Slot
9193

9294
QApplication = QtWidgets.QApplication
95+
AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00)
9396

9497
elif QtModuleName == "PySide6":
9598
from PySide6 import QtWidgets
9699
from PySide6.QtCore import Slot
97100

98101
QApplication = QtWidgets.QApplication
102+
AllEvents = QtCore.QEventLoop.ProcessEventsFlags(0x00)
99103

100104
from ._common import with_logger # noqa
101105

@@ -142,9 +146,15 @@ def run(self):
142146
else:
143147
self._logger.debug("Setting Future result: %s", r)
144148
future.set_result(r)
149+
finally:
150+
# Release potential reference
151+
r = None # noqa
145152
else:
146153
self._logger.debug("Future was canceled")
147154

155+
# Delete references
156+
del command, future, callback, args, kwargs
157+
148158
self._logger.debug("Thread #%s stopped", self.__num)
149159

150160
def wait(self):
@@ -228,7 +238,7 @@ def __exit__(self, *args):
228238

229239
def _format_handle(handle: asyncio.Handle):
230240
cb = getattr(handle, "_callback", None)
231-
if isinstance(getattr(cb, '__self__', None), asyncio.tasks.Task):
241+
if isinstance(getattr(cb, "__self__", None), asyncio.tasks.Task):
232242
return repr(cb.__self__)
233243
return str(handle)
234244

@@ -286,7 +296,11 @@ def timerEvent(self, event): # noqa: N802
286296
handle._run()
287297
dt = time.time() - t0
288298
if dt >= loop.slow_callback_duration:
289-
self._logger.warning('Executing %s took %.3f seconds', _format_handle(handle), dt)
299+
self._logger.warning(
300+
"Executing %s took %.3f seconds",
301+
_format_handle(handle),
302+
dt,
303+
)
290304
finally:
291305
loop._current_handle = None
292306
else:
@@ -331,10 +345,7 @@ class _QEventLoop:
331345
... assert x + y == 4
332346
... await asyncio.sleep(.1)
333347
>>>
334-
>>> loop = QEventLoop(app)
335-
>>> asyncio.set_event_loop(loop)
336-
>>> with loop:
337-
... loop.run_until_complete(xplusy(2, 2))
348+
>>> asyncio.run(xplusy(2, 2), loop_factory=lambda:QEventLoop(app))
338349
339350
If the event loop shall be used with an existing and already running QApplication
340351
it must be specified in the constructor via already_running=True
@@ -417,7 +428,9 @@ def stop(*args):
417428
self.run_forever()
418429
finally:
419430
future.remove_done_callback(stop)
420-
self.__app.processEvents() # run loop one last time to process all the events
431+
self.__app.eventDispatcher().processEvents(
432+
AllEvents
433+
) # run loop one last time to process all the events
421434
if not future.done():
422435
raise RuntimeError("Event loop stopped before Future completed.")
423436

@@ -801,6 +814,8 @@ def _error_handler(task):
801814
task.result()
802815
except Exception:
803816
sys.excepthook(*sys.exc_info())
817+
except asyncio.CancelledError:
818+
pass
804819

805820
def outer_decorator(fn):
806821
@Slot(*args, **kwargs)
@@ -871,28 +886,34 @@ def helper():
871886
QtCore.QTimer.singleShot(0, helper)
872887
return await future
873888

874-
class QEventLoopPolicyMixin:
875-
def new_event_loop(self):
876-
return QEventLoop(QApplication.instance() or QApplication(sys.argv))
877889

890+
def _get_qevent_loop():
891+
return QEventLoop(QApplication.instance() or QApplication(sys.argv))
878892

879-
class DefaultQEventLoopPolicy(
880-
QEventLoopPolicyMixin,
881-
asyncio.DefaultEventLoopPolicy,
882-
):
883-
pass
884893

894+
if sys.version_info >= (3, 12):
885895

886-
@contextlib.contextmanager
887-
def _set_event_loop_policy(policy):
888-
old_policy = asyncio.get_event_loop_policy()
889-
asyncio.set_event_loop_policy(policy)
890-
try:
891-
yield
892-
finally:
893-
asyncio.set_event_loop_policy(old_policy)
894-
896+
def run(*args, **kwargs):
897+
return asyncio.run(
898+
*args,
899+
**kwargs,
900+
loop_factory=_get_qevent_loop,
901+
)
902+
else:
903+
# backwards compatibility with event loop policies
904+
class DefaultQEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
905+
def new_event_loop(self):
906+
return _get_qevent_loop()
907+
908+
@contextlib.contextmanager
909+
def _set_event_loop_policy(policy):
910+
old_policy = asyncio.get_event_loop_policy()
911+
asyncio.set_event_loop_policy(policy)
912+
try:
913+
yield
914+
finally:
915+
asyncio.set_event_loop_policy(old_policy)
895916

896-
def run(*args, **kwargs):
897-
with _set_event_loop_policy(DefaultQEventLoopPolicy()):
898-
return asyncio.run(*args, **kwargs)
917+
def run(*args, **kwargs):
918+
with _set_event_loop_policy(DefaultQEventLoopPolicy()):
919+
return asyncio.run(*args, **kwargs)

tests/conftest.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
# © 2018 Gerard Marull-Paretas <gerard@teslabs.com>
2-
# © 2014 Mark Harviston <mark.harviston@gmail.com>
3-
# © 2014 Arve Knudsen <arve.knudsen@gmail.com>
4-
# BSD License
1+
"""
2+
Copyright (c) 2018 Gerard Marull-Paretas <gerard@teslabs.com>
3+
Copyright (c) 2014 Mark Harviston <mark.harviston@gmail.com>
4+
Copyright (c) 2014 Arve Knudsen <arve.knudsen@gmail.com>
5+
6+
BSD License
7+
"""
58

6-
import os
79
import logging
8-
from pytest import fixture
10+
import os
911

12+
from pytest import fixture
1013

1114
logging.basicConfig(
1215
level=logging.DEBUG, format="%(levelname)s\t%(filename)s:%(lineno)s %(message)s"

tests/test_qeventloop.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import socket
1212
import subprocess
1313
import sys
14+
import threading
1415
import time
1516
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
1617

@@ -104,7 +105,7 @@ def test_can_handle_exception_in_executor(self, loop, executor):
104105
loop.run_until_complete(
105106
asyncio.wait_for(
106107
loop.run_in_executor(executor, self.blocking_failure),
107-
timeout=3.0,
108+
timeout=10.0,
108109
)
109110
)
110111

@@ -125,7 +126,7 @@ def blocking_func(self, was_invoked):
125126
async def blocking_task(self, loop, executor, was_invoked):
126127
logging.debug("start blocking task()")
127128
fut = loop.run_in_executor(executor, self.blocking_func, was_invoked)
128-
await asyncio.wait_for(fut, timeout=5.0)
129+
await asyncio.wait_for(fut, timeout=10.0)
129130
logging.debug("start blocking task()")
130131

131132

@@ -139,7 +140,7 @@ async def mycoro():
139140
await process.wait()
140141
assert process.returncode == 5
141142

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

144145

145146
def test_can_read_subprocess(loop):
@@ -159,7 +160,7 @@ async def mycoro():
159160
assert process.returncode == 0
160161
assert received_stdout.strip() == b"Hello async world!"
161162

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

164165

165166
def test_can_communicate_subprocess(loop):
@@ -180,7 +181,7 @@ async def mycoro():
180181
assert process.returncode == 0
181182
assert received_stdout.strip() == b"Hello async world!"
182183

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

185186

186187
def test_can_terminate_subprocess(loop):
@@ -857,6 +858,61 @@ async def mycoro():
857858
assert "seconds" in msg
858859

859860

861+
def test_run_until_complete_returns_future_result(loop):
862+
async def coro():
863+
await asyncio.sleep(0)
864+
return 42
865+
866+
assert loop.run_until_complete(asyncio.wait_for(coro(), timeout=1)) == 42
867+
868+
869+
def test_run_forever_custom_exit_code(loop, application):
870+
if hasattr(application, "exec"):
871+
orig_exec = application.exec
872+
application.exec = lambda: 42
873+
try:
874+
assert loop.run_forever() == 42
875+
finally:
876+
application.exec = orig_exec
877+
else:
878+
orig_exec = application.exec_
879+
application.exec_ = lambda: 42
880+
try:
881+
assert loop.run_forever() == 42
882+
finally:
883+
application.exec_ = orig_exec
884+
885+
886+
def test_qeventloop_in_qthread():
887+
class CoroutineExecutorThread(qasync.QtCore.QThread):
888+
def __init__(self, coro):
889+
super().__init__()
890+
self.coro = coro
891+
self.loop = None
892+
893+
def run(self):
894+
self.loop = qasync.QEventLoop(self)
895+
asyncio.set_event_loop(self.loop)
896+
asyncio.run(self.coro)
897+
898+
def join(self):
899+
self.loop.stop()
900+
self.loop.close()
901+
self.wait()
902+
903+
event = threading.Event()
904+
905+
async def coro():
906+
await asyncio.sleep(0.1)
907+
event.set()
908+
909+
thread = CoroutineExecutorThread(coro())
910+
thread.start()
911+
912+
assert event.wait(timeout=1), "Coroutine did not execute successfully"
913+
914+
thread.join() # Ensure thread cleanup
915+
860916
def teardown_module(module):
861917
"""
862918
Remove handlers from all loggers

0 commit comments

Comments
 (0)