From 5c3c953b101555325f925ce1b5974d5639e8a5cd Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 12 May 2025 20:07:47 +0100 Subject: [PATCH 01/14] gh-133745: Fix asyncio task factory name/context kwarg breaks --- Lib/asyncio/base_events.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index d2cedb68985853..fbd54e50c66e5c 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -458,16 +458,26 @@ def create_future(self): """Create a Future object attached to the loop.""" return futures.Future(loop=self) - def create_task(self, coro, **kwargs): + def create_task(self, coro, *, name=None, context=None, **kwargs): """Schedule a coroutine object. Return a task object. """ self._check_closed() if self._task_factory is not None: - return self._task_factory(self, coro, **kwargs) + if context is not None: + kwargs["context"] = context - task = tasks.Task(coro, loop=self, **kwargs) + task = self._task_factory(self, coro, **kwargs) + task.set_name(name) + try: + # gh-128552: prevent a refcycle of + # task.exception().__traceback__->BaseEventLoop.create_task->task + return task + finally: + del task + + task = tasks.Task(coro, loop=self, name=name, context=context, **kwargs) if task._source_traceback: del task._source_traceback[-1] try: From 6247a57ef04a52966b4cd8327e4e8deb500d77c0 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 12 May 2025 20:25:50 +0100 Subject: [PATCH 02/14] expect failure on named eager tasks --- Lib/test/test_asyncio/test_taskgroups.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 9f2211e3232e54..670331b00f8517 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1081,6 +1081,7 @@ async def throw_error(): # cancellation happens here and error is more understandable await asyncio.sleep(0) + @unittest.expectedFailure async def test_name(self): name = None From ddfbeb2aed73a30c121ff15a84dceec47736ca19 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 13 May 2025 08:42:56 +0100 Subject: [PATCH 03/14] remove failing test --- Lib/test/test_asyncio/test_taskgroups.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 670331b00f8517..ad61cb46c7c07c 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1081,19 +1081,6 @@ async def throw_error(): # cancellation happens here and error is more understandable await asyncio.sleep(0) - @unittest.expectedFailure - async def test_name(self): - name = None - - async def asyncfn(): - nonlocal name - name = asyncio.current_task().get_name() - - async with asyncio.TaskGroup() as tg: - tg.create_task(asyncfn(), name="example name") - - self.assertEqual(name, "example name") - class TestTaskGroup(BaseTestTaskGroup, unittest.IsolatedAsyncioTestCase): loop_factory = asyncio.EventLoop From 9dcf0e8c7c9098bd989570f25d37f1590eff6742 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 13 May 2025 08:45:51 +0100 Subject: [PATCH 04/14] use only one try/finally/del block --- Lib/asyncio/base_events.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index fbd54e50c66e5c..c41b1d0225e4c2 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -464,22 +464,19 @@ def create_task(self, coro, *, name=None, context=None, **kwargs): Return a task object. """ self._check_closed() + if self._task_factory is not None: if context is not None: kwargs["context"] = context task = self._task_factory(self, coro, **kwargs) task.set_name(name) - try: - # gh-128552: prevent a refcycle of - # task.exception().__traceback__->BaseEventLoop.create_task->task - return task - finally: - del task - task = tasks.Task(coro, loop=self, name=name, context=context, **kwargs) - if task._source_traceback: - del task._source_traceback[-1] + else: + task = tasks.Task(coro, loop=self, name=name, context=context, **kwargs) + if task._source_traceback: + del task._source_traceback[-1] + try: return task finally: From d607230ee3dd56f0a8b010e0222298c95eba8788 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Tue, 13 May 2025 08:48:06 +0100 Subject: [PATCH 05/14] remove extra whitespace --- Lib/asyncio/base_events.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index c41b1d0225e4c2..07ec6de6414ec2 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -464,7 +464,6 @@ def create_task(self, coro, *, name=None, context=None, **kwargs): Return a task object. """ self._check_closed() - if self._task_factory is not None: if context is not None: kwargs["context"] = context From ac5759aecae70e603d6be4475d15e701bcc714c8 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 08:13:17 +0000 Subject: [PATCH 06/14] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst b/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst new file mode 100644 index 00000000000000..b1e819191ef010 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst @@ -0,0 +1 @@ +In 3.13.3 we accidentally changed the signature of create_task() and how it calls a custom task factory in a backwards incompatible way. Since some 3rd party libraries have already made changes to work around the issue that might break if we simply reverted the changes, we're instead changing things to be backwards compatible with 3.13.2 while still supporting those workarounds. In particular, the special-casing of `name` and `context` is back (until 3.14) and consequently eager tasks may still find that their name hasn't been set before they execute their first line of code. From 363d33653abb3cdd6f2cb4b88da45b3b8b985120 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Wed, 14 May 2025 09:15:28 +0100 Subject: [PATCH 07/14] copy editing for news --- .../next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst b/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst index b1e819191ef010..78cc3d9cfa0ee7 100644 --- a/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst +++ b/Misc/NEWS.d/next/Library/2025-05-14-08-13-08.gh-issue-133745.rjgJkH.rst @@ -1 +1 @@ -In 3.13.3 we accidentally changed the signature of create_task() and how it calls a custom task factory in a backwards incompatible way. Since some 3rd party libraries have already made changes to work around the issue that might break if we simply reverted the changes, we're instead changing things to be backwards compatible with 3.13.2 while still supporting those workarounds. In particular, the special-casing of `name` and `context` is back (until 3.14) and consequently eager tasks may still find that their name hasn't been set before they execute their first line of code. +In 3.13.3 we accidentally changed the signature of the asyncio ``create_task()`` family of methods and how it calls a custom task factory in a backwards incompatible way. Since some 3rd party libraries have already made changes to work around the issue that might break if we simply reverted the changes, we're instead changing things to be backwards compatible with 3.13.2 while still supporting those workarounds for 3.13.3. In particular, the special-casing of ``name`` and ``context`` is back (until 3.14) and consequently eager tasks may still find that their name hasn't been set before they execute their first yielding await. From 2ba0d191c9db1e340b145d13ba9f7820554e2c1f Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 17 May 2025 09:32:33 +0100 Subject: [PATCH 08/14] update docs about create_task --- Doc/library/asyncio-eventloop.rst | 24 +++++++++++++++++++++++- Doc/library/asyncio-task.rst | 31 ++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 92b98e2b2c862c..4cc184c71bc902 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -355,7 +355,7 @@ Creating Futures and Tasks .. versionadded:: 3.5.2 -.. method:: loop.create_task(coro, *, name=None, context=None) +.. method:: loop.create_task(coro, *, name=None, context=None, eager_start=None, **kwargs) Schedule the execution of :ref:`coroutine ` *coro*. Return a :class:`Task` object. @@ -371,12 +371,27 @@ Creating Futures and Tasks custom :class:`contextvars.Context` for the *coro* to run in. The current context copy is created when no *context* is provided. + An optional keyword-only *eager_start* argument allows eagerly starting + the execution of the :class:`asyncio.Task` at task creation time. + If set to ``True`` and the event loop is running, the task will start + executing the coroutine immediately, until the first time the coroutine + blocks. If the coroutine returns or raises without blocking, the task + will be finished eagerly and will skip scheduling to the event loop. + .. versionchanged:: 3.8 Added the *name* parameter. .. versionchanged:: 3.11 Added the *context* parameter. + .. versionchanged:: 3.13.3 + Added ``kwargs`` which always passes on ``kwargs`` such as the *eager_start* + parameter and *name* parameter. + + .. versionchanged:: 3.13.4 + Rolled back the change that passes on *name* and *context* (if it is None), + passing on new keword arguments such as *eager_start* is still supported. + .. method:: loop.set_task_factory(factory) Set a task factory that will be used by @@ -388,6 +403,13 @@ Creating Futures and Tasks event loop, and *coro* is a coroutine object. The callable must pass on all *kwargs*, and return a :class:`asyncio.Task`-compatible object. + .. versionchanged:: 3.13.3 + Required that all *kwargs* are passed on to :class:`asyncio.Task`. + + .. versionchanged:: 3.13.4 + *name* is no longer passed to task factories. *context* is no longer passed + to task factories if it is ``None``. + .. method:: loop.get_task_factory() Return a task factory or ``None`` if the default one is in use. diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 6c83ab94af3955..e554c381b7bdb5 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -238,11 +238,17 @@ Creating Tasks ----------------------------------------------- -.. function:: create_task(coro, *, name=None, context=None) +.. function:: create_task(coro, *, name=None, context=None, eager_start=None, **kwargs) Wrap the *coro* :ref:`coroutine ` into a :class:`Task` and schedule its execution. Return the Task object. + The arguments shown above are merely the most common ones, described below + The full function signature is largely the same as that of the + :class:`Task` constructor (or factory) - all of the keyword arguments to + this function are passed through to that interface, except *name*, + or *context* if it is ``None``. + If *name* is not ``None``, it is set as the name of the task using :meth:`Task.set_name`. @@ -250,6 +256,13 @@ Creating Tasks custom :class:`contextvars.Context` for the *coro* to run in. The current context copy is created when no *context* is provided. + An optional keyword-only *eager_start* argument allows eagerly starting + the execution of the :class:`asyncio.Task` at task creation time. + If set to ``True`` and the event loop is running, the task will start + executing the coroutine immediately, until the first time the coroutine + blocks. If the coroutine returns or raises without blocking, the task + will be finished eagerly and will skip scheduling to the event loop. + The task is executed in the loop returned by :func:`get_running_loop`, :exc:`RuntimeError` is raised if there is no running loop in current thread. @@ -290,6 +303,14 @@ Creating Tasks .. versionchanged:: 3.11 Added the *context* parameter. + .. versionchanged:: 3.13.3 + Added ``kwargs`` which always passes on ``kwargs`` such as the *eager_start* + parameter and *name* parameter. + + .. versionchanged:: 3.13.4 + Rolled back the change that passes on *name* and *context* (if it is None), + passing on new keword arguments such as *eager_start* is still supported. + Task Cancellation ================= @@ -330,10 +351,10 @@ and reliable way to wait for all tasks in the group to finish. .. versionadded:: 3.11 - .. method:: create_task(coro, *, name=None, context=None) + .. method:: create_task(coro, *, name=None, context=None, eager_start=None, **kwargs) Create a task in this task group. - The signature matches that of :func:`asyncio.create_task`. + The signature matches that of :meth:`loop.create_task`. If the task group is inactive (e.g. not yet entered, already finished, or in the process of shutting down), we will close the given ``coro``. @@ -342,6 +363,10 @@ and reliable way to wait for all tasks in the group to finish. Close the given coroutine if the task group is not active. + .. versionchanged:: 3.13.3 + + Passes on all keyword arguments to :meth:`loop.create_task` + Example:: async def main(): From 2e7556d018622c205c8bced28cefbe7afbd8d61b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 17 May 2025 09:35:26 +0100 Subject: [PATCH 09/14] Discard changes to Doc/library/asyncio-task.rst --- Doc/library/asyncio-task.rst | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index e554c381b7bdb5..6c83ab94af3955 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -238,17 +238,11 @@ Creating Tasks ----------------------------------------------- -.. function:: create_task(coro, *, name=None, context=None, eager_start=None, **kwargs) +.. function:: create_task(coro, *, name=None, context=None) Wrap the *coro* :ref:`coroutine ` into a :class:`Task` and schedule its execution. Return the Task object. - The arguments shown above are merely the most common ones, described below - The full function signature is largely the same as that of the - :class:`Task` constructor (or factory) - all of the keyword arguments to - this function are passed through to that interface, except *name*, - or *context* if it is ``None``. - If *name* is not ``None``, it is set as the name of the task using :meth:`Task.set_name`. @@ -256,13 +250,6 @@ Creating Tasks custom :class:`contextvars.Context` for the *coro* to run in. The current context copy is created when no *context* is provided. - An optional keyword-only *eager_start* argument allows eagerly starting - the execution of the :class:`asyncio.Task` at task creation time. - If set to ``True`` and the event loop is running, the task will start - executing the coroutine immediately, until the first time the coroutine - blocks. If the coroutine returns or raises without blocking, the task - will be finished eagerly and will skip scheduling to the event loop. - The task is executed in the loop returned by :func:`get_running_loop`, :exc:`RuntimeError` is raised if there is no running loop in current thread. @@ -303,14 +290,6 @@ Creating Tasks .. versionchanged:: 3.11 Added the *context* parameter. - .. versionchanged:: 3.13.3 - Added ``kwargs`` which always passes on ``kwargs`` such as the *eager_start* - parameter and *name* parameter. - - .. versionchanged:: 3.13.4 - Rolled back the change that passes on *name* and *context* (if it is None), - passing on new keword arguments such as *eager_start* is still supported. - Task Cancellation ================= @@ -351,10 +330,10 @@ and reliable way to wait for all tasks in the group to finish. .. versionadded:: 3.11 - .. method:: create_task(coro, *, name=None, context=None, eager_start=None, **kwargs) + .. method:: create_task(coro, *, name=None, context=None) Create a task in this task group. - The signature matches that of :meth:`loop.create_task`. + The signature matches that of :func:`asyncio.create_task`. If the task group is inactive (e.g. not yet entered, already finished, or in the process of shutting down), we will close the given ``coro``. @@ -363,10 +342,6 @@ and reliable way to wait for all tasks in the group to finish. Close the given coroutine if the task group is not active. - .. versionchanged:: 3.13.3 - - Passes on all keyword arguments to :meth:`loop.create_task` - Example:: async def main(): From d3021aee2b5d894bb70fa8b7f44b53d465c377f5 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 17 May 2025 09:37:26 +0100 Subject: [PATCH 10/14] add pass through docs --- Doc/library/asyncio-eventloop.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 4cc184c71bc902..cb6edcfc9fc62f 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -364,6 +364,12 @@ Creating Futures and Tasks for interoperability. In this case, the result type is a subclass of :class:`Task`. + The arguments shown above are merely the most common ones, described below + The full function signature is largely the same as that of the + :class:`Task` constructor (or factory) - all of the keyword arguments to + this function are passed through to that interface, except *name*, + or *context* if it is ``None``. + If the *name* argument is provided and not ``None``, it is set as the name of the task using :meth:`Task.set_name`. From b26c1ff454eaf6240bcc2fd902507679de832e60 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 17 May 2025 09:38:06 +0100 Subject: [PATCH 11/14] Update Doc/library/asyncio-eventloop.rst --- Doc/library/asyncio-eventloop.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index cb6edcfc9fc62f..cac54b950cb031 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -364,7 +364,6 @@ Creating Futures and Tasks for interoperability. In this case, the result type is a subclass of :class:`Task`. - The arguments shown above are merely the most common ones, described below The full function signature is largely the same as that of the :class:`Task` constructor (or factory) - all of the keyword arguments to this function are passed through to that interface, except *name*, From 8088dac879fe7e7dc711e8d8ccf03198b39cf0a1 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sat, 17 May 2025 10:10:14 +0100 Subject: [PATCH 12/14] Update Doc/library/asyncio-eventloop.rst Co-authored-by: Kumar Aditya --- Doc/library/asyncio-eventloop.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index cac54b950cb031..cb40d92297b677 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -395,7 +395,7 @@ Creating Futures and Tasks .. versionchanged:: 3.13.4 Rolled back the change that passes on *name* and *context* (if it is None), - passing on new keword arguments such as *eager_start* is still supported. + passing on new keyword arguments such as *eager_start* is still supported. .. method:: loop.set_task_factory(factory) From 5c26ed72f2ba435261042b0a6c5dbc816f2594da Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 18 May 2025 09:02:43 +0100 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Guido van Rossum --- Doc/library/asyncio-eventloop.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index cb40d92297b677..8f9200d2e8d61e 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -390,12 +390,11 @@ Creating Futures and Tasks Added the *context* parameter. .. versionchanged:: 3.13.3 - Added ``kwargs`` which always passes on ``kwargs`` such as the *eager_start* - parameter and *name* parameter. + Added ``kwargs`` which passes on arbitrary extra parameters, including ``name`` and ``context``. .. versionchanged:: 3.13.4 Rolled back the change that passes on *name* and *context* (if it is None), - passing on new keyword arguments such as *eager_start* is still supported. + while still passing on other arbitrary keyword arguments (to avoid breaking backwards compatibility with 3.13.3). .. method:: loop.set_task_factory(factory) @@ -409,11 +408,11 @@ Creating Futures and Tasks must pass on all *kwargs*, and return a :class:`asyncio.Task`-compatible object. .. versionchanged:: 3.13.3 - Required that all *kwargs* are passed on to :class:`asyncio.Task`. + Required that all *kwargs* are passed on to :class:`asyncio.Task`. .. versionchanged:: 3.13.4 - *name* is no longer passed to task factories. *context* is no longer passed - to task factories if it is ``None``. + *name* is no longer passed to task factories. *context* is no longer passed + to task factories if it is ``None``. .. method:: loop.get_task_factory() From d600dd7ee9379c19edbb15ac2daa327b7f8cef22 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 18 May 2025 09:16:17 +0100 Subject: [PATCH 14/14] Apply suggestions from code review --- Doc/library/asyncio-eventloop.rst | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 8f9200d2e8d61e..20b1ccd105109e 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -355,7 +355,7 @@ Creating Futures and Tasks .. versionadded:: 3.5.2 -.. method:: loop.create_task(coro, *, name=None, context=None, eager_start=None, **kwargs) +.. method:: loop.create_task(coro, *, name=None, context=None, **kwargs) Schedule the execution of :ref:`coroutine ` *coro*. Return a :class:`Task` object. @@ -376,13 +376,6 @@ Creating Futures and Tasks custom :class:`contextvars.Context` for the *coro* to run in. The current context copy is created when no *context* is provided. - An optional keyword-only *eager_start* argument allows eagerly starting - the execution of the :class:`asyncio.Task` at task creation time. - If set to ``True`` and the event loop is running, the task will start - executing the coroutine immediately, until the first time the coroutine - blocks. If the coroutine returns or raises without blocking, the task - will be finished eagerly and will skip scheduling to the event loop. - .. versionchanged:: 3.8 Added the *name* parameter.