Skip to content

Commit 1ce5841

Browse files
authored
bpo-31033: Add a msg argument to Future.cancel() and Task.cancel() (GH-19979)
1 parent fe1176e commit 1ce5841

File tree

10 files changed

+358
-73
lines changed

10 files changed

+358
-73
lines changed

Doc/library/asyncio-future.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,17 @@ Future Object
170170
Returns the number of callbacks removed, which is typically 1,
171171
unless a callback was added more than once.
172172

173-
.. method:: cancel()
173+
.. method:: cancel(msg=None)
174174

175175
Cancel the Future and schedule callbacks.
176176

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

181+
.. versionchanged:: 3.9
182+
Added the ``msg`` parameter.
183+
181184
.. method:: exception()
182185

183186
Return the exception that was set on this Future.
@@ -255,3 +258,6 @@ the Future has a result::
255258
- asyncio Future is not compatible with the
256259
:func:`concurrent.futures.wait` and
257260
:func:`concurrent.futures.as_completed` functions.
261+
262+
- :meth:`asyncio.Future.cancel` accepts an optional ``msg`` argument,
263+
but :func:`concurrent.futures.cancel` does not.

Doc/library/asyncio-task.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ Task Object
724724
.. deprecated-removed:: 3.8 3.10
725725
The *loop* parameter.
726726

727-
.. method:: cancel()
727+
.. method:: cancel(msg=None)
728728

729729
Request the Task to be cancelled.
730730

@@ -739,6 +739,9 @@ Task Object
739739
suppressing cancellation completely is not common and is actively
740740
discouraged.
741741

742+
.. versionchanged:: 3.9
743+
Added the ``msg`` parameter.
744+
742745
.. _asyncio_example_task_cancel:
743746

744747
The following example illustrates how coroutines can intercept

Lib/asyncio/futures.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ class Future:
5151
_exception = None
5252
_loop = None
5353
_source_traceback = None
54+
_cancel_message = None
5455

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

126-
def cancel(self):
127+
def cancel(self, msg=None):
127128
"""Cancel the future and schedule callbacks.
128129
129130
If the future is already done or cancelled, return False. Otherwise,
@@ -134,6 +135,7 @@ def cancel(self):
134135
if self._state != _PENDING:
135136
return False
136137
self._state = _CANCELLED
138+
self._cancel_message = msg
137139
self.__schedule_callbacks()
138140
return True
139141

@@ -173,7 +175,9 @@ def result(self):
173175
the future is done and has an exception set, this exception is raised.
174176
"""
175177
if self._state == _CANCELLED:
176-
raise exceptions.CancelledError
178+
raise exceptions.CancelledError(
179+
'' if self._cancel_message is None else self._cancel_message)
180+
177181
if self._state != _FINISHED:
178182
raise exceptions.InvalidStateError('Result is not ready.')
179183
self.__log_traceback = False
@@ -190,7 +194,8 @@ def exception(self):
190194
InvalidStateError.
191195
"""
192196
if self._state == _CANCELLED:
193-
raise exceptions.CancelledError
197+
raise exceptions.CancelledError(
198+
'' if self._cancel_message is None else self._cancel_message)
194199
if self._state != _FINISHED:
195200
raise exceptions.InvalidStateError('Exception is not set.')
196201
self.__log_traceback = False

Lib/asyncio/tasks.py

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def print_stack(self, *, limit=None, file=None):
230230
"""
231231
return base_tasks._task_print_stack(self, limit, file)
232232

