aboutsummaryrefslogtreecommitdiffstats
path: root/sources/pyside6
diff options
context:
space:
mode:
authorAdrian Herrmann <adrian.herrmann@qt.io>2024-07-03 18:36:20 +0200
committerAdrian Herrmann <adrian.herrmann@qt.io>2024-07-16 06:32:02 +0200
commit526bc12e42db7c6305bcc28ad8f6b7554597d725 (patch)
treea98f909fdf5c893bfe5d2937e0246acdae688dea /sources/pyside6
parent32c36073e212e7ca17b1f7578d61195aefd9dbca (diff)
QtAsyncio: Add cancel count and uncancel
Implement the QAsyncioTask.uncancel() function and the associated cancel count. Note to reader: Unlike what the name suggests, the uncancel() function on its own does not undo a task cancellation. This must be performed by consuming the CancelledError exception, at which point uncancel() serves to remove the cancellation state. Pick-to: 6.7 Task-number: PYSIDE-769 Fixes: PYSIDE-2790 Change-Id: I4e817e1dd3f49179855432d20ed2f043090fd8f1 Reviewed-by: Shyamnath Premnadh <Shyamnath.Premnadh@qt.io> Reviewed-by: Friedemann Kleint <Friedemann.Kleint@qt.io>
Diffstat (limited to 'sources/pyside6')
-rw-r--r--sources/pyside6/PySide6/QtAsyncio/tasks.py27
-rw-r--r--sources/pyside6/tests/QtAsyncio/bug_2790.py47
-rw-r--r--sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py61
3 files changed, 126 insertions, 9 deletions
diff --git a/sources/pyside6/PySide6/QtAsyncio/tasks.py b/sources/pyside6/PySide6/QtAsyncio/tasks.py
index bd7884838..9a5698432 100644
--- a/sources/pyside6/PySide6/QtAsyncio/tasks.py
+++ b/sources/pyside6/PySide6/QtAsyncio/tasks.py
@@ -34,6 +34,7 @@ class QAsyncioTask(futures.QAsyncioFuture):
self._future_to_await: asyncio.Future | None = None
self._cancelled = False
+ self._cancel_count = 0
self._cancel_message: str | None = None
# https://docs.python.org/3/library/asyncio-extending.html#task-lifetime-support
@@ -77,6 +78,10 @@ class QAsyncioTask(futures.QAsyncioFuture):
result = None
self._future_to_await = None
+ if self._cancelled:
+ exception_or_future = asyncio.CancelledError(self._cancel_message)
+ self._cancelled = False
+
if asyncio.futures.isfuture(exception_or_future):
try:
exception_or_future.result()
@@ -135,10 +140,13 @@ class QAsyncioTask(futures.QAsyncioFuture):
asyncio._leave_task(self._loop, self) # type: ignore[arg-type]
if self._exception:
+ message = str(self._exception)
+ if message == "None":
+ message = ""
+ else:
+ message = "An exception occurred during task execution"
self._loop.call_exception_handler({
- "message": (str(self._exception) if self._exception
- else "An exception occurred during task "
- "execution"),
+ "message": message,
"exception": self._exception,
"task": self,
"future": (exception_or_future
@@ -172,6 +180,7 @@ class QAsyncioTask(futures.QAsyncioFuture):
def cancel(self, msg: str | None = None) -> bool:
if self.done():
return False
+ self._cancel_count += 1
self._cancel_message = msg
self._handle.cancel()
if self._future_to_await is not None:
@@ -181,10 +190,10 @@ class QAsyncioTask(futures.QAsyncioFuture):
self._cancelled = True
return True
- def uncancel(self) -> None:
- # TODO
- raise NotImplementedError("QtTask.uncancel is not implemented")
+ def uncancel(self) -> int:
+ if self._cancel_count > 0:
+ self._cancel_count -= 1
+ return self._cancel_count
- def cancelling(self) -> bool:
- # TODO
- raise NotImplementedError("QtTask.cancelling is not implemented")
+ def cancelling(self) -> int:
+ return self._cancel_count
diff --git a/sources/pyside6/tests/QtAsyncio/bug_2790.py b/sources/pyside6/tests/QtAsyncio/bug_2790.py
new file mode 100644
index 000000000..9fd152b15
--- /dev/null
+++ b/sources/pyside6/tests/QtAsyncio/bug_2790.py
@@ -0,0 +1,47 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+'''Test cases for QtAsyncio'''
+
+import unittest
+import asyncio
+
+import PySide6.QtAsyncio as QtAsyncio
+
+
+class QAsyncioTestCaseBug2790(unittest.TestCase):
+
+ async def producer(self, products: list[str]):
+ while True:
+ products.append("product")
+ await asyncio.sleep(2)
+
+ async def task(self, outputs: list[str]):
+ products = []
+ asyncio.ensure_future(self.producer(products))
+ for _ in range(6):
+ try:
+ async with asyncio.timeout(0.5):
+ while len(products) == 0:
+ await asyncio.sleep(0)
+ outputs.append(products.pop(0))
+ except TimeoutError:
+ outputs.append("Timeout")
+
+ def test_timeout(self):
+ # The Qt event loop (and thus QtAsyncio) does not guarantee that events
+ # will be processed in the order they were posted, so there is two
+ # possible outputs for this test.
+ outputs_expected_1 = ["product", "Timeout", "Timeout", "Timeout", "Timeout", "product"]
+ outputs_expected_2 = ["product", "Timeout", "Timeout", "Timeout", "product", "Timeout"]
+
+ outputs_real = []
+
+ QtAsyncio.run(self.task(outputs_real), keep_running=False)
+
+ self.assertTrue(outputs_real in [outputs_expected_1, outputs_expected_2])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py b/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py
new file mode 100644
index 000000000..036622845
--- /dev/null
+++ b/sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py
@@ -0,0 +1,61 @@
+# Copyright (C) 2024 The Qt Company Ltd.
+# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+from __future__ import annotations
+
+"""Test cases for QtAsyncio"""
+
+import unittest
+import asyncio
+
+import PySide6.QtAsyncio as QtAsyncio
+
+
+class QAsyncioTestCaseUncancel(unittest.TestCase):
+ """ https://superfastpython.com/asyncio-cancel-task-cancellation """
+
+ async def worker(self, outputs: list[str]):
+ # Ensure the task always gets done.
+ while True:
+ try:
+ await asyncio.sleep(2)
+ outputs.append("Task sleep completed normally")
+ break
+ except asyncio.CancelledError:
+ outputs.append("Task is cancelled, ignore and try again")
+ asyncio.current_task().uncancel()
+
+ async def main(self, outputs: list[str]):
+ task = asyncio.create_task(self.worker(outputs))
+ # Allow the task to run briefly.
+ await asyncio.sleep(0.5)
+ task.cancel()
+ try:
+ await task
+ except asyncio.CancelledError:
+ outputs.append("Task was cancelled")
+
+ cancelling = task.cancelling()
+ self.assertEqual(cancelling, 0)
+ outputs.append(f"Task cancelling: {cancelling}")
+
+ cancelled = task.cancelled()
+ self.assertFalse(cancelled)
+ outputs.append(f"Task cancelled: {cancelled}")
+
+ done = task.done()
+ self.assertTrue(done)
+ outputs.append(f"Task done: {done}")
+
+ def test_uncancel(self):
+ outputs_expected = []
+ outputs_real = []
+
+ asyncio.run(self.main(outputs_real))
+ QtAsyncio.run(self.main(outputs_expected), keep_running=False)
+
+ self.assertIsNotNone(outputs_real)
+ self.assertEqual(outputs_real, outputs_expected)
+
+
+if __name__ == "__main__":
+ unittest.main()