Skip to content

Commit

Permalink
Add eager_tasks parameter to asyncio.run() and asyncio.Runner
Browse files Browse the repository at this point in the history
  • Loading branch information
asvetlov committed Dec 27, 2024
1 parent 0b5f1fa commit 8493e59
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 6 deletions.
26 changes: 24 additions & 2 deletions Doc/library/asyncio-runner.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -47,10 +47,18 @@ 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;
the default lazy tasks will be deprecated starting from Python 3.16.

Example::

async def main():
Expand All @@ -76,11 +84,13 @@ Running an asyncio Program

*coro* can be any awaitable object.

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.
Expand All @@ -97,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():
Expand All @@ -106,8 +119,17 @@ Runner context manager
with asyncio.Runner() as runner:
runner.run(main())

.. note::

Users are encouraged to use ``eager_tasks=True`` in their code;
the default lazy tasks will be deprecated starting from Python 3.16.

.. versionadded:: 3.11

.. versionchanged:: 3.14

Added *eager_tasks* parameter.

.. method:: run(coro, *, context=None)

Execute *coro* in the embedded event loop.
Expand Down
8 changes: 7 additions & 1 deletion Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

****************************
What's new in Python 3.14
****************************
Expand Down Expand Up @@ -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
------------------

Expand Down
12 changes: 9 additions & 3 deletions Lib/asyncio/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)


Expand Down
30 changes: 30 additions & 0 deletions Lib/test/test_asyncio/test_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add *eager_tasks* parameter to :func:`asyncio.run` function and
:class:`asyncio.Runner` class constructor.

0 comments on commit 8493e59

Please sign in to comment.