From 96bef420d605e4012c312ef47e6ed8e0e4e63f29 Mon Sep 17 00:00:00 2001 From: kovan Date: Sat, 28 Feb 2026 09:44:09 +0100 Subject: [PATCH 1/2] Add asyncio.CancelScope with level-triggered cancellation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce CancelScope, an async context manager that provides level-triggered cancellation semantics for asyncio. Once cancelled (via cancel() or deadline expiry), every subsequent await inside the scope raises CancelledError until the scope exits — the coroutine cannot simply catch-and-ignore. This integrates with Task.__step by checking the current scope's state after the existing edge-triggered _must_cancel check. The scope pushes/pops itself on a per-task _current_cancel_scope linked-list stack. Existing edge-triggered mechanisms (cancel(), uncancel(), Timeout, TaskGroup) remain completely unchanged. New public API: - asyncio.CancelScope(*, deadline=None, shield=False) - asyncio.cancel_scope(delay, *, shield=False) - asyncio.cancel_scope_at(when, *, shield=False) Both the Python (_PyTask) and C (_CTask) Task implementations are updated. Code not using CancelScope has zero overhead (_current_cancel_scope is None → single pointer check). Co-Authored-By: Claude Opus 4.6 --- Lib/asyncio/__init__.py | 2 + Lib/asyncio/cancelscope.py | 154 +++++++++ Lib/asyncio/tasks.py | 6 + Lib/test/test_asyncio/test_cancelscope.py | 381 ++++++++++++++++++++++ Modules/_asynciomodule.c | 80 +++++ Modules/clinic/_asynciomodule.c.h | 42 +++ 6 files changed, 665 insertions(+) create mode 100644 Lib/asyncio/cancelscope.py create mode 100644 Lib/test/test_asyncio/test_cancelscope.py diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py index 32a5dbae03af21..53d4290c5a453a 100644 --- a/Lib/asyncio/__init__.py +++ b/Lib/asyncio/__init__.py @@ -6,6 +6,7 @@ # This relies on each of the submodules having an __all__ variable. from .base_events import * +from .cancelscope import * from .coroutines import * from .events import * from .exceptions import * @@ -24,6 +25,7 @@ from .transports import * __all__ = (base_events.__all__ + + cancelscope.__all__ + coroutines.__all__ + events.__all__ + exceptions.__all__ + diff --git a/Lib/asyncio/cancelscope.py b/Lib/asyncio/cancelscope.py new file mode 100644 index 00000000000000..57ffa2db2c4f12 --- /dev/null +++ b/Lib/asyncio/cancelscope.py @@ -0,0 +1,154 @@ +"""CancelScope — level-triggered cancellation for asyncio.""" + +__all__ = ('CancelScope', 'cancel_scope', 'cancel_scope_at') + +from . import events +from . import exceptions +from . import tasks + + +class CancelScope: + """Async context manager providing level-triggered cancellation. + + Once cancelled (via :meth:`cancel` or deadline expiry), every subsequent + ``await`` inside the scope raises :exc:`~asyncio.CancelledError` until the + scope exits. The coroutine cannot simply catch-and-ignore the error. + + Parameters + ---------- + deadline : float or None + Absolute event-loop time after which the scope auto-cancels. + shield : bool + If ``True``, the level-triggered re-injection is suppressed while + the scope is the current scope. + """ + + def __init__(self, *, deadline=None, shield=False): + self._deadline = deadline + self._shield = shield + self._cancel_called = False + self._task = None + self._parent_scope = None + self._timeout_handle = None + self._host_task_cancelling = 0 + self._cancelled_caught = False + + # -- public properties --------------------------------------------------- + + @property + def deadline(self): + """Absolute event-loop time of the deadline, or *None*.""" + return self._deadline + + @deadline.setter + def deadline(self, value): + self._deadline = value + if self._task is not None and not self._task.done(): + self._reschedule() + + @property + def shield(self): + """Whether level-triggered re-injection is suppressed.""" + return self._shield + + @shield.setter + def shield(self, value): + self._shield = value + + @property + def cancel_called(self): + """``True`` after :meth:`cancel` has been called.""" + return self._cancel_called + + @property + def cancelled_caught(self): + """``True`` if the scope caught the :exc:`CancelledError` on exit.""" + return self._cancelled_caught + + # -- control methods ----------------------------------------------------- + + def cancel(self): + """Cancel this scope. + + All subsequent awaits inside the scope will raise + :exc:`~asyncio.CancelledError`. + """ + if not self._cancel_called: + self._cancel_called = True + if self._task is not None and not self._task.done(): + self._task.cancel() + + def reschedule(self, deadline): + """Change the deadline. + + If *deadline* is ``None`` the timeout is removed. + """ + self._deadline = deadline + if self._task is not None: + self._reschedule() + + # -- async context manager ----------------------------------------------- + + async def __aenter__(self): + task = tasks.current_task() + if task is None: + # Fallback: _PyTask uses Python-level tracking that the + # C current_task() does not see. + task = tasks._py_current_task() + if task is None: + raise RuntimeError("CancelScope requires a running task") + self._task = task + self._host_task_cancelling = task.cancelling() + self._parent_scope = task._current_cancel_scope + task._current_cancel_scope = self + if self._deadline is not None: + loop = events.get_running_loop() + self._timeout_handle = loop.call_at( + self._deadline, self._on_deadline) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if self._timeout_handle is not None: + self._timeout_handle.cancel() + self._timeout_handle = None + + # Pop scope stack + self._task._current_cancel_scope = self._parent_scope + + if self._cancel_called: + # Consume the one cancel() we injected, bringing the task's + # cancellation counter back to where it was on __aenter__. + if self._task.cancelling() > self._host_task_cancelling: + self._task.uncancel() + if exc_type is not None and issubclass( + exc_type, exceptions.CancelledError): + self._cancelled_caught = True + return True # suppress the CancelledError + + return False + + # -- internal helpers ---------------------------------------------------- + + def _reschedule(self): + if self._timeout_handle is not None: + self._timeout_handle.cancel() + self._timeout_handle = None + if self._deadline is not None and not self._task.done(): + loop = events.get_running_loop() + self._timeout_handle = loop.call_at( + self._deadline, self._on_deadline) + + def _on_deadline(self): + self._timeout_handle = None + self.cancel() + + +def cancel_scope(delay, *, shield=False): + """Return a :class:`CancelScope` that expires *delay* seconds from now.""" + loop = events.get_running_loop() + return CancelScope(deadline=loop.time() + delay, shield=shield) + + +def cancel_scope_at(when, *, shield=False): + """Return a :class:`CancelScope` that expires at absolute time *when*.""" + return CancelScope(deadline=when, shield=shield) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index fbd5c39a7c56ac..105a9b00fdefae 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -101,6 +101,7 @@ def __init__(self, coro, *, loop=None, name=None, context=None, self._must_cancel = False self._fut_waiter = None self._coro = coro + self._current_cancel_scope = None if context is None: self._context = contextvars.copy_context() else: @@ -271,6 +272,11 @@ def __step(self, exc=None): if not isinstance(exc, exceptions.CancelledError): exc = self._make_cancelled_error() self._must_cancel = False + elif (self._current_cancel_scope is not None + and self._current_cancel_scope._cancel_called + and not self._current_cancel_scope._shield + and not isinstance(exc, exceptions.CancelledError)): + exc = self._make_cancelled_error() self._fut_waiter = None _py_enter_task(self._loop, self) diff --git a/Lib/test/test_asyncio/test_cancelscope.py b/Lib/test/test_asyncio/test_cancelscope.py new file mode 100644 index 00000000000000..6c23baea8bfd5e --- /dev/null +++ b/Lib/test/test_asyncio/test_cancelscope.py @@ -0,0 +1,381 @@ +"""Tests for asyncio/cancelscope.py""" + +import unittest + +import asyncio +from asyncio import tasks + + +def tearDownModule(): + asyncio.events._set_event_loop_policy(None) + + +class BaseCancelScopeTests: + """Mixin of CancelScope tests run against both Python and C Task.""" + + Task = None # set by subclasses + + def _run(self, coro): + """Run *coro* using asyncio.run() but with the desired Task class.""" + loop = asyncio.new_event_loop() + if self.Task is not None: + loop.set_task_factory( + lambda loop, coro, **kw: self.Task(coro, loop=loop, **kw)) + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + # -- basic tests --------------------------------------------------------- + + def test_cancel_raises_at_next_await(self): + async def main(): + async with asyncio.CancelScope() as scope: + scope.cancel() + with self.assertRaises(asyncio.CancelledError): + await asyncio.sleep(0) + # CancelledError was caught inside, so cancelled_caught is False + self.assertFalse(scope.cancelled_caught) + + self._run(main()) + + def test_cancel_propagates_to_scope(self): + """CancelledError propagates to __aexit__ and is suppressed.""" + async def main(): + async with asyncio.CancelScope() as scope: + scope.cancel() + await asyncio.sleep(0) # raises CancelledError, NOT caught + # Scope suppressed it + self.assertTrue(scope.cancelled_caught) + + self._run(main()) + + def test_cancel_called_property(self): + async def main(): + scope = asyncio.CancelScope() + self.assertFalse(scope.cancel_called) + scope.cancel() + self.assertTrue(scope.cancel_called) + + self._run(main()) + + def test_scope_without_cancel(self): + async def main(): + async with asyncio.CancelScope() as scope: + await asyncio.sleep(0) + self.assertFalse(scope.cancel_called) + self.assertFalse(scope.cancelled_caught) + + self._run(main()) + + def test_scope_requires_task(self): + async def main(): + async with asyncio.CancelScope() as scope: + pass + self.assertFalse(scope.cancel_called) + + self._run(main()) + + # -- level-triggered re-injection ---------------------------------------- + + def test_level_triggered_reinjection(self): + """Once cancelled, CancelledError re-raises at every subsequent await.""" + caught_count = 0 + + async def main(): + nonlocal caught_count + async with asyncio.CancelScope() as scope: + scope.cancel() + for _ in range(5): + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + caught_count += 1 + + self._run(main()) + self.assertEqual(caught_count, 5) + + def test_level_triggered_successive_catches(self): + """Multiple try/except blocks each catch a fresh CancelledError.""" + async def main(): + async with asyncio.CancelScope() as scope: + scope.cancel() + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + # still cancelled — next await raises again + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + # Error was caught inside each time; cancelled_caught is False + self.assertFalse(scope.cancelled_caught) + + self._run(main()) + + def test_no_reinjection_after_scope_exits(self): + """CancelledError stops once the scope is exited.""" + async def main(): + async with asyncio.CancelScope() as scope: + scope.cancel() + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + # Outside scope — no re-injection + await asyncio.sleep(0) # should NOT raise + + self._run(main()) + + # -- deadline / timeout -------------------------------------------------- + + def test_deadline_fires(self): + """Deadline causes CancelledError which the scope suppresses.""" + async def main(): + loop = asyncio.get_running_loop() + deadline = loop.time() + 0.01 + async with asyncio.CancelScope(deadline=deadline) as scope: + await asyncio.sleep(10) + self.assertTrue(scope.cancel_called) + self.assertTrue(scope.cancelled_caught) + + self._run(main()) + + def test_deadline_caught_inside(self): + """Deadline fires, error caught inside, scope exits normally.""" + async def main(): + loop = asyncio.get_running_loop() + deadline = loop.time() + 0.01 + async with asyncio.CancelScope(deadline=deadline) as scope: + try: + await asyncio.sleep(10) + except asyncio.CancelledError: + pass + self.assertTrue(scope.cancel_called) + self.assertFalse(scope.cancelled_caught) + + self._run(main()) + + def test_cancel_scope_convenience(self): + """cancel_scope(delay) fires and suppresses the CancelledError.""" + async def main(): + async with asyncio.cancel_scope(0.01) as scope: + await asyncio.sleep(10) + self.assertTrue(scope.cancel_called) + self.assertTrue(scope.cancelled_caught) + + self._run(main()) + + def test_cancel_scope_at_convenience(self): + """cancel_scope_at(when) fires and suppresses the CancelledError.""" + async def main(): + loop = asyncio.get_running_loop() + async with asyncio.cancel_scope_at(loop.time() + 0.01) as scope: + await asyncio.sleep(10) + self.assertTrue(scope.cancel_called) + self.assertTrue(scope.cancelled_caught) + + self._run(main()) + + def test_reschedule(self): + """reschedule() to a sooner deadline fires.""" + async def main(): + loop = asyncio.get_running_loop() + async with asyncio.CancelScope(deadline=loop.time() + 100) as scope: + scope.reschedule(loop.time() + 0.01) + await asyncio.sleep(10) + self.assertTrue(scope.cancel_called) + self.assertTrue(scope.cancelled_caught) + + self._run(main()) + + def test_reschedule_remove(self): + async def main(): + loop = asyncio.get_running_loop() + async with asyncio.CancelScope(deadline=loop.time() + 0.01) as scope: + # Remove deadline + scope.reschedule(None) + await asyncio.sleep(0.05) + self.assertFalse(scope.cancel_called) + + self._run(main()) + + # -- shield -------------------------------------------------------------- + + def test_shield_blocks_reinjection(self): + """shield=True prevents level-triggered re-injection.""" + async def main(): + async with asyncio.CancelScope(shield=True) as scope: + scope.cancel() + # The initial cancel() still sends one edge-triggered + # CancelledError; catch it. + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + # With shield, subsequent awaits should NOT re-inject + await asyncio.sleep(0) # should NOT raise + + self._run(main()) + + def test_shield_property(self): + async def main(): + scope = asyncio.CancelScope(shield=True) + self.assertTrue(scope.shield) + scope.shield = False + self.assertFalse(scope.shield) + + self._run(main()) + + # -- nested scopes ------------------------------------------------------- + + def test_inner_cancelled_outer_not(self): + async def main(): + async with asyncio.CancelScope() as outer: + async with asyncio.CancelScope() as inner: + inner.cancel() + await asyncio.sleep(0) # raises, propagates to inner + # inner suppressed the CancelledError + self.assertTrue(inner.cancelled_caught) + # Outer scope is not cancelled, should work fine + await asyncio.sleep(0) + self.assertFalse(outer.cancel_called) + + self._run(main()) + + def test_outer_cancelled_inner_not(self): + async def main(): + async with asyncio.CancelScope() as outer: + outer.cancel() + async with asyncio.CancelScope() as inner: + # inner is not cancelled; the edge-triggered cancel from + # outer.cancel() → task.cancel() comes through once. + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + # No level-triggered re-injection from inner (it's not cancelled) + await asyncio.sleep(0) + + self._run(main()) + + def test_nested_both_cancelled(self): + async def main(): + async with asyncio.CancelScope() as outer: + outer.cancel() + async with asyncio.CancelScope() as inner: + inner.cancel() + # CancelledError propagates to inner scope + await asyncio.sleep(0) + # inner suppresses it + self.assertTrue(inner.cancelled_caught) + # outer is still cancelled; next await re-injects + await asyncio.sleep(0) + # outer suppresses its own CancelledError + self.assertTrue(outer.cancelled_caught) + + self._run(main()) + + # -- edge-triggered unchanged -------------------------------------------- + + def test_plain_task_cancel_unchanged(self): + """task.cancel() without CancelScope remains edge-triggered.""" + async def inner(): + try: + await asyncio.sleep(10) + except asyncio.CancelledError: + # Swallow: edge-triggered, so it stays swallowed + return 'swallowed' + return 'completed' + + async def main(): + task = asyncio.ensure_future(inner()) + await asyncio.sleep(0) # let inner start + task.cancel() + result = await task + self.assertEqual(result, 'swallowed') + + self._run(main()) + + # -- cancelled_caught property ------------------------------------------- + + def test_cancelled_caught_true(self): + """cancelled_caught is True when CancelledError propagates to scope.""" + async def main(): + async with asyncio.CancelScope() as scope: + scope.cancel() + await asyncio.sleep(0) # raises, NOT caught → to __aexit__ + self.assertTrue(scope.cancelled_caught) + + self._run(main()) + + def test_cancelled_caught_false_when_not_cancelled(self): + async def main(): + async with asyncio.CancelScope() as scope: + await asyncio.sleep(0) + self.assertFalse(scope.cancelled_caught) + + self._run(main()) + + def test_cancelled_caught_false_when_caught_inside(self): + """cancelled_caught is False when CancelledError caught inside scope.""" + async def main(): + async with asyncio.CancelScope() as scope: + scope.cancel() + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + pass + self.assertTrue(scope.cancel_called) + self.assertFalse(scope.cancelled_caught) + + self._run(main()) + + # -- deadline property --------------------------------------------------- + + def test_deadline_property(self): + async def main(): + scope = asyncio.CancelScope(deadline=42.0) + self.assertEqual(scope.deadline, 42.0) + scope.deadline = 99.0 + self.assertEqual(scope.deadline, 99.0) + + self._run(main()) + + def test_deadline_none(self): + async def main(): + scope = asyncio.CancelScope() + self.assertIsNone(scope.deadline) + + self._run(main()) + + +class PyTask_CancelScopeTests(BaseCancelScopeTests, unittest.TestCase): + Task = tasks._PyTask + + +@unittest.skipUnless(hasattr(tasks, '_CTask'), + 'requires the C _asyncio module') +class CTask_CancelScopeTests(BaseCancelScopeTests, unittest.TestCase): + Task = getattr(tasks, '_CTask', None) + + def test_cancel_scope_inside_taskgroup(self): + """CancelScope inside TaskGroup: scope cancellation doesn't abort TG.""" + async def main(): + results = [] + async with asyncio.TaskGroup() as tg: + async with asyncio.CancelScope() as scope: + scope.cancel() + try: + await asyncio.sleep(0) + except asyncio.CancelledError: + results.append('caught') + results.append('after_scope') + self.assertEqual(results, ['caught', 'after_scope']) + + self._run(main()) + + +if __name__ == '__main__': + unittest.main() diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 8eb8e191530a33..b4c219593158b5 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -65,6 +65,7 @@ typedef struct TaskObj { PyObject *task_coro; PyObject *task_name; PyObject *task_context; + PyObject *task_cancel_scope; struct llist_node task_node; #ifdef Py_GIL_DISABLED // thread id of the thread where this task was created @@ -2348,6 +2349,7 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop, self->task_must_cancel = 0; self->task_log_destroy_pending = 1; self->task_num_cancels_requested = 0; + Py_CLEAR(self->task_cancel_scope); set_task_coro(self, coro); if (name == Py_None) { @@ -2406,6 +2408,7 @@ TaskObj_clear(PyObject *op) Py_CLEAR(task->task_context); Py_CLEAR(task->task_name); Py_CLEAR(task->task_fut_waiter); + Py_CLEAR(task->task_cancel_scope); return 0; } @@ -2418,6 +2421,7 @@ TaskObj_traverse(PyObject *op, visitproc visit, void *arg) Py_VISIT(task->task_coro); Py_VISIT(task->task_name); Py_VISIT(task->task_fut_waiter); + Py_VISIT(task->task_cancel_scope); FutureObj *fut = (FutureObj *)task; Py_VISIT(fut->fut_loop); Py_VISIT(fut->fut_callback0); @@ -2528,6 +2532,40 @@ _asyncio_Task__fut_waiter_get_impl(TaskObj *self) Py_RETURN_NONE; } +/*[clinic input] +@critical_section +@getter +_asyncio.Task._current_cancel_scope +[clinic start generated code]*/ + +static PyObject * +_asyncio_Task__current_cancel_scope_get_impl(TaskObj *self) +/*[clinic end generated code: output=15046cb0fcee9abf input=07be6e2e1497d228]*/ +{ + if (self->task_cancel_scope) { + return Py_NewRef(self->task_cancel_scope); + } + Py_RETURN_NONE; +} + +/*[clinic input] +@critical_section +@setter +_asyncio.Task._current_cancel_scope +[clinic start generated code]*/ + +static int +_asyncio_Task__current_cancel_scope_set_impl(TaskObj *self, PyObject *value) +/*[clinic end generated code: output=5765aaca52153089 input=c0c3e38a37538b5c]*/ +{ + if (value == NULL) { + Py_CLEAR(self->task_cancel_scope); + return 0; + } + Py_XSETREF(self->task_cancel_scope, Py_NewRef(value)); + return 0; +} + static PyObject * TaskObj_repr(PyObject *task) { @@ -2942,6 +2980,7 @@ static PyGetSetDef TaskType_getsetlist[] = { _ASYNCIO_TASK__MUST_CANCEL_GETSETDEF _ASYNCIO_TASK__CORO_GETSETDEF _ASYNCIO_TASK__FUT_WAITER_GETSETDEF + _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {NULL} /* Sentinel */ }; @@ -3082,6 +3121,47 @@ task_step_impl(asyncio_state *state, TaskObj *task, PyObject *exc) task->task_must_cancel = 0; } + else if (task->task_cancel_scope != NULL + && task->task_cancel_scope != Py_None) { + /* Level-triggered cancellation: re-inject CancelledError at every + step while the current CancelScope is cancelled and not shielded. */ + PyObject *cancel_called = PyObject_GetAttrString( + task->task_cancel_scope, "_cancel_called"); + if (cancel_called == NULL) { + goto fail; + } + int is_cancelled = PyObject_IsTrue(cancel_called); + Py_DECREF(cancel_called); + if (is_cancelled < 0) { + goto fail; + } + if (is_cancelled) { + PyObject *shield = PyObject_GetAttrString( + task->task_cancel_scope, "_shield"); + if (shield == NULL) { + goto fail; + } + int is_shielded = PyObject_IsTrue(shield); + Py_DECREF(shield); + if (is_shielded < 0) { + goto fail; + } + if (!is_shielded + && (!exc || !PyErr_GivenExceptionMatches( + exc, state->asyncio_CancelledError))) { + PyObject *new_exc = create_cancelled_error( + state, (FutureObj*)task); + if (!new_exc) { + goto fail; + } + if (clear_exc) { + Py_DECREF(exc); + } + exc = new_exc; + clear_exc = 1; + } + } + } Py_CLEAR(task->task_fut_waiter); diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index 66953d74213b66..de2080abb04aec 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -1083,6 +1083,48 @@ _asyncio_Task__fut_waiter_get(PyObject *self, void *Py_UNUSED(context)) return return_value; } +#if !defined(_asyncio_Task__current_cancel_scope_DOCSTR) +# define _asyncio_Task__current_cancel_scope_DOCSTR NULL +#endif +#if defined(_ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF) +# undef _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF +# define _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {"_current_cancel_scope", (getter)_asyncio_Task__current_cancel_scope_get, (setter)_asyncio_Task__current_cancel_scope_set, _asyncio_Task__current_cancel_scope_DOCSTR}, +#else +# define _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {"_current_cancel_scope", (getter)_asyncio_Task__current_cancel_scope_get, NULL, _asyncio_Task__current_cancel_scope_DOCSTR}, +#endif + +static PyObject * +_asyncio_Task__current_cancel_scope_get_impl(TaskObj *self); + +static PyObject * +_asyncio_Task__current_cancel_scope_get(PyObject *self, void *Py_UNUSED(context)) +{ + PyObject *return_value = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _asyncio_Task__current_cancel_scope_get_impl((TaskObj *)self); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + +static int +_asyncio_Task__current_cancel_scope_set_impl(TaskObj *self, PyObject *value); + +static int +_asyncio_Task__current_cancel_scope_set(PyObject *self, PyObject *value, void *Py_UNUSED(context)) +{ + int return_value; + + Py_BEGIN_CRITICAL_SECTION(self); + return_value = _asyncio_Task__current_cancel_scope_set_impl((TaskObj *)self, value); + Py_END_CRITICAL_SECTION(); + + return return_value; +} + +#define _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {"_current_cancel_scope", (getter)_asyncio_Task__current_cancel_scope_get, (setter)_asyncio_Task__current_cancel_scope_set, _asyncio_Task__current_cancel_scope_DOCSTR}, + PyDoc_STRVAR(_asyncio_Task__make_cancelled_error__doc__, "_make_cancelled_error($self, /)\n" "--\n" From 2a33256daac0d276e7fbc0b7ed83c6d485775889 Mon Sep 17 00:00:00 2001 From: kovan Date: Sat, 28 Feb 2026 10:35:59 +0100 Subject: [PATCH 2/2] Regenerate Argument Clinic output for _asynciomodule.c Co-Authored-By: Claude Opus 4.6 --- Modules/_asynciomodule.c | 4 ++-- Modules/clinic/_asynciomodule.c.h | 14 +++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index b4c219593158b5..6f977d6d1991de 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -2540,7 +2540,7 @@ _asyncio.Task._current_cancel_scope static PyObject * _asyncio_Task__current_cancel_scope_get_impl(TaskObj *self) -/*[clinic end generated code: output=15046cb0fcee9abf input=07be6e2e1497d228]*/ +/*[clinic end generated code: output=24575703fbc10903 input=14b2f1b260514c03]*/ { if (self->task_cancel_scope) { return Py_NewRef(self->task_cancel_scope); @@ -2556,7 +2556,7 @@ _asyncio.Task._current_cancel_scope static int _asyncio_Task__current_cancel_scope_set_impl(TaskObj *self, PyObject *value) -/*[clinic end generated code: output=5765aaca52153089 input=c0c3e38a37538b5c]*/ +/*[clinic end generated code: output=fe5839e86e66c3eb input=369024f3887ac2ce]*/ { if (value == NULL) { Py_CLEAR(self->task_cancel_scope); diff --git a/Modules/clinic/_asynciomodule.c.h b/Modules/clinic/_asynciomodule.c.h index de2080abb04aec..9c6d8fd6d11f78 100644 --- a/Modules/clinic/_asynciomodule.c.h +++ b/Modules/clinic/_asynciomodule.c.h @@ -1108,6 +1108,16 @@ _asyncio_Task__current_cancel_scope_get(PyObject *self, void *Py_UNUSED(context) return return_value; } +#if !defined(_asyncio_Task__current_cancel_scope_DOCSTR) +# define _asyncio_Task__current_cancel_scope_DOCSTR NULL +#endif +#if defined(_ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF) +# undef _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF +# define _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {"_current_cancel_scope", (getter)_asyncio_Task__current_cancel_scope_get, (setter)_asyncio_Task__current_cancel_scope_set, _asyncio_Task__current_cancel_scope_DOCSTR}, +#else +# define _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {"_current_cancel_scope", NULL, (setter)_asyncio_Task__current_cancel_scope_set, NULL}, +#endif + static int _asyncio_Task__current_cancel_scope_set_impl(TaskObj *self, PyObject *value); @@ -1123,8 +1133,6 @@ _asyncio_Task__current_cancel_scope_set(PyObject *self, PyObject *value, void *P return return_value; } -#define _ASYNCIO_TASK__CURRENT_CANCEL_SCOPE_GETSETDEF {"_current_cancel_scope", (getter)_asyncio_Task__current_cancel_scope_get, (setter)_asyncio_Task__current_cancel_scope_set, _asyncio_Task__current_cancel_scope_DOCSTR}, - PyDoc_STRVAR(_asyncio_Task__make_cancelled_error__doc__, "_make_cancelled_error($self, /)\n" "--\n" @@ -2274,4 +2282,4 @@ _asyncio_future_discard_from_awaited_by(PyObject *module, PyObject *const *args, exit: return return_value; } -/*[clinic end generated code: output=b69948ed810591d9 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=46c804efacf0be43 input=a9049054013a1b77]*/