aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6
diff options
context:
space:
mode:
authorAdrian Herrmann <adrian.herrmann@qt.io>2023-12-23 18:07:26 +0100
committerAdrian Herrmann <adrian.herrmann@qt.io>2024-01-07 16:21:29 +0100
commitb91596118fda9b586f19adcb1feee1db40abeb86 (patch)
tree52f9ab13bc7c2ea1b43e4a430317f27c09bf6bdd /sources/pyside6
parent10a75de16b1d5e8f4acd1af121aaa56cbe4e65a4 (diff)
QtAsyncio: Fix cancelling waiting tasks
A task that is awaiting a future must also cancel this future in order for the cancellation to be successful. Pick-to: 6.6 Task-number: PYSIDE-769 Change-Id: I22a9132fc8506e7a007fe625bc9217f0760bdc6b Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
Diffstat (limited to 'sources/pyside6')
-rw-r--r--sources/pyside6/PySide6/QtAsyncio/futures.py13
-rw-r--r--sources/pyside6/PySide6/QtAsyncio/tasks.py10
-rw-r--r--sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py46
3 files changed, 61 insertions, 8 deletions
diff --git a/sources/pyside6/PySide6/QtAsyncio/futures.py b/sources/pyside6/PySide6/QtAsyncio/futures.py
index f4ac2c561..0cf94ebb1 100644
--- a/sources/pyside6/PySide6/QtAsyncio/futures.py
+++ b/sources/pyside6/PySide6/QtAsyncio/futures.py
@@ -25,8 +25,9 @@ class QAsyncioFuture():
def __init__(self, *, loop: typing.Optional["events.QAsyncioEventLoop"] = None,
context: typing.Optional[contextvars.Context] = None) -> None:
+ self._loop: "events.QAsyncioEventLoop"
if loop is None:
- self._loop = asyncio.events.get_event_loop()
+ self._loop = asyncio.events.get_event_loop() # type: ignore[assignment]
else:
self._loop = loop
self._context = context
@@ -50,10 +51,9 @@ class QAsyncioFuture():
__iter__ = __await__
def _schedule_callbacks(self, context: typing.Optional[contextvars.Context] = None):
- if self._loop.is_running():
- for cb in self._callbacks:
- self._loop.call_soon(
- cb, self, context=context if context else self._context)
+ for cb in self._callbacks:
+ self._loop.call_soon(
+ cb, self, context=context if context else self._context)
def result(self) -> typing.Union[typing.Any, Exception]:
if self._state == QAsyncioFuture.FutureState.DONE_WITH_RESULT:
@@ -96,10 +96,11 @@ class QAsyncioFuture():
self._callbacks = [_cb for _cb in self._callbacks if _cb != cb]
return original_len - len(self._callbacks)
- def cancel(self) -> bool:
+ def cancel(self, msg: typing.Optional[str] = None) -> bool:
if self.done():
return False
self._state = QAsyncioFuture.FutureState.CANCELLED
+ self._cancel_message = msg
self._schedule_callbacks()
return True
diff --git a/sources/pyside6/PySide6/QtAsyncio/tasks.py b/sources/pyside6/PySide6/QtAsyncio/tasks.py
index c8e7da7e4..4f214c65a 100644
--- a/sources/pyside6/PySide6/QtAsyncio/tasks.py
+++ b/sources/pyside6/PySide6/QtAsyncio/tasks.py
@@ -27,6 +27,9 @@ class QAsyncioTask(futures.QAsyncioFuture):
self._cancellation_requests = 0
+ self._future_to_await: typing.Optional[asyncio.Future] = None
+ self._cancel_message: typing.Optional[str] = None
+
asyncio._register_task(self) # type: ignore[arg-type]
def __repr__(self) -> str:
@@ -66,6 +69,7 @@ class QAsyncioTask(futures.QAsyncioFuture):
if self.done():
return
result = None
+ self._future_to_await = None
try:
asyncio._enter_task(self._loop, self) # type: ignore[arg-type]
@@ -83,7 +87,7 @@ class QAsyncioTask(futures.QAsyncioFuture):
except StopIteration as e:
self._state = futures.QAsyncioFuture.FutureState.DONE_WITH_RESULT
self._result = e.value
- except concurrent.futures.CancelledError as e:
+ except (concurrent.futures.CancelledError, asyncio.exceptions.CancelledError) as e:
self._state = futures.QAsyncioFuture.FutureState.CANCELLED
self._exception = e
except BaseException as e:
@@ -93,6 +97,7 @@ class QAsyncioTask(futures.QAsyncioFuture):
if asyncio.futures.isfuture(result):
result.add_done_callback(
self._step, context=self._context) # type: ignore[arg-type]
+ self._future_to_await = result
elif result is None:
self._loop.call_soon(self._step, context=self._context)
else:
@@ -137,7 +142,8 @@ class QAsyncioTask(futures.QAsyncioFuture):
return False
self._cancel_message = msg
self._handle.cancel()
- self._state = futures.QAsyncioFuture.FutureState.CANCELLED
+ if self._future_to_await is not None:
+ self._future_to_await.cancel(msg)
return True
def uncancel(self) -> None:
diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py
new file mode 100644
index 000000000..7ef2bb90d
--- /dev/null
+++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py
@@ -0,0 +1,46 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+'''Test cases for QtAsyncio'''
+
+import asyncio
+import unittest
+
+import PySide6.QtAsyncio as QtAsyncio
+
+
+class QAsyncioTestCaseCancelTask(unittest.TestCase):
+ # Taken from https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel
+
+ async def cancel_me(self, output):
+ output += "(1) cancel_me(): before sleep"
+
+ try:
+ await asyncio.sleep(10)
+ except asyncio.CancelledError:
+ output += "(2) cancel_me(): cancel sleep"
+ raise
+ finally:
+ output += "(3) cancel_me(): after sleep"
+
+ async def main(self, output):
+ task = asyncio.create_task(self.cancel_me(output))
+ await asyncio.sleep(0.1)
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ output += "(4) main(): cancel_me is cancelled now"
+
+ def test_await_tasks(self):
+ output_expected = []
+ output_real = []
+
+ asyncio.run(self.main(output_expected))
+ QtAsyncio.run(self.main(output_real), keep_running=False)
+
+ self.assertEqual(output_real, output_expected)
+
+
+if __name__ == '__main__':
+ unittest.main()