233-
def cancel(self):
233+
def cancel(self, msg=None):
234234
"""Request that this task cancel itself.
235235
236236
This arranges for a CancelledError to be thrown into the
@@ -254,13 +254,14 @@ def cancel(self):
254254
if self.done():
255255
return False
256256
if self._fut_waiter is not None:
257-
if self._fut_waiter.cancel():
257+
if self._fut_waiter.cancel(msg=msg):
258258
# Leave self._fut_waiter; it may be a Task that
259259
# catches and ignores the cancellation so we may have
260260
# to cancel it again later.
261261
return True
262262
# It must be the case that self.__step is already scheduled.
263263
self._must_cancel = True
264+
self._cancel_message = msg
264265
return True
265266

266267
def __step(self, exc=None):
@@ -269,7 +270,8 @@ def __step(self, exc=None):
269270
f'_step(): already done: {self!r}, {exc!r}')
270271
if self._must_cancel:
271272
if not isinstance(exc, exceptions.CancelledError):
272-
exc = exceptions.CancelledError()
273+
exc = exceptions.CancelledError(''
274+
if self._cancel_message is None else self._cancel_message)
273275
self._must_cancel = False
274276
coro = self._coro
275277
self._fut_waiter = None
@@ -287,11 +289,15 @@ def __step(self, exc=None):
287289
if self._must_cancel:
288290
# Task is cancelled right before coro stops.
289291
self._must_cancel = False
290-
super().cancel()
292+
super().cancel(msg=self._cancel_message)
291293
else:
292294
super().set_result(exc.value)
293-
except exceptions.CancelledError:
294-
super().cancel() # I.e., Future.cancel(self).
295+
except exceptions.CancelledError as exc:
296+
if exc.args:
297+
cancel_msg = exc.args[0]
298+
else:
299+
cancel_msg = None
300+
super().cancel(msg=cancel_msg) # I.e., Future.cancel(self).
295301
except (KeyboardInterrupt, SystemExit) as exc:
296302
super().set_exception(exc)
297303
raise
@@ -319,7 +325,8 @@ def __step(self, exc=None):
319325
self.__wakeup, context=self._context)
320326
self._fut_waiter = result
321327
if self._must_cancel:
322-
if self._fut_waiter.cancel():
328+
if self._fut_waiter.cancel(
329+
msg=self._cancel_message):
323330
self._must_cancel = False
324331
else:
325332
new_exc = RuntimeError(
@@ -716,12 +723,12 @@ def __init__(self, children, *, loop=None):
716723
self._children = children
717724
self._cancel_requested = False
718725

719-
def cancel(self):
726+
def cancel(self, msg=None):
720727
if self.done():
721728
return False
722729
ret = False
723730
for child in self._children:
724-
if child.cancel():
731+
if child.cancel(msg=msg):
725732
ret = True
726733
if ret:
727734
# If any child tasks were actually cancelled, we should
@@ -780,7 +787,8 @@ def _done_callback(fut):
780787
# Check if 'fut' is cancelled first, as
781788
# 'fut.exception()' will *raise* a CancelledError
782789
# instead of returning it.
783-
exc = exceptions.CancelledError()
790+
exc = exceptions.CancelledError(''
791+
if fut._cancel_message is None else fut._cancel_message)
784792
outer.set_exception(exc)
785793
return
786794
else:
@@ -799,7 +807,9 @@ def _done_callback(fut):
799807
# Check if 'fut' is cancelled first, as
800808
# 'fut.exception()' will *raise* a CancelledError
801809
# instead of returning it.
802-
res = exceptions.CancelledError()
810+
res = exceptions.CancelledError(
811+
'' if fut._cancel_message is None else
812+
fut._cancel_message)
803813
else:
804814
res = fut.exception()
805815
if res is None:
@@ -810,7 +820,9 @@ def _done_callback(fut):
810820
# If gather is being cancelled we must propagate the
811821
# cancellation regardless of *return_exceptions* argument.
812822
# See issue 32684.
813-
outer.set_exception(exceptions.CancelledError())
823+
exc = exceptions.CancelledError(''
824+
if fut._cancel_message is None else fut._cancel_message)
825+
outer.set_exception(exc)
814826
else:
815827
outer.set_result(results)
816828

Lib/asyncio/windows_events.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ def _cancel_overlapped(self):
7575
self._loop.call_exception_handler(context)
7676
self._ov = None
7777

78-
def cancel(self):
78+
def cancel(self, msg=None):
7979
self._cancel_overlapped()
80-
return super().cancel()
80+
return super().cancel(msg=msg)
8181

8282
def set_exception(self, exception):
8383
super().set_exception(exception)
@@ -149,9 +149,9 @@ def _unregister_wait(self):
149149

150150
self._unregister_wait_cb(None)
151151

152-
def cancel(self):
152+
def cancel(self, msg=None):
153153
self._unregister_wait()
154-
return super().cancel()
154+
return super().cancel(msg=msg)
155155

156156
def set_exception(self, exception):
157157
self._unregister_wait()

Lib/test/test_asyncio/test_futures.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,27 @@ def test_uninitialized(self):
201201
self.assertFalse(fut.cancelled())
202202
self.assertFalse(fut.done())
203203

204+
def test_future_cancel_message_getter(self):
205+
f = self._new_future(loop=self.loop)
206+
self.assertTrue(hasattr(f, '_cancel_message'))
207+
self.assertEqual(f._cancel_message, None)
208+
209+
f.cancel('my message')
210+
with self.assertRaises(asyncio.CancelledError):
211+
self.loop.run_until_complete(f)
212+
self.assertEqual(f._cancel_message, 'my message')
213+
214+
def test_future_cancel_message_setter(self):
215+
f = self._new_future(loop=self.loop)
216+
f.cancel('my message')
217+
f._cancel_message = 'my new message'
218+
self.assertEqual(f._cancel_message, 'my new message')
219+
220+
# Also check that the value is used for cancel().
221+
with self.assertRaises(asyncio.CancelledError):
222+
self.loop.run_until_complete(f)
223+
self.assertEqual(f._cancel_message, 'my new message')
224+
204225
def test_cancel(self):
205226
f = self._new_future(loop=self.loop)
206227
self.assertTrue(f.cancel())

0 commit comments

Comments
 (0)