Skip to content

Commit 9addefc

Browse files
authored
Merge pull request #159 from kristjanvalur/create-task
Convert "ensure_future" to "create_task"
2 parents 0927491 + 6f8e6e6 commit 9addefc

2 files changed

Lines changed: 134 additions & 16 deletions

File tree

src/qasync/__init__.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ def _get_qt_flavor() -> QtFlavor:
8181

8282
from ._common import with_logger # noqa
8383

84+
# strong references to running background tasks
85+
background_tasks = set()
86+
8487

8588
@with_logger
8689
class _QThreadWorker(QtCore.QThread):
@@ -415,6 +418,8 @@ def run_until_complete(self, future):
415418
raise RuntimeError("Event loop already running")
416419

417420
self.__log_debug("Running %s until complete", future)
421+
422+
# future may actually be a coroutine. This ensures it is wrapped in a Task.
418423
future = asyncio.ensure_future(future, loop=self)
419424

420425
def stop(*args):
@@ -834,23 +839,27 @@ def asyncClose(fn):
834839

835840
@functools.wraps(fn)
836841
def wrapper(*args, **kwargs):
837-
f = asyncio.ensure_future(fn(*args, **kwargs))
838-
while not f.done():
839-
QApplication.instance().processEvents()
842+
loop = asyncio.get_running_loop()
843+
assert isinstance(loop, QEventLoop)
844+
task = loop.create_task(fn(*args, **kwargs))
845+
while not task.done():
846+
QApplication.processEvents(AllEvents)
847+
try:
848+
return task.result()
849+
except asyncio.CancelledError:
850+
pass
840851

841852
return wrapper
842853

843854

844855
def asyncSlot(*args, **kwargs):
845856
"""Make a Qt async slot run on asyncio loop."""
846857

847-
def _error_handler(task):
858+
async def _error_handler(fn, args, kwargs):
848859
try:
849-
task.result()
860+
await fn(*args, **kwargs)
850861
except Exception:
851862
sys.excepthook(*sys.exc_info())
852-
except asyncio.CancelledError:
853-
pass
854863

855864
def outer_decorator(fn):
856865
@Slot(*args, **kwargs)
@@ -873,9 +882,10 @@ def wrapper(*args, **kwargs):
873882
"asyncSlot was not callable from Signal. Potential signature mismatch."
874883
)
875884
else:
876-
task = asyncio.ensure_future(fn(*args, **kwargs))
877-
task.add_done_callback(_error_handler)
878-
return task
885+
task = asyncio.create_task(_error_handler(fn, args, kwargs))
886+
background_tasks.add(task)
887+
task.add_done_callback(background_tasks.discard)
888+
return
879889

880890
return wrapper
881891

tests/test_qeventloop.py

Lines changed: 114 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import threading
1515
import time
1616
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
17+
from unittest import mock
1718

1819
import pytest
1920

@@ -595,8 +596,8 @@ def cb3():
595596

596597
loop._add_reader(c_sock.fileno(), cb1)
597598

598-
_client_task = asyncio.ensure_future(client_coro())
599-
_server_task = asyncio.ensure_future(server_coro())
599+
_client_task = loop.create_task(client_coro())
600+
_server_task = loop.create_task(server_coro())
600601

601602
both_done = asyncio.gather(client_done, server_done)
602603
loop.run_until_complete(asyncio.wait_for(both_done, timeout=1.0))
@@ -640,8 +641,8 @@ async def client_coro():
640641
loop._remove_reader(c_sock.fileno())
641642
assert (await loop.sock_recv(c_sock, 3)) == b"foo"
642643

643-
client_done = asyncio.ensure_future(client_coro())
644-
server_done = asyncio.ensure_future(server_coro())
644+
client_done = loop.create_task(client_coro())
645+
server_done = loop.create_task(server_coro())
645646

646647
both_done = asyncio.wait(
647648
[server_done, client_done], return_when=asyncio.FIRST_EXCEPTION
@@ -758,7 +759,7 @@ def exct_handler(loop, data):
758759
handler_called = True
759760

760761
loop.set_exception_handler(exct_handler)
761-
asyncio.ensure_future(future_except())
762+
loop.create_task(future_except())
762763
loop.run_forever()
763764

764765
assert coro_run
@@ -775,7 +776,11 @@ def exct_handler(loop, data):
775776
loop.set_exception_handler(exct_handler)
776777
fut1 = asyncio.Future()
777778
fut1.set_exception(ExceptionTester())
778-
asyncio.ensure_future(fut1)
779+
780+
async def coro(future):
781+
await future
782+
783+
loop.create_task(coro(fut1))
779784
del fut1
780785
loop.call_later(0.1, loop.stop)
781786
loop.run_forever()
@@ -798,6 +803,8 @@ def test_async_slot(loop):
798803
no_args_called = asyncio.Event()
799804
with_args_called = asyncio.Event()
800805
trailing_args_called = asyncio.Event()
806+
error_called = asyncio.Event()
807+
cancel_called = asyncio.Event()
801808

802809
async def slot_no_args():
803810
no_args_called.set()
@@ -812,6 +819,14 @@ async def slot_trailing_args(flag: bool):
812819

813820
async def slot_signature_mismatch(_: bool): ...
814821

822+
async def slot_with_error():
823+
error_called.set()
824+
raise ValueError("Test")
825+
826+
async def slot_with_cancel():
827+
cancel_called.set()
828+
raise asyncio.CancelledError()
829+
815830
async def main():
816831
# passing kwargs to the underlying Slot such as name, arguments, return
817832
sig = qasync._make_signaller(qasync.QtCore)
@@ -836,6 +851,73 @@ async def main():
836851
)
837852
await asyncio.wait_for(all_done, timeout=1.0)
838853

