Skip to content

Commit 4119d2d

Browse files
asvetlovzware
andauthored
bpo-47062: Implement asyncio.Runner context manager (GH-31799)
Co-authored-by: Zachary Ware <zach@python.org>
1 parent 2f49b97 commit 4119d2d

File tree

7 files changed

+381
-106
lines changed

7 files changed

+381
-106
lines changed

Doc/library/asyncio-runner.rst

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
.. currentmodule:: asyncio
2+
3+
4+
=======
5+
Runners
6+
=======
7+
8+
**Source code:** :source:`Lib/asyncio/runners.py`
9+
10+
11+
This section outlines high-level asyncio primitives to run asyncio code.
12+
13+
They are built on top of an :ref:`event loop <asyncio-event-loop>` with the aim
14+
to simplify async code usage for common wide-spread scenarios.
15+
16+
.. contents::
17+
:depth: 1
18+
:local:
19+
20+
21+
22+
Running an asyncio Program
23+
==========================
24+
25+
.. function:: run(coro, *, debug=None)
26+
27+
Execute the :term:`coroutine` *coro* and return the result.
28+
29+
This function runs the passed coroutine, taking care of
30+
managing the asyncio event loop, *finalizing asynchronous
31+
generators*, and closing the threadpool.
32+
33+
This function cannot be called when another asyncio event loop is
34+
running in the same thread.
35+
36+
If *debug* is ``True``, the event loop will be run in debug mode. ``False`` disables
37+
debug mode explicitly. ``None`` is used to respect the global
38+
:ref:`asyncio-debug-mode` settings.
39+
40+
This function always creates a new event loop and closes it at
41+
the end. It should be used as a main entry point for asyncio
42+
programs, and should ideally only be called once.
43+
44+
Example::
45+
46+
async def main():
47+
await asyncio.sleep(1)
48+
print('hello')
49+
50+
asyncio.run(main())
51+
52+
.. versionadded:: 3.7
53+
54+
.. versionchanged:: 3.9
55+
Updated to use :meth:`loop.shutdown_default_executor`.
56+
57+
.. versionchanged:: 3.10
58+
59+
*debug* is ``None`` by default to respect the global debug mode settings.
60+
61+
62+
Runner context manager
63+
======================
64+
65+
.. class:: Runner(*, debug=None, factory=None)
66+
67+
A context manager that simplifies *multiple* async function calls in the same
68+
context.
69+
70+
Sometimes several top-level async functions should be called in the same :ref:`event
71+
loop <asyncio-event-loop>` and :class:`contextvars.Context`.
72+
73+
If *debug* is ``True``, the event loop will be run in debug mode. ``False`` disables
74+
debug mode explicitly. ``None`` is used to respect the global
75+
:ref:`asyncio-debug-mode` settings.
76+
77+
*factory* could be used for overriding the loop creation.
78+
:func:`asyncio.new_event_loop` is used if ``None``.
79+
80+
Basically, :func:`asyncio.run()` example can be rewritten with the runner usage::
81+
82+
async def main():
83+
await asyncio.sleep(1)
84+
print('hello')
85+
86+
with asyncio.Runner() as runner:
87+
runner.run(main())
88+
89+
.. versionadded:: 3.11
90+
91+
.. method:: run(coro, *, context=None)
92+
93+
Run a :term:`coroutine <coroutine>` *coro* in the embedded loop.
94+
95+
Return the coroutine's result or raise its exception.
96+
97+
An optional keyword-only *context* argument allows specifying a
98+
custom :class:`contextvars.Context` for the *coro* to run in.
99+
The runner's default context is used if ``None``.
100+
101+
This function cannot be called when another asyncio event loop is
102+
running in the same thread.
103+
104+
.. method:: close()
105+
106+
Close the runner.
107+
108+
Finalize asynchronous generators, shutdown default executor, close the event loop
109+
and release embedded :class:`contextvars.Context`.
110+
111+
.. method:: get_loop()
112+
113+
Return the event loop associated with the runner instance.
114+
115+
.. note::
116+
117+
:class:`Runner` uses the lazy initialization strategy, its constructor doesn't
118+
initialize underlying low-level structures.
119+
120+
Embedded *loop* and *context* are created at the :keyword:`with` body entering
121+
or the first call of :meth:`run` or :meth:`get_loop`.

Doc/library/asyncio-task.rst

-37
Original file line numberDiff line numberDiff line change
@@ -204,43 +204,6 @@ A good example of a low-level function that returns a Future object
204204
is :meth:`loop.run_in_executor`.
205205

206206

207-
Running an asyncio Program
208-
==========================
209-
210-
.. function:: run(coro, *, debug=False)
211-
212-
Execute the :term:`coroutine` *coro* and return the result.
213-
214-
This function runs the passed coroutine, taking care of
215-
managing the asyncio event loop, *finalizing asynchronous
216-
generators*, and closing the threadpool.
217-
218-
This function cannot be called when another asyncio event loop is
219-
running in the same thread.
220-
221-
If *debug* is ``True``, the event loop will be run in debug mode.
222-
223-
This function always creates a new event loop and closes it at
224-
the end. It should be used as a main entry point for asyncio
225-
programs, and should ideally only be called once.
226-
227-
Example::
228-
229-
async def main():
230-
await asyncio.sleep(1)
231-
print('hello')
232-
233-
asyncio.run(main())
234-
235-
.. versionadded:: 3.7
236-
237-
.. versionchanged:: 3.9
238-
Updated to use :meth:`loop.shutdown_default_executor`.
239-
240-
.. note::
241-
The source code for ``asyncio.run()`` can be found in
242-
:source:`Lib/asyncio/runners.py`.
243-
244207
Creating Tasks
245208
==============
246209

