From 0da18e8e801fe76226aaffff9e349d007e8d58f2 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 13 Jan 2025 07:31:18 +0000 Subject: [PATCH 01/12] gh-128308: pass **kwargs to asyncio task_factory --- Doc/library/asyncio-eventloop.rst | 4 ++-- Lib/asyncio/base_events.py | 20 +++++++------------ Lib/asyncio/taskgroups.py | 8 +++----- Lib/asyncio/tasks.py | 11 ++-------- .../test_asyncio/test_eager_task_factory.py | 12 +++++++++++ Lib/test/test_asyncio/test_taskgroups.py | 12 +++++++++++ 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 072ab206f25e4f..0505865aea8ff1 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -392,9 +392,9 @@ Creating Futures and Tasks If *factory* is ``None`` the default task factory will be set. Otherwise, *factory* must be a *callable* with the signature matching - ``(loop, coro, context=None)``, where *loop* is a reference to the active + ``(loop, coro, **kwargs)``, where *loop* is a reference to the active event loop, and *coro* is a coroutine object. The callable - must return a :class:`asyncio.Future`-compatible object. + must pass on all *kwargs*, and return a :class:`asyncio.Task`-compatible object. .. method:: loop.get_task_factory() diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 6e6e5aaac15caf..82bc8d02f6b87e 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -458,25 +458,18 @@ def create_future(self): """Create a Future object attached to the loop.""" return futures.Future(loop=self) - def create_task(self, coro, *, name=None, context=None): + def create_task(self, coro, **kwargs): """Schedule a coroutine object. Return a task object. """ self._check_closed() if self._task_factory is None: - task = tasks.Task(coro, loop=self, name=name, context=context) + task = tasks.Task(coro, loop=self, **kwargs) if task._source_traceback: del task._source_traceback[-1] else: - if context is None: - # Use legacy API if context is not needed - task = self._task_factory(self, coro) - else: - task = self._task_factory(self, coro, context=context) - - task.set_name(name) - + task = self._task_factory(self, coro, **kwargs) try: return task finally: @@ -490,9 +483,10 @@ def set_task_factory(self, factory): If factory is None the default task factory will be set. If factory is a callable, it should have a signature matching - '(loop, coro)', where 'loop' will be a reference to the active - event loop, 'coro' will be a coroutine object. The callable - must return a Future. + '(loop, coro, **kwargs)', where 'loop' will be a reference to the active + event loop, 'coro' will be a coroutine object, and **kwargs will be + arbitrary keyword arguments that should be passed on to Task. + The callable must return a Task. """ if factory is not None and not callable(factory): raise TypeError('task factory must be a callable or None') diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index 8af199d6dcc41a..b0388097a5622b 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -178,7 +178,7 @@ async def _aexit(self, et, exc): exc = None - def create_task(self, coro, *, name=None, context=None): + def create_task(self, coro, **kwargs): """Create a new task in this group and return it. Similar to `asyncio.create_task`. @@ -192,10 +192,8 @@ def create_task(self, coro, *, name=None, context=None): if self._aborting: coro.close() raise RuntimeError(f"TaskGroup {self!r} is shutting down") - if context is None: - task = self._loop.create_task(coro, name=name) - else: - task = self._loop.create_task(coro, name=name, context=context) + + task = self._loop.create_task(coro, **kwargs) # optimization: Immediately call the done callback if the task is # already done (e.g. if the coro was able to complete eagerly), diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 2112dd4b99d17f..498c92fca53ac3 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -384,19 +384,12 @@ def __wakeup(self, future): Task = _CTask = _asyncio.Task -def create_task(coro, *, name=None, context=None): +def create_task(coro, **kwargs): """Schedule the execution of a coroutine object in a spawn task. Return a Task object. """ - loop = events.get_running_loop() - if context is None: - # Use legacy API if context is not needed - task = loop.create_task(coro, name=name) - else: - task = loop.create_task(coro, name=name, context=context) - - return task + return events.get_running_loop().create_task(coro, **kwargs) # wait() and as_completed() similar to those in PEP 3148. diff --git a/Lib/test/test_asyncio/test_eager_task_factory.py b/Lib/test/test_asyncio/test_eager_task_factory.py index dcf9ff716ad399..10450c11b68279 100644 --- a/Lib/test/test_asyncio/test_eager_task_factory.py +++ b/Lib/test/test_asyncio/test_eager_task_factory.py @@ -302,6 +302,18 @@ async def run(): self.run_coro(run()) + def test_name(self): + name = None + async def coro(): + nonlocal name + name = asyncio.current_task().get_name() + + async def main(): + task = self.loop.create_task(coro(), name="test name") + self.assertEqual(name, "test name") + await task + + self.run_coro(coro()) class AsyncTaskCounter: def __init__(self, loop, *, task_class, eager): diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 870fa8dbbf2714..7859b33532fa27 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1040,6 +1040,18 @@ class MyKeyboardInterrupt(KeyboardInterrupt): self.assertIsNotNone(exc) self.assertListEqual(gc.get_referrers(exc), no_other_refs()) + 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 ec197f855207a8c43e43651fa0ede7f98714ac1f Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 07:54:38 +0000 Subject: [PATCH 02/12] =?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-01-13-07-54-32.gh-issue-128308.kYSDRF.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-01-13-07-54-32.gh-issue-128308.kYSDRF.rst diff --git a/Misc/NEWS.d/next/Library/2025-01-13-07-54-32.gh-issue-128308.kYSDRF.rst b/Misc/NEWS.d/next/Library/2025-01-13-07-54-32.gh-issue-128308.kYSDRF.rst new file mode 100644 index 00000000000000..efa613876a35fd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-01-13-07-54-32.gh-issue-128308.kYSDRF.rst @@ -0,0 +1 @@ +Support the *name* keyword argument for eager tasks in :func:`asyncio.loop.create_task`, :func:`asyncio.create_task` and :func:`asyncio.TaskGroup.create_task`, by passing on all *kwargs* to the task factory set by :func:`asyncio.loop.set_task_factory`. From 5c48da0b4e93c44739d296084fad2abbf00fe5dc Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 13 Jan 2025 08:10:55 +0000 Subject: [PATCH 03/12] if name is not supported by task factory and no other kwargs are passed try again without name --- Lib/asyncio/base_events.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 82bc8d02f6b87e..b567dff62a66e6 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -469,7 +469,18 @@ def create_task(self, coro, **kwargs): if task._source_traceback: del task._source_traceback[-1] else: - task = self._task_factory(self, coro, **kwargs) + task = None + name = None + try: + task = self._task_factory(self, coro, **kwargs) + except TypeError as e: + name = kwargs.pop("name", None) + if kwargs: + raise + + if task is None: + task = self._task_factory(self, coro, **kwargs) + task.set_name(name) try: return task finally: From 6afb25f425c7e4a4e50c468d20a0f810c2f02dbe Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 17 Jan 2025 19:15:52 +0000 Subject: [PATCH 04/12] Discard changes to Lib/asyncio/taskgroups.py --- Lib/asyncio/taskgroups.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/taskgroups.py b/Lib/asyncio/taskgroups.py index b0388097a5622b..8af199d6dcc41a 100644 --- a/Lib/asyncio/taskgroups.py +++ b/Lib/asyncio/taskgroups.py @@ -178,7 +178,7 @@ async def _aexit(self, et, exc): exc = None - def create_task(self, coro, **kwargs): + def create_task(self, coro, *, name=None, context=None): """Create a new task in this group and return it. Similar to `asyncio.create_task`. @@ -192,8 +192,10 @@ def create_task(self, coro, **kwargs): if self._aborting: coro.close() raise RuntimeError(f"TaskGroup {self!r} is shutting down") - - task = self._loop.create_task(coro, **kwargs) + if context is None: + task = self._loop.create_task(coro, name=name) + else: + task = self._loop.create_task(coro, name=name, context=context) # optimization: Immediately call the done callback if the task is # already done (e.g. if the coro was able to complete eagerly), From 880879475592a0287b70bf5cfd3d6eeda8193126 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Fri, 17 Jan 2025 19:16:05 +0000 Subject: [PATCH 05/12] Discard changes to Lib/asyncio/tasks.py --- Lib/asyncio/tasks.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 498c92fca53ac3..2112dd4b99d17f 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -384,12 +384,19 @@ def __wakeup(self, future): Task = _CTask = _asyncio.Task -def create_task(coro, **kwargs): +def create_task(coro, *, name=None, context=None): """Schedule the execution of a coroutine object in a spawn task. Return a Task object. """ - return events.get_running_loop().create_task(coro, **kwargs) + loop = events.get_running_loop() + if context is None: + # Use legacy API if context is not needed + task = loop.create_task(coro, name=name) + else: + task = loop.create_task(coro, name=name, context=context) + + return task # wait() and as_completed() similar to those in PEP 3148. From 9c91cfa53cace7190d2b30c60e7b10bac56fcf0c Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 19 Jan 2025 11:22:34 +0000 Subject: [PATCH 06/12] simplify name handling --- Lib/asyncio/base_events.py | 21 +++++---------------- Lib/test/test_asyncio/test_base_events.py | 21 ++++++++++++--------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index b567dff62a66e6..7a2863ee6d00f4 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -464,23 +464,12 @@ def create_task(self, coro, **kwargs): Return a task object. """ self._check_closed() - if self._task_factory is None: - task = tasks.Task(coro, loop=self, **kwargs) - if task._source_traceback: - del task._source_traceback[-1] - else: - task = None - name = None - try: - task = self._task_factory(self, coro, **kwargs) - except TypeError as e: - name = kwargs.pop("name", None) - if kwargs: - raise + if self._task_factory is not None: + return self._task_factory(self, coro, **kwargs) - if task is None: - task = self._task_factory(self, coro, **kwargs) - task.set_name(name) + task = tasks.Task(coro, loop=self, **kwargs) + if task._source_traceback: + del task._source_traceback[-1] try: return task finally: diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 1e063c1352ecb9..aca1266005fcc4 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1,6 +1,7 @@ """Tests for base_events.py""" import concurrent.futures +import contextlib import errno import math import platform @@ -834,19 +835,21 @@ async def test(): def test_create_named_task_with_custom_factory(self): def task_factory(loop, coro): - return asyncio.Task(coro, loop=loop) + assert False async def test(): pass - loop = asyncio.new_event_loop() - loop.set_task_factory(task_factory) - task = loop.create_task(test(), name='test_task') - try: - self.assertEqual(task.get_name(), 'test_task') - finally: - loop.run_until_complete(task) - loop.close() + with ( + contextlib.closing(asyncio.EventLoop()) as loop, + contextlib.closing(test()) as coro, + ): + loop.set_task_factory(task_factory) + with self.assertRaisesRegex( + TypeError, + r"got an unexpected keyword argument 'name'" + ): + loop.create_task(coro, name='test_task') def test_run_forever_keyboard_interrupt(self): # Python issue #22601: ensure that the temporary task created by From c147534f318b8e361c3434aff6c011480b8174cc Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Sun, 19 Jan 2025 14:17:21 +0000 Subject: [PATCH 07/12] update AbstractEventLoop to take **kwargs also --- Lib/asyncio/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index 2ee9870e80f20b..71a3cf1844d61c 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -329,7 +329,7 @@ def create_future(self): # Method scheduling a coroutine object: create a task. - def create_task(self, coro, *, name=None, context=None): + def create_task(self, coro, *, **kwargs): raise NotImplementedError # Methods for interacting with threads. From 856a5eee988b65a52a4ebdffd71a1d93e24bab77 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 20 Jan 2025 10:33:22 +0000 Subject: [PATCH 08/12] Update Lib/asyncio/events.py --- Lib/asyncio/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index 71a3cf1844d61c..2e45b4fe6fa2dd 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -329,7 +329,7 @@ def create_future(self): # Method scheduling a coroutine object: create a task. - def create_task(self, coro, *, **kwargs): + def create_task(self, coro, **kwargs): raise NotImplementedError # Methods for interacting with threads. From 539d2e9b073c7cfce83b8b3d7cf593446811ae44 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 20 Jan 2025 11:02:37 +0000 Subject: [PATCH 09/12] fix test_asyncio.test_free_threading --- Lib/test/test_asyncio/test_free_threading.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index 8f4bba5f3b97d9..a1d81767f64386 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -112,8 +112,8 @@ class TestPyFreeThreading(TestFreeThreading, TestCase): all_tasks = staticmethod(asyncio.tasks._py_all_tasks) current_task = staticmethod(asyncio.tasks._py_current_task) - def factory(self, loop, coro, context=None): - return asyncio.tasks._PyTask(coro, loop=loop, context=context) + def factory(self, loop, coro, **kwargs): + return asyncio.tasks._PyTask(coro, loop=loop, **kwargs) @unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio") @@ -121,16 +121,16 @@ class TestCFreeThreading(TestFreeThreading, TestCase): all_tasks = staticmethod(getattr(asyncio.tasks, "_c_all_tasks", None)) current_task = staticmethod(getattr(asyncio.tasks, "_c_current_task", None)) - def factory(self, loop, coro, context=None): - return asyncio.tasks._CTask(coro, loop=loop, context=context) + def factory(self, loop, coro, **kwargs): + return asyncio.tasks._CTask(coro, loop=loop, **kwargs) class TestEagerPyFreeThreading(TestPyFreeThreading): - def factory(self, loop, coro, context=None): - return asyncio.tasks._PyTask(coro, loop=loop, context=context, eager_start=True) + def factory(self, loop, coro, eager_start=True, **kwargs): + return asyncio.tasks._PyTask(coro, loop=loop, **kwargs, eager_start=eager_start) @unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio") class TestEagerCFreeThreading(TestCFreeThreading, TestCase): - def factory(self, loop, coro, context=None): - return asyncio.tasks._CTask(coro, loop=loop, context=context, eager_start=True) + def factory(self, loop, coro, *, eager_start=True, **kwargs): + return asyncio.tasks._CTask(coro, loop=loop, **kwargs, eager_start=eager_start) From aa3fda59afbe75221c4b784aa42bfe5c98f4fea1 Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 20 Jan 2025 12:53:11 +0000 Subject: [PATCH 10/12] Update Lib/test/test_asyncio/test_free_threading.py Co-authored-by: Kumar Aditya --- Lib/test/test_asyncio/test_free_threading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_free_threading.py b/Lib/test/test_asyncio/test_free_threading.py index a1d81767f64386..05106a2c2fe3f6 100644 --- a/Lib/test/test_asyncio/test_free_threading.py +++ b/Lib/test/test_asyncio/test_free_threading.py @@ -132,5 +132,5 @@ def factory(self, loop, coro, eager_start=True, **kwargs): @unittest.skipUnless(hasattr(asyncio.tasks, "_c_all_tasks"), "requires _asyncio") class TestEagerCFreeThreading(TestCFreeThreading, TestCase): - def factory(self, loop, coro, *, eager_start=True, **kwargs): + def factory(self, loop, coro, eager_start=True, **kwargs): return asyncio.tasks._CTask(coro, loop=loop, **kwargs, eager_start=eager_start) From bccdb0542be69dcedba94e88b715dd10612cb7ad Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 20 Jan 2025 13:24:53 +0000 Subject: [PATCH 11/12] Discard changes to Lib/test/test_asyncio/test_base_events.py --- Lib/test/test_asyncio/test_base_events.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index aca1266005fcc4..1e063c1352ecb9 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1,7 +1,6 @@ """Tests for base_events.py""" import concurrent.futures -import contextlib import errno import math import platform @@ -835,21 +834,19 @@ async def test(): def test_create_named_task_with_custom_factory(self): def task_factory(loop, coro): - assert False + return asyncio.Task(coro, loop=loop) async def test(): pass - with ( - contextlib.closing(asyncio.EventLoop()) as loop, - contextlib.closing(test()) as coro, - ): - loop.set_task_factory(task_factory) - with self.assertRaisesRegex( - TypeError, - r"got an unexpected keyword argument 'name'" - ): - loop.create_task(coro, name='test_task') + loop = asyncio.new_event_loop() + loop.set_task_factory(task_factory) + task = loop.create_task(test(), name='test_task') + try: + self.assertEqual(task.get_name(), 'test_task') + finally: + loop.run_until_complete(task) + loop.close() def test_run_forever_keyboard_interrupt(self): # Python issue #22601: ensure that the temporary task created by From c62d059510db3e47563e2f9b4ddcd2c10287297b Mon Sep 17 00:00:00 2001 From: Thomas Grainger Date: Mon, 20 Jan 2025 13:25:50 +0000 Subject: [PATCH 12/12] fix test_create_named_task_with_custom_factory --- Lib/test/test_asyncio/test_base_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 1e063c1352ecb9..cbf7996f1e3770 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -833,8 +833,8 @@ async def test(): loop.close() def test_create_named_task_with_custom_factory(self): - def task_factory(loop, coro): - return asyncio.Task(coro, loop=loop) + def task_factory(loop, coro, **kwargs): + return asyncio.Task(coro, loop=loop, **kwargs) async def test(): pass