From 0360e9f34659e7d7f3dae021b82f78452db8c714 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Wed, 23 Mar 2022 17:43:05 +0200 Subject: [PATCH] bpo-46829: Deprecate passing a message into Future.cancel() and Task.cancel() (GH-31840) After a long deliberation we ended up feeling that the message argument for Future.cancel(), added in 3.9, was a bad idea, so we're deprecating it in 3.11 and plan to remove it in 3.13. --- Doc/library/asyncio-future.rst | 10 ++++ Doc/library/asyncio-task.rst | 6 ++- Lib/asyncio/futures.py | 6 +++ Lib/asyncio/tasks.py | 5 ++ Lib/test/test_asyncio/test_futures.py | 12 ++++- Lib/test/test_asyncio/test_taskgroups.py | 8 +-- Lib/test/test_asyncio/test_tasks.py | 54 ++++++++++++++++--- .../2022-03-12-21-07-21.bpo-46829.cpGoPV.rst | 2 + Modules/_asynciomodule.c | 20 +++++++ 9 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2022-03-12-21-07-21.bpo-46829.cpGoPV.rst diff --git a/Doc/library/asyncio-future.rst b/Doc/library/asyncio-future.rst index 7426e8291e1..f74f2e6f893 100644 --- a/Doc/library/asyncio-future.rst +++ b/Doc/library/asyncio-future.rst @@ -196,6 +196,11 @@ Future Object .. versionchanged:: 3.9 Added the *msg* parameter. + .. deprecated-removed:: 3.11 3.14 + *msg* parameter is ambiguous when multiple :meth:`cancel` + are called with different cancellation messages. + The argument will be removed. + .. method:: exception() Return the exception that was set on this Future. @@ -276,3 +281,8 @@ the Future has a result:: - :meth:`asyncio.Future.cancel` accepts an optional ``msg`` argument, but :func:`concurrent.futures.cancel` does not. + + .. deprecated-removed:: 3.11 3.14 + *msg* parameter is ambiguous when multiple :meth:`cancel` + are called with different cancellation messages. + The argument will be removed. diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 294f5ab2b22..21a4cb5820b 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -810,8 +810,10 @@ Task Object .. versionchanged:: 3.9 Added the *msg* parameter. - .. versionchanged:: 3.11 - The ``msg`` parameter is propagated from cancelled task to its awaiter. + .. deprecated-removed:: 3.11 3.14 + *msg* parameter is ambiguous when multiple :meth:`cancel` + are called with different cancellation messages. + The argument will be removed. .. _asyncio_example_task_cancel: diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index bfd4ee926f0..08c79e7b510 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -8,6 +8,7 @@ import concurrent.futures import contextvars import logging import sys +import warnings from types import GenericAlias from . import base_futures @@ -150,6 +151,11 @@ class Future: change the future's state to cancelled, schedule the callbacks and return True. """ + if msg is not None: + warnings.warn("Passing 'msg' argument to Future.cancel() " + "is deprecated since Python 3.11, and " + "scheduled for removal in Python 3.14.", + DeprecationWarning, stacklevel=2) self.__log_traceback = False if self._state != _PENDING: return False diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index e876f8d002c..27fe58da151 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -207,6 +207,11 @@ class Task(futures._PyFuture): # Inherit Python Task implementation This also increases the task's count of cancellation requests. """ + if msg is not None: + warnings.warn("Passing 'msg' argument to Task.cancel() " + "is deprecated since Python 3.11, and " + "scheduled for removal in Python 3.14.", + DeprecationWarning, stacklevel=2) self._log_traceback = False if self.done(): return False diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index 84d7d45af94..cf677f6a951 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -228,14 +228,22 @@ class BaseFutureTests: self.assertTrue(hasattr(f, '_cancel_message')) self.assertEqual(f._cancel_message, None) - f.cancel('my message') + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + f.cancel('my message') with self.assertRaises(asyncio.CancelledError): self.loop.run_until_complete(f) self.assertEqual(f._cancel_message, 'my message') def test_future_cancel_message_setter(self): f = self._new_future(loop=self.loop) - f.cancel('my message') + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + f.cancel('my message') f._cancel_message = 'my new message' self.assertEqual(f._cancel_message, 'my new message') diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index dea5d6de524..69369a6100a 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -191,12 +191,10 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase): await asyncio.sleep(0.1) self.assertFalse(r.done()) - r.cancel("test") + r.cancel() with self.assertRaises(asyncio.CancelledError) as cm: await r - self.assertEqual(cm.exception.args, ('test',)) - self.assertEqual(NUM, 5) async def test_taskgroup_07(self): @@ -253,12 +251,10 @@ class TestTaskGroup(unittest.IsolatedAsyncioTestCase): await asyncio.sleep(0.1) self.assertFalse(r.done()) - r.cancel("test") + r.cancel() with self.assertRaises(asyncio.CancelledError) as cm: await r - self.assertEqual(cm.exception.args, ('test',)) - async def test_taskgroup_09(self): t1 = t2 = None diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index b86646ee398..8df1957bbe9 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -113,7 +113,11 @@ class BaseTaskTests: self.assertTrue(hasattr(t, '_cancel_message')) self.assertEqual(t._cancel_message, None) - t.cancel('my message') + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + t.cancel('my message') self.assertEqual(t._cancel_message, 'my message') with self.assertRaises(asyncio.CancelledError) as cm: @@ -125,7 +129,11 @@ class BaseTaskTests: async def coro(): pass t = self.new_task(self.loop, coro()) - t.cancel('my message') + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + t.cancel('my message') t._cancel_message = 'my new message' self.assertEqual(t._cancel_message, 'my new message') @@ -582,7 +590,14 @@ class BaseTaskTests: async def coro(): task = self.new_task(loop, sleep()) await asyncio.sleep(0) - task.cancel(*cancel_args) + if cancel_args not in ((), (None,)): + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + task.cancel(*cancel_args) + else: + task.cancel(*cancel_args) done, pending = await asyncio.wait([task]) task.result() @@ -616,7 +631,14 @@ class BaseTaskTests: async def coro(): task = self.new_task(loop, sleep()) await asyncio.sleep(0) - task.cancel(*cancel_args) + if cancel_args not in ((), (None,)): + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + task.cancel(*cancel_args) + else: + task.cancel(*cancel_args) done, pending = await asyncio.wait([task]) task.exception() @@ -639,10 +661,17 @@ class BaseTaskTests: fut.set_result(None) await asyncio.sleep(10) + def cancel(task, msg): + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + task.cancel(msg) + async def coro(): inner_task = self.new_task(loop, sleep()) await fut - loop.call_soon(inner_task.cancel, 'msg') + loop.call_soon(cancel, inner_task, 'msg') try: await inner_task except asyncio.CancelledError as ex: @@ -668,7 +697,11 @@ class BaseTaskTests: async def coro(): task = self.new_task(loop, sleep()) # We deliberately leave out the sleep here. - task.cancel('my message') + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + task.cancel('my message') done, pending = await asyncio.wait([task]) task.exception() @@ -2029,7 +2062,14 @@ class BaseTaskTests: async def main(): qwe = self.new_task(loop, test()) await asyncio.sleep(0.2) - qwe.cancel(*cancel_args) + if cancel_args not in ((), (None,)): + with self.assertWarnsRegex( + DeprecationWarning, + "Passing 'msg' argument" + ): + qwe.cancel(*cancel_args) + else: + qwe.cancel(*cancel_args) await qwe try: diff --git a/Misc/NEWS.d/next/Core and Builtins/2022-03-12-21-07-21.bpo-46829.cpGoPV.rst b/Misc/NEWS.d/next/Core and Builtins/2022-03-12-21-07-21.bpo-46829.cpGoPV.rst new file mode 100644 index 00000000000..9d260f5b1dd --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2022-03-12-21-07-21.bpo-46829.cpGoPV.rst @@ -0,0 +1,2 @@ +Deprecate passing a message into :meth:`asyncio.Future.cancel` and +:meth:`asyncio.Task.cancel` diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index c3e9cb2fa2a..5b8555e74b3 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1096,6 +1096,16 @@ static PyObject * _asyncio_Future_cancel_impl(FutureObj *self, PyObject *msg) /*[clinic end generated code: output=3edebbc668e5aba3 input=925eb545251f2c5a]*/ { + if (msg != Py_None) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Passing 'msg' argument to Future.cancel() " + "is deprecated since Python 3.11, and " + "scheduled for removal in Python 3.14.", + 2)) + { + return NULL; + } + } ENSURE_FUTURE_ALIVE(self) return future_cancel(self, msg); } @@ -2176,6 +2186,16 @@ static PyObject * _asyncio_Task_cancel_impl(TaskObj *self, PyObject *msg) /*[clinic end generated code: output=c66b60d41c74f9f1 input=7bb51bf25974c783]*/ { + if (msg != Py_None) { + if (PyErr_WarnEx(PyExc_DeprecationWarning, + "Passing 'msg' argument to Task.cancel() " + "is deprecated since Python 3.11, and " + "scheduled for removal in Python 3.14.", + 2)) + { + return NULL; + } + } self->task_log_tb = 0; if (self->task_state != STATE_PENDING) {