diff --git a/Doc/library/asyncio-runner.rst b/Doc/library/asyncio-runner.rst index cd125b30703c79..cd42705bad83a8 100644 --- a/Doc/library/asyncio-runner.rst +++ b/Doc/library/asyncio-runner.rst @@ -22,7 +22,7 @@ to simplify async code usage for common wide-spread scenarios. Running an asyncio Program ========================== -.. function:: run(coro, *, debug=None, loop_factory=None) +.. function:: run(coro, *, debug=None, loop_factory=None, eager_tasks=False) Execute *coro* in an asyncio event loop and return the result. @@ -45,10 +45,18 @@ Running an asyncio Program and should ideally only be called once. It is recommended to use *loop_factory* to configure the event loop. + If *eager_tasks* is ``True``, the created loop is configured to use + :ref:`eager-task-factory` by default. + The executor is given a timeout duration of 5 minutes to shutdown. If the executor hasn't finished within that duration, a warning is emitted and the executor is closed. + .. note:: + + Users concerned about performance are encouraged to experiment with + ``eager_tasks=True`` in their code. + Example:: async def main(): @@ -74,11 +82,15 @@ Running an asyncio Program *coro* can be any awaitable object. + .. versionchanged:: 3.16 + + Added *eager_tasks* parameter. + Runner context manager ====================== -.. class:: Runner(*, debug=None, loop_factory=None) +.. class:: Runner(*, debug=None, loop_factory=None, eager_tasks=False) A context manager that simplifies *multiple* async function calls in the same context. @@ -95,6 +107,9 @@ Runner context manager current one. By default :func:`asyncio.new_event_loop` is used and set as current event loop with :func:`asyncio.set_event_loop` if *loop_factory* is ``None``. + If *eager_tasks* is ``True``, the created loop is configured to use + :ref:`eager-task-factory` by default. + Basically, :func:`asyncio.run` example can be rewritten with the runner usage:: async def main(): @@ -104,8 +119,17 @@ Runner context manager with asyncio.Runner() as runner: runner.run(main()) + .. note:: + + Users concerned about performance are encouraged to experiment with + ``eager_tasks=True`` in their code. + .. versionadded:: 3.11 + .. versionchanged:: 3.16 + + Added *eager_tasks* parameter. + .. method:: run(coro, *, context=None) Execute *coro* in the embedded event loop. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index e342aa71c7924e..ce001d07731bcc 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -144,6 +144,10 @@ asyncio socket file created for *path*. (Contributed by Sam Bull in :gh:`94984`.) +* Add *eager_tasks* parameter to :func:`asyncio.run` function and + :class:`asyncio.Runner` class constructor. + (Contributed by Andrew Svetlov in :gh:`128289`.) + bdb --- diff --git a/Lib/asyncio/runners.py b/Lib/asyncio/runners.py index 774a317564df22..deafd9a7006f6b 100644 --- a/Lib/asyncio/runners.py +++ b/Lib/asyncio/runners.py @@ -27,6 +27,7 @@ class Runner: If debug is True, the event loop will be run in debug mode. If loop_factory is passed, it is used for new event loop creation. + If eager_tasks is True, the loop creates eager tasks by default. asyncio.run(main(), debug=True) @@ -47,10 +48,11 @@ class Runner: # Note: the class is final, it is not intended for inheritance. - def __init__(self, *, debug=None, loop_factory=None): + def __init__(self, *, debug=None, loop_factory=None, eager_tasks=False): self._state = _State.CREATED self._debug = debug self._loop_factory = loop_factory + self._eager_tasks = eager_tasks self._loop = None self._context = None self._interrupt_count = 0 @@ -154,6 +156,8 @@ def _lazy_init(self): self._loop = self._loop_factory() if self._debug is not None: self._loop.set_debug(self._debug) + if self._eager_tasks: + self._loop.set_task_factory(tasks.eager_task_factory) self._context = contextvars.copy_context() self._state = _State.INITIALIZED @@ -167,7 +171,7 @@ def _on_sigint(self, signum, frame, main_task): raise KeyboardInterrupt() -def run(main, *, debug=None, loop_factory=None): +def run(main, *, debug=None, loop_factory=None, eager_tasks=False): """Execute the coroutine and return the result. This function runs the passed coroutine, taking care of @@ -179,6 +183,7 @@ def run(main, *, debug=None, loop_factory=None): If debug is True, the event loop will be run in debug mode. If loop_factory is passed, it is used for new event loop creation. + If eager_tasks is True, the loop creates eager tasks by default. This function always creates a new event loop and closes it at the end. It should be used as a main entry point for asyncio programs, and should @@ -201,7 +206,8 @@ async def main(): raise RuntimeError( "asyncio.run() cannot be called from a running event loop") - with Runner(debug=debug, loop_factory=loop_factory) as runner: + with Runner(debug=debug, loop_factory=loop_factory, + eager_tasks=eager_tasks) as runner: return runner.run(main) diff --git a/Lib/test/test_asyncio/test_runners.py b/Lib/test/test_asyncio/test_runners.py index b9d94833a2cccd..c9370358aee953 100644 --- a/Lib/test/test_asyncio/test_runners.py +++ b/Lib/test/test_asyncio/test_runners.py @@ -271,6 +271,20 @@ async def main(): asyncio.run(main(), loop_factory=asyncio.EventLoop) + def test_default_task_factory(self): + async def main(): + factory = asyncio.get_running_loop().get_task_factory() + self.assertIsNone(factory) + + asyncio.run(main()) + + def test_eager_task_factory(self): + async def main(): + factory = asyncio.get_running_loop().get_task_factory() + self.assertIs(factory, asyncio.eager_task_factory) + + asyncio.run(main(), eager_tasks=True) + class RunnerTests(BaseTest): @@ -513,6 +527,22 @@ async def coro(): self.assertEqual(0, result.repr_count) + def test_default_task_factory(self): + async def main(): + factory = asyncio.get_running_loop().get_task_factory() + self.assertIsNone(factory) + + with asyncio.Runner() as runner: + runner.run(main()) + + def test_eager_task_factory(self): + async def main(): + factory = asyncio.get_running_loop().get_task_factory() + self.assertIs(factory, asyncio.eager_task_factory) + + with asyncio.Runner(eager_tasks=True) as runner: + runner.run(main()) + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2024-12-27-15-41-55.gh-issue-128289.-hCzEc.rst b/Misc/NEWS.d/next/Library/2024-12-27-15-41-55.gh-issue-128289.-hCzEc.rst new file mode 100644 index 00000000000000..015512afad6ed3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-27-15-41-55.gh-issue-128289.-hCzEc.rst @@ -0,0 +1,2 @@ +Add *eager_tasks* parameter to :func:`asyncio.run` function and +:class:`asyncio.Runner` class constructor.