Skip to content

Commit 02a0a19

Browse files
authored
bpo-32314: Implement asyncio.run() (#4852)
1 parent eadad1b commit 02a0a19

File tree

5 files changed

+173
-9
lines changed

5 files changed

+173
-9
lines changed

Doc/library/asyncio-task.rst

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ Coroutines (and tasks) can only run when the event loop is running.
9292
used in a callback-style code, wrap its result with :func:`ensure_future`.
9393

9494

95+
.. function:: asyncio.run(coro, \*, debug=False)
96+
97+
This function runs the passed coroutine, taking care of
98+
managing the asyncio event loop and finalizing asynchronous
99+
generators.
100+
101+
This function cannot be called when another asyncio event loop is
102+
running in the same thread.
103+
104+
If debug is True, the event loop will be run in debug mode.
105+
106+
This function always creates a new event loop and closes it at
107+
the end. It should be used as a main entry point for asyncio
108+
programs, and should ideally only be called once.
109+
110+
.. versionadded:: 3.7
111+
112+
95113
.. _asyncio-hello-world-coroutine:
96114

97115
Example: Hello World coroutine
@@ -104,10 +122,7 @@ Example of coroutine displaying ``"Hello World"``::
104122
async def hello_world():
105123
print("Hello World!")
106124

107-
loop = asyncio.get_event_loop()
108-
# Blocking call which returns when the hello_world() coroutine is done
109-
loop.run_until_complete(hello_world())
110-
loop.close()
125+
asyncio.run(hello_world())
111126

112127
.. seealso::
113128

@@ -127,18 +142,16 @@ using the :meth:`sleep` function::
127142
import asyncio
128143
import datetime
129144

130-
async def display_date(loop):
145+
async def display_date():
146+
loop = asyncio.get_running_loop()
131147
end_time = loop.time() + 5.0
132148
while True:
133149
print(datetime.datetime.now())
134150
if (loop.time() + 1.0) >= end_time:
135151
break
136152
await asyncio.sleep(1)
137153

138-
loop = asyncio.get_event_loop()
139-
# Blocking call which returns when the display_date() coroutine is done
140-
loop.run_until_complete(display_date(loop))
141-
loop.close()
154+
asyncio.run(display_date())
142155

143156
.. seealso::
144157

Lib/asyncio/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .futures import *
1212
from .locks import *
1313
from .protocols import *
14+
from .runners import *
1415
from .queues import *
1516
from .streams import *
1617
from .subprocess import *
@@ -23,6 +24,7 @@
2324
futures.__all__ +
2425
locks.__all__ +
2526
protocols.__all__ +
27+
runners.__all__ +
2628
queues.__all__ +
2729
streams.__all__ +
2830
subprocess.__all__ +

Lib/asyncio/runners.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
__all__ = 'run',
2+
3+
from . import coroutines
4+
from . import events
5+
6+
7+
def run(main, *, debug=False):
8+
"""Run a coroutine.
9+
10+
This function runs the passed coroutine, taking care of
11+
managing the asyncio event loop and finalizing asynchronous
12+
generators.
13+
14+
This function cannot be called when another asyncio event loop is
15+
running in the same thread.
16+
17+
If debug is True, the event loop will be run in debug mode.
18+
19+
This function always creates a new event loop and closes it at the end.
20+
It should be used as a main entry point for asyncio programs, and should
21+
ideally only be called once.
22+
23+
Example:
24+
25+
async def main():
26+
await asyncio.sleep(1)
27+
print('hello')
28+
29+
asyncio.run(main())
30+
"""
31+
if events._get_running_loop() is not None:
32+
raise RuntimeError(
33+
"asyncio.run() cannot be called from a running event loop")
34+
35+
if not coroutines.iscoroutine(main):
36+
raise ValueError("a coroutine was expected, got {!r}".format(main))
37+
38+
loop = events.new_event_loop()
39+
try:
40+
events.set_event_loop(loop)
41+
loop.set_debug(debug)
42+
return loop.run_until_complete(main)
43+
finally:
44+
try:
45+
loop.run_until_complete(loop.shutdown_asyncgens())
46+
finally:
47+
events.set_event_loop(None)
48+
loop.close()
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import asyncio
2+
import unittest
3+
4+
from unittest import mock
5+
6+
7+
class TestPolicy(asyncio.AbstractEventLoopPolicy):
8+
9+
def __init__(self, loop_factory):
10+
self.loop_factory = loop_factory
11+
self.loop = None
12+
13+
def get_event_loop(self):
14+
# shouldn't ever be called by asyncio.run()
15+
raise RuntimeError
16+
17+
def new_event_loop(self):
18+
return self.loop_factory()
19+
20+
def set_event_loop(self, loop):
21+
if loop is not None:
22+
# we want to check if the loop is closed
23+
# in BaseTest.tearDown
24+
self.loop = loop
25+
26+
27+
class BaseTest(unittest.TestCase):
28+
29+
def new_loop(self):
30+
loop = asyncio.BaseEventLoop()
31+
loop._process_events = mock.Mock()
32+
loop._selector = mock.Mock()
33+
loop._selector.select.return_value = ()
34+
loop.shutdown_ag_run = False
35+
36+
async def shutdown_asyncgens():
37+
loop.shutdown_ag_run = True
38+
loop.shutdown_asyncgens = shutdown_asyncgens
39+
40+
return loop
41+
42+
def setUp(self):
43+
super().setUp()
44+
45+
policy = TestPolicy(self.new_loop)
46+
asyncio.set_event_loop_policy(policy)
47+
48+
def tearDown(self):
49+
policy = asyncio.get_event_loop_policy()
50+
if policy.loop is not None:
51+
self.assertTrue(policy.loop.is_closed())
52+
self.assertTrue(policy.loop.shutdown_ag_run)
53+
54+
asyncio.set_event_loop_policy(None)
55+
super().tearDown()
56+
57+
58+
class RunTests(BaseTest):
59+
60+
def test_asyncio_run_return(self):
61+
async def main():
62+
await asyncio.sleep(0)
63+
return 42
64+
65+
self.assertEqual(asyncio.run(main()), 42)
66+
67+
def test_asyncio_run_raises(self):
68+
async def main():
69+
await asyncio.sleep(0)
70+
raise ValueError('spam')
71+
72+
with self.assertRaisesRegex(ValueError, 'spam'):
73+
asyncio.run(main())
74+
75+
def test_asyncio_run_only_coro(self):
76+
for o in {1, lambda: None}:
77+
with self.subTest(obj=o), \
78+
self.assertRaisesRegex(ValueError,
79+
'a coroutine was expected'):
80+
asyncio.run(o)
81+
82+
def test_asyncio_run_debug(self):
83+
async def main(expected):
84+
loop = asyncio.get_event_loop()
85+
self.assertIs(loop.get_debug(), expected)
86+
87+
asyncio.run(main(False))
88+
asyncio.run(main(True), debug=True)
89+
90+
def test_asyncio_run_from_running_loop(self):
91+
async def main():
92+
coro = main()
93+
try:
94+
asyncio.run(coro)
95+
finally:
96+
coro.close() # Suppress ResourceWarning
97+
98+
with self.assertRaisesRegex(RuntimeError,
99+
'cannot be called from a running'):
100+
asyncio.run(main())
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Implement asyncio.run().

0 commit comments

Comments
 (0)