854+
with mock.patch.object(sys, "excepthook") as excepthook:
855+
sig3 = qasync._make_signaller(qasync.QtCore)
856+
sig3.signal.connect(qasync.asyncSlot()(slot_with_error))
857+
sig3.signal.emit()
858+
await asyncio.wait_for(error_called.wait(), timeout=1.0)
859+
excepthook.assert_called_once()
860+
assert isinstance(excepthook.call_args[0][1], ValueError)
861+
862+
with mock.patch.object(sys, "excepthook") as excepthook:
863+
sig4 = qasync._make_signaller(qasync.QtCore)
864+
sig4.signal.connect(qasync.asyncSlot()(slot_with_cancel))
865+
sig4.signal.emit()
866+
await asyncio.wait_for(cancel_called.wait(), timeout=1.0)
867+
excepthook.assert_not_called()
868+
869+
loop.run_until_complete(main())
870+
871+
872+
def test_async_close(loop, application):
873+
close_called = asyncio.Event()
874+
close_err_called = asyncio.Event()
875+
close_hang_called = asyncio.Event()
876+
877+
@qasync.asyncClose
878+
async def close():
879+
close_called.set()
880+
return 33
881+
882+
@qasync.asyncClose
883+
async def close_err():
884+
close_err_called.set()
885+
raise ValueError("Test")
886+
887+
@qasync.asyncClose
888+
async def close_hang():
889+
# do an actual cancel instead of directly raising, for completeness.
890+
current = asyncio.current_task()
891+
assert current is not None
892+
893+
async def killer():
894+
await asyncio.sleep(0.001)
895+
current.cancel()
896+
897+
asyncio.create_task(killer())
898+
close_hang_called.set()
899+
await asyncio.Event().wait()
900+
assert False, "Should have been cancelled"
901+
902+
# need to run in async context to have a running event loop
903+
async def main():
904+
# close() is a synchronous top level call, need
905+
# to wrap it to be able to enter event loop
906+
907+
# test that a regular close works
908+
assert await qasync.asyncWrap(close) == 33
909+
assert close_called.is_set()
910+
911+
# test that an exception in the async close is propagated
912+
with pytest.raises(ValueError) as err:
913+
await qasync.asyncWrap(close_err)
914+
assert err.value.args[0] == "Test"
915+
assert close_err_called.is_set()
916+
917+
# test that a CancelledError is not propagated
918+
assert await qasync.asyncWrap(close_hang) is None
919+
assert close_hang_called.is_set()
920+
839921
loop.run_until_complete(main())
840922

841923

@@ -915,6 +997,14 @@ async def coro():
915997
assert loop.run_until_complete(asyncio.wait_for(coro(), timeout=1)) == 42
916998

917999

1000+
def test_run_until_complete_future(loop):
1001+
"""Test that run_until_complete accepts futures"""
1002+
1003+
fut = asyncio.Future()
1004+
loop.call_soon(lambda: fut.set_result(42))
1005+
assert loop.run_until_complete(fut) == 42
1006+
1007+
9181008
def test_run_forever_custom_exit_code(loop, application):
9191009
if hasattr(application, "exec"):
9201010
orig_exec = application.exec
@@ -932,6 +1022,24 @@ def test_run_forever_custom_exit_code(loop, application):
9321022
application.exec_ = orig_exec
9331023

9341024

1025+
def test_loop_non_reentrant(loop):
1026+
async def noop():
1027+
pass
1028+
1029+
async def task():
1030+
t = loop.create_task(noop())
1031+
with pytest.raises(RuntimeError):
1032+
loop.run_forever()
1033+
1034+
with pytest.raises(RuntimeError):
1035+
loop.run_until_complete(t)
1036+
return 43
1037+
1038+
t = loop.create_task(task())
1039+
loop.run_until_complete(t)
1040+
assert t.result() == 43
1041+
1042+
9351043
@pytest.mark.parametrize("qtparent", [False, True])
9361044
def test_qeventloop_in_qthread(qtparent):
9371045
class CoroutineExecutorThread(qasync.QtCore.QThread):

0 commit comments

Comments
 (0)