Skip to content

Commit 708cb27

Browse files
committed
gh-90908: Document asyncio.Task.cancelling() and asyncio.Task.uncancel()
1 parent ac6a94c commit 708cb27

File tree

2 files changed

+53
-3
lines changed

2 files changed

+53
-3
lines changed

Doc/library/asyncio-task.rst

+32
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ It is recommended that coroutines use ``try/finally`` blocks to robustly
293293
perform clean-up logic. In case :exc:`asyncio.CancelledError`
294294
is explicitly caught, it should generally be propagated when
295295
clean-up is complete. Most code can safely ignore :exc:`asyncio.CancelledError`.
296+
If a task needs to continue despite receiving an :exc:`asyncio.CancelledError`,
297+
it should :func:`uncancel itself <asyncio.Task.uncancel>`.
296298

297299
Important asyncio components, like :class:`asyncio.TaskGroup` and the
298300
:func:`asyncio.timeout` context manager, are implemented using cancellation
@@ -1064,6 +1066,36 @@ Task Object
10641066
:meth:`cancel` and the wrapped coroutine propagated the
10651067
:exc:`CancelledError` exception thrown into it.
10661068

1069+
.. method:: cancelling()
1070+
1071+
Return the number of cancellation requests to this Task, i.e.,
1072+
the number of calls to :meth:`cancel`.
1073+
1074+
Note that if this number is greater than zero but the Task is
1075+
still executing, :meth:`cancelled` will still return ``False``.
1076+
It's because this number can be lowered by calling :meth:`uncancel`,
1077+
which can lead to the task not being cancelled after all if the
1078+
cancellation requests go down to zero.
1079+
1080+
.. method:: uncancel()
1081+
1082+
Decrement the count of cancellation requests to this Task.
1083+
1084+
Returns the remaining number of cancellation requests.
1085+
1086+
This should be used by tasks that catch :exc:`CancelledError`
1087+
and wish to continue indefinitely until they are cancelled again::
1088+
1089+
async def resilient_task():
1090+
try:
1091+
await do_work()
1092+
except asyncio.CancelledError:
1093+
asyncio.current_task().uncancel()
1094+
await do_work()
1095+
1096+
Note that once execution of a cancelled task completed, further
1097+
calls to :meth:`uncancel` are ineffective.
1098+
10671099
.. method:: done()
10681100

10691101
Return ``True`` if the Task is *done*.

Lib/test/test_asyncio/test_tasks.py

+21-3
Original file line numberDiff line numberDiff line change
@@ -534,14 +534,32 @@ async def task():
534534
try:
535535
t = self.new_task(loop, task())
536536
loop.run_until_complete(asyncio.sleep(0.01))
537-
self.assertTrue(t.cancel()) # Cancel first sleep
537+
538+
# Cancel first sleep
539+
self.assertTrue(t.cancel())
538540
self.assertIn(" cancelling ", repr(t))
541+
self.assertEqual(t.cancelling(), 1)
542+
self.assertFalse(t.cancelled()) # Task is still not complete
539543
loop.run_until_complete(asyncio.sleep(0.01))
540-
self.assertNotIn(" cancelling ", repr(t)) # after .uncancel()
541-
self.assertTrue(t.cancel()) # Cancel second sleep
542544

545+
# after .uncancel()
546+
self.assertNotIn(" cancelling ", repr(t))
547+
self.assertEqual(t.cancelling(), 0)
548+
self.assertFalse(t.cancelled()) # Task is still not complete
549+
550+
# Cancel second sleep
551+
self.assertTrue(t.cancel())
552+
self.assertEqual(t.cancelling(), 1)
553+
self.assertFalse(t.cancelled()) # Task is still not complete
543554
with self.assertRaises(asyncio.CancelledError):
544555
loop.run_until_complete(t)
556+
self.assertTrue(t.cancelled()) # Finally, task complete
557+
self.assertTrue(t.done())
558+
559+
# uncancel is no longer effective after the task is complete
560+
t.uncancel()
561+
self.assertTrue(t.cancelled())
562+
self.assertTrue(t.done())
545563
finally:
546564
loop.close()
547565

0 commit comments

Comments
 (0)