Skip to content

bpo-31033: Add a msg argument to Future.cancel() and Task.cancel() #19979

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Doc/library/asyncio-future.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,17 @@ Future Object
Returns the number of callbacks removed, which is typically 1,
unless a callback was added more than once.

.. method:: cancel()
.. method:: cancel(msg=None)

Cancel the Future and schedule callbacks.

If the Future is already *done* or *cancelled*, return ``False``.
Otherwise, change the Future's state to *cancelled*,
schedule the callbacks, and return ``True``.

.. versionchanged:: 3.9
Added the ``msg`` parameter.

.. method:: exception()

Return the exception that was set on this Future.
Expand Down Expand Up @@ -255,3 +258,6 @@ the Future has a result::
- asyncio Future is not compatible with the
:func:`concurrent.futures.wait` and
:func:`concurrent.futures.as_completed` functions.

- :meth:`asyncio.Future.cancel` accepts an optional ``msg`` argument,
but :func:`concurrent.futures.cancel` does not.
5 changes: 4 additions & 1 deletion Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ Task Object
.. deprecated-removed:: 3.8 3.10
The *loop* parameter.

.. method:: cancel()
.. method:: cancel(msg=None)

Request the Task to be cancelled.

Expand All @@ -738,6 +738,9 @@ Task Object
suppressing cancellation completely is not common and is actively
discouraged.

.. versionchanged:: 3.9
Added the ``msg`` parameter.

.. _asyncio_example_task_cancel:

The following example illustrates how coroutines can intercept
Expand Down
11 changes: 8 additions & 3 deletions Lib/asyncio/futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class Future:
_exception = None
_loop = None
_source_traceback = None
_cancel_message = None

# This field is used for a dual purpose:
# - Its presence is a marker to declare that a class implements
Expand Down Expand Up @@ -123,7 +124,7 @@ def get_loop(self):
raise RuntimeError("Future object is not initialized.")
return loop

def cancel(self):
def cancel(self, msg=None):
"""Cancel the future and schedule callbacks.

If the future is already done or cancelled, return False. Otherwise,
Expand All @@ -134,6 +135,7 @@ def cancel(self):
if self._state != _PENDING:
return False
self._state = _CANCELLED
self._cancel_message = msg
self.__schedule_callbacks()
return True

Expand Down Expand Up @@ -173,7 +175,9 @@ def result(self):
the future is done and has an exception set, this exception is raised.
"""
if self._state == _CANCELLED:
raise exceptions.CancelledError
raise exceptions.CancelledError(
'' if self._cancel_message is None else self._cancel_message)

if self._state != _FINISHED:
raise exceptions.InvalidStateError('Result is not ready.')
self.__log_traceback = False
Expand All @@ -190,7 +194,8 @@ def exception(self):
InvalidStateError.
"""
if self._state == _CANCELLED:
raise exceptions.CancelledError
raise exceptions.CancelledError(
'' if self._cancel_message is None else self._cancel_message)
if self._state != _FINISHED:
raise exceptions.InvalidStateError('Exception is not set.')
self.__log_traceback = False
Expand Down
36 changes: 24 additions & 12 deletions Lib/asyncio/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ def print_stack(self, *, limit=None, file=None):
"""
return base_tasks._task_print_stack(self, limit, file)

def cancel(self):
def cancel(self, msg=None):
"""Request that this task cancel itself.

This arranges for a CancelledError to be thrown into the
Expand All @@ -254,13 +254,14 @@ def cancel(self):
if self.done():
return False
if self._fut_waiter is not None:
if self._fut_waiter.cancel():
if self._fut_waiter.cancel(msg=msg):
# Leave self._fut_waiter; it may be a Task that
# catches and ignores the cancellation so we may have
# to cancel it again later.
return True
# It must be the case that self.__step is already scheduled.
self._must_cancel = True
self._cancel_message = msg
return True

def __step(self, exc=None):
Expand All @@ -269,7 +270,8 @@ def __step(self, exc=None):
f'_step(): already done: {self!r}, {exc!r}')
if self._must_cancel:
if not isinstance(exc, exceptions.CancelledError):
exc = exceptions.CancelledError()
exc = exceptions.CancelledError(''
if self._cancel_message is None else self._cancel_message)
self._must_cancel = False
coro = self._coro
self._fut_waiter = None
Expand All @@ -287,11 +289,15 @@ def __step(self, exc=None):
if self._must_cancel:
# Task is cancelled right before coro stops.
self._must_cancel = False
super().cancel()
super().cancel(msg=self._cancel_message)
else:
super().set_result(exc.value)
except exceptions.CancelledError:
super().cancel() # I.e., Future.cancel(self).
except exceptions.CancelledError as exc:
if exc.args:
cancel_msg = exc.args[0]
else:
cancel_msg = None
super().cancel(msg=cancel_msg) # I.e., Future.cancel(self).
except (KeyboardInterrupt, SystemExit) as exc:
super().set_exception(exc)
raise
Expand Down Expand Up @@ -319,7 +325,8 @@ def __step(self, exc=None):
self.__wakeup, context=self._context)
self._fut_waiter = result
if self._must_cancel:
if self._fut_waiter.cancel():
if self._fut_waiter.cancel(
msg=self._cancel_message):
self._must_cancel = False
else:
new_exc = RuntimeError(
Expand Down Expand Up @@ -708,12 +715,12 @@ def __init__(self, children, *, loop=None):
self._children = children
self._cancel_requested = False

def cancel(self):
def cancel(self, msg=None):
if self.done():
return False
ret = False
for child in self._children:
if child.cancel():
if child.cancel(msg=msg):
ret = True
if ret:
# If any child tasks were actually cancelled, we should
Expand Down Expand Up @@ -772,7 +779,8 @@ def _done_callback(fut):
# Check if 'fut' is cancelled first, as
# 'fut.exception()' will *raise* a CancelledError
# instead of returning it.
exc = exceptions.CancelledError()
exc = exceptions.CancelledError(''
if fut._cancel_message is None else fut._cancel_message)
outer.set_exception(exc)
return
else:
Expand All @@ -791,7 +799,9 @@ def _done_callback(fut):
# Check if 'fut' is cancelled first, as
# 'fut.exception()' will *raise* a CancelledError
# instead of returning it.
res = exceptions.CancelledError()
res = exceptions.CancelledError(
'' if fut._cancel_message is None else
fut._cancel_message)
else:
res = fut.exception()
if res is None:
Expand All @@ -802,7 +812,9 @@ def _done_callback(fut):
# If gather is being cancelled we must propagate the
# cancellation regardless of *return_exceptions* argument.
# See issue 32684.
outer.set_exception(exceptions.CancelledError())
exc = exceptions.CancelledError(''
if fut._cancel_message is None else fut._cancel_message)
outer.set_exception(exc)
else:
outer.set_result(results)

Expand Down
8 changes: 4 additions & 4 deletions Lib/asyncio/windows_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ def _cancel_overlapped(self):
self._loop.call_exception_handler(context)
self._ov = None

def cancel(self):
def cancel(self, msg=None):
self._cancel_overlapped()
return super().cancel()
return super().cancel(msg=msg)

def set_exception(self, exception):
super().set_exception(exception)
Expand Down Expand Up @@ -149,9 +149,9 @@ def _unregister_wait(self):

self._unregister_wait_cb(None)

def cancel(self):
def cancel(self, msg=None):
self._unregister_wait()
return super().cancel()
return super().cancel(msg=msg)

def set_exception(self, exception):
self._unregister_wait()
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_asyncio/test_futures.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,27 @@ def test_uninitialized(self):
self.assertFalse(fut.cancelled())
self.assertFalse(fut.done())

def test_future_cancel_message_getter(self):
f = self._new_future(loop=self.loop)
self.assertTrue(hasattr(f, '_cancel_message'))
self.assertEqual(f._cancel_message, None)

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')
f._cancel_message = 'my new message'
self.assertEqual(f._cancel_message, 'my new message')

# Also check that the value is used for cancel().
with self.assertRaises(asyncio.CancelledError):
self.loop.run_until_complete(f)
self.assertEqual(f._cancel_message, 'my new message')

def test_cancel(self):
f = self._new_future(loop=self.loop)
self.assertTrue(f.cancel())
Expand Down
Loading