diff --git a/Doc/library/asyncio-runner.rst b/Doc/library/asyncio-runner.rst index 48d78099fd3ce7..9e84e4b94500df 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. @@ -47,10 +47,17 @@ Running an asyncio Program Passing :class:`asyncio.EventLoop` allows running asyncio without the policy system. + 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 are encouraged to use ``eager_tasks=True`` in their code. + Example:: async def main(): @@ -76,6 +83,8 @@ Running an asyncio Program *coro* can be any awaitable object. + Added *eager_tasks* parameter. + .. note:: The :mod:`!asyncio` policy system is deprecated and will be removed @@ -86,7 +95,7 @@ Running an asyncio Program 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. @@ -103,6 +112,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(): @@ -112,8 +124,16 @@ Runner context manager with asyncio.Runner() as runner: runner.run(main()) + .. note:: + + Users are encouraged to use ``eager_tasks=True`` in their code. + .. versionadded:: 3.11 + .. versionchanged:: 3.14 + + Added *eager_tasks* parameter. + .. method:: run(coro, *, context=None) Execute *coro* in the embedded event loop. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 935c61c474e889..d1975de7bd6955 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -1,4 +1,3 @@ - **************************** What's new in Python 3.14 **************************** @@ -292,6 +291,13 @@ ast * The ``repr()`` output for AST nodes now includes more information. (Contributed by Tomas R in :gh:`116022`.) +asyncio +------- + +* Add *eager_tasks* parameter to :func:`asyncio.run` function and + :class:`asyncio.Runner` class constructor. + (Contributed by Andrew Svetlov in :gh:`128289`.) + concurrent.futures ------------------ diff --git a/Lib/asyncio/runners.py b/Lib/asyncio/runners.py index 14397b4ad0c732..622d9d732b3180 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) @@ -46,10 +47,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 @@ -153,6 +155,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 @@ -166,7 +170,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 @@ -178,6 +182,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 @@ -200,7 +205,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 21f277bc2d8d5f..19d02863034949 100644 --- a/Lib/test/test_asyncio/test_runners.py +++ b/Lib/test/test_asyncio/test_runners.py @@ -283,6 +283,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): @@ -522,6 +536,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.