diff options
| author | Adrian Herrmann <adrian.herrmann@qt.io> | 2024-07-03 18:36:20 +0200 |
|---|---|---|
| committer | Adrian Herrmann <adrian.herrmann@qt.io> | 2024-07-16 06:32:02 +0200 |
| commit | 526bc12e42db7c6305bcc28ad8f6b7554597d725 (patch) | |
| tree | a98f909fdf5c893bfe5d2937e0246acdae688dea /sources/pyside6 | |
| parent | 32c36073e212e7ca17b1f7578d61195aefd9dbca (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.py | 27 | ||||
| -rw-r--r-- | sources/pyside6/tests/QtAsyncio/bug_2790.py | 47 | ||||
| -rw-r--r-- | sources/pyside6/tests/QtAsyncio/qasyncio_test_uncancel.py | 61 |
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() |