Doc/library/asyncio.rst

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Additionally, there are **low-level** APIs for
6767
:caption: High-level APIs
6868
:maxdepth: 1
6969

70+
asyncio-runner.rst
7071
asyncio-task.rst
7172
asyncio-stream.rst
7273
asyncio-sync.rst

Lib/asyncio/runners.py

+106-18
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,112 @@
1-
__all__ = 'run',
1+
__all__ = ('Runner', 'run')
22

3+
import contextvars
4+
import enum
35
from . import coroutines
46
from . import events
57
from . import tasks
68

79

10+
class _State(enum.Enum):
11+
CREATED = "created"
12+
INITIALIZED = "initialized"
13+
CLOSED = "closed"
14+
15+
16+
class Runner:
17+
"""A context manager that controls event loop life cycle.
18+
19+
The context manager always creates a new event loop,
20+
allows to run async functions inside it,
21+
and properly finalizes the loop at the context manager exit.
22+
23+
If debug is True, the event loop will be run in debug mode.
24+
If factory is passed, it is used for new event loop creation.
25+
26+
asyncio.run(main(), debug=True)
27+
28+
is a shortcut for
29+
30+
with asyncio.Runner(debug=True) as runner:
31+
runner.run(main())
32+
33+
The run() method can be called multiple times within the runner's context.
34+
35+
This can be useful for interactive console (e.g. IPython),
36+
unittest runners, console tools, -- everywhere when async code
37+
is called from existing sync framework and where the preferred single
38+
asyncio.run() call doesn't work.
39+
40+
"""
41+
42+
# Note: the class is final, it is not intended for inheritance.
43+
44+
def __init__(self, *, debug=None, factory=None):
45+
self._state = _State.CREATED
46+
self._debug = debug
47+
self._factory = factory
48+
self._loop = None
49+
self._context = None
50+
51+
def __enter__(self):
52+
self._lazy_init()
53+
return self
54+
55+
def __exit__(self, exc_type, exc_val, exc_tb):
56+
self.close()
57+
58+
def close(self):
59+
"""Shutdown and close event loop."""
60+
if self._state is not _State.INITIALIZED:
61+
return
62+
try:
63+
loop = self._loop
64+
_cancel_all_tasks(loop)
65+
loop.run_until_complete(loop.shutdown_asyncgens())
66+
loop.run_until_complete(loop.shutdown_default_executor())
67+
finally:
68+
loop.close()
69+
self._loop = None
70+
self._state = _State.CLOSED
71+
72+
def get_loop(self):
73+
"""Return embedded event loop."""
74+
self._lazy_init()
75+
return self._loop
76+
77+
def run(self, coro, *, context=None):
78+
"""Run a coroutine inside the embedded event loop."""
79+
if not coroutines.iscoroutine(coro):
80+
raise ValueError("a coroutine was expected, got {!r}".format(coro))
81+
82+
if events._get_running_loop() is not None:
83+
# fail fast with short traceback
84+
raise RuntimeError(
85+
"Runner.run() cannot be called from a running event loop")
86+
87+
self._lazy_init()
88+
89+
if context is None:
90+
context = self._context
91+
task = self._loop.create_task(coro, context=context)
92+
return self._loop.run_until_complete(task)
93+
94+
def _lazy_init(self):
95+
if self._state is _State.CLOSED:
96+
raise RuntimeError("Runner is closed")
97+
if self._state is _State.INITIALIZED:
98+
return
99+
if self._factory is None:
100+
self._loop = events.new_event_loop()
101+
else:
102+
self._loop = self._factory()
103+
if self._debug is not None:
104+
self._loop.set_debug(self._debug)
105+
self._context = contextvars.copy_context()
106+
self._state = _State.INITIALIZED
107+
108+
109+
8110
def run(main, *, debug=None):
9111
"""Execute the coroutine and return the result.
10112
@@ -30,26 +132,12 @@ async def main():
30132
asyncio.run(main())
31133
"""
32134
if events._get_running_loop() is not None:
135+
# fail fast with short traceback
33136
raise RuntimeError(
34137
"asyncio.run() cannot be called from a running event loop")
35138

36-
if not coroutines.iscoroutine(main):
37-
raise ValueError("a coroutine was expected, got {!r}".format(main))
38-
39-
loop = events.new_event_loop()
40-
try:
41-
events.set_event_loop(loop)
42-
if debug is not None:
43-
loop.set_debug(debug)
44-
return loop.run_until_complete(main)
45-
finally:
46-
try:
47-
_cancel_all_tasks(loop)
48-
loop.run_until_complete(loop.shutdown_asyncgens())
49-
loop.run_until_complete(loop.shutdown_default_executor())
50-
finally:
51-
events.set_event_loop(None)
52-
loop.close()
139+
with Runner(debug=debug) as runner:
140+
return runner.run(main)
53141

54142

55143
def _cancel_all_tasks(loop):

0 commit comments

Comments
 (0)