diff options
| author | Adrian Herrmann <adrian.herrmann@qt.io> | 2023-12-23 18:07:26 +0100 |
|---|---|---|
| committer | Adrian Herrmann <adrian.herrmann@qt.io> | 2024-01-07 16:21:29 +0100 |
| commit | b91596118fda9b586f19adcb1feee1db40abeb86 (patch) | |
| tree | 52f9ab13bc7c2ea1b43e4a430317f27c09bf6bdd /sources/pyside6 | |
| parent | 10a75de16b1d5e8f4acd1af121aaa56cbe4e65a4 (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.py | 13 | ||||
| -rw-r--r-- | sources/pyside6/PySide6/QtAsyncio/tasks.py | 10 | ||||
| -rw-r--r-- | sources/pyside6/tests/QtAsyncio/qasyncio_test_cancel_task.py | 46 |
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() |
