Skip to content

Commit 1229cb8

Browse files
ronfkumaraditya303
andauthored
gh-120284: Enhance asyncio.run to accept awaitable objects (#120566)
Co-authored-by: Kumar Aditya <[email protected]>
1 parent 46f5cbc commit 1229cb8

File tree

4 files changed

+56
-22
lines changed

4 files changed

+56
-22
lines changed

Doc/library/asyncio-runner.rst

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ Running an asyncio Program
2424

2525
.. function:: run(coro, *, debug=None, loop_factory=None)
2626

27-
Execute the :term:`coroutine` *coro* and return the result.
27+
Execute *coro* in an asyncio event loop and return the result.
2828

29-
This function runs the passed coroutine, taking care of
30-
managing the asyncio event loop, *finalizing asynchronous
31-
generators*, and closing the executor.
29+
The argument can be any awaitable object.
30+
31+
This function runs the awaitable, taking care of managing the
32+
asyncio event loop, *finalizing asynchronous generators*, and
33+
closing the executor.
3234

3335
This function cannot be called when another asyncio event loop is
3436
running in the same thread.
@@ -70,6 +72,10 @@ Running an asyncio Program
7072

7173
Added *loop_factory* parameter.
7274

75+
.. versionchanged:: 3.14
76+
77+
*coro* can be any awaitable object.
78+
7379

7480
Runner context manager
7581
======================
@@ -104,17 +110,25 @@ Runner context manager
104110

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

107-
Run a :term:`coroutine <coroutine>` *coro* in the embedded loop.
113+
Execute *coro* in the embedded event loop.
114+
115+
The argument can be any awaitable object.
108116

109-
Return the coroutine's result or raise its exception.
117+
If the argument is a coroutine, it is wrapped in a Task.
110118

111119
An optional keyword-only *context* argument allows specifying a
112-
custom :class:`contextvars.Context` for the *coro* to run in.
113-
The runner's default context is used if ``None``.
120+
custom :class:`contextvars.Context` for the code to run in.
121+
The runner's default context is used if context is ``None``.
122+
123+
Returns the awaitable's result or raises an exception.
114124

115125
This function cannot be called when another asyncio event loop is
116126
running in the same thread.
117127

128+
.. versionchanged:: 3.14
129+
130+
*coro* can be any awaitable object.
131+
118132
.. method:: close()
119133

120134
Close the runner.

Lib/asyncio/runners.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import contextvars
44
import enum
55
import functools
6+
import inspect
67
import threading
78
import signal
89
from . import coroutines
@@ -84,19 +85,27 @@ def get_loop(self):
8485
return self._loop
8586

8687
def run(self, coro, *, context=None):
87-
"""Run a coroutine inside the embedded event loop."""
88-
if not coroutines.iscoroutine(coro):
89-
raise ValueError("a coroutine was expected, got {!r}".format(coro))
90-
88+
"""Run code in the embedded event loop."""
9189
if events._get_running_loop() is not None:
9290
# fail fast with short traceback
9391
raise RuntimeError(
9492
"Runner.run() cannot be called from a running event loop")
9593

9694
self._lazy_init()
9795

96+
if not coroutines.iscoroutine(coro):
97+
if inspect.isawaitable(coro):
98+
async def _wrap_awaitable(awaitable):
99+
return await awaitable
100+
101+
coro = _wrap_awaitable(coro)
102+
else:
103+
raise TypeError('An asyncio.Future, a coroutine or an '
104+
'awaitable is required')
105+
98106
if context is None:
99107
context = self._context
108+
100109
task = self._loop.create_task(coro, context=context)
101110

102111
if (threading.current_thread() is threading.main_thread()

Lib/test/test_asyncio/test_runners.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ async def main():
9393
def test_asyncio_run_only_coro(self):
9494
for o in {1, lambda: None}:
9595
with self.subTest(obj=o), \
96-
self.assertRaisesRegex(ValueError,
97-
'a coroutine was expected'):
96+
self.assertRaisesRegex(TypeError,
97+
'an awaitable is required'):
9898
asyncio.run(o)
9999

100100
def test_asyncio_run_debug(self):
@@ -319,19 +319,28 @@ async def f():
319319
def test_run_non_coro(self):
320320
with asyncio.Runner() as runner:
321321
with self.assertRaisesRegex(
322-
ValueError,
323-
"a coroutine was expected"
322+
TypeError,
323+
"an awaitable is required"
324324
):
325325
runner.run(123)
326326

327327
def test_run_future(self):
328328
with asyncio.Runner() as runner:
329-
with self.assertRaisesRegex(
330-
ValueError,
331-
"a coroutine was expected"
332-
):
333-
fut = runner.get_loop().create_future()
334-
runner.run(fut)
329+
fut = runner.get_loop().create_future()
330+
fut.set_result('done')
331+
self.assertEqual('done', runner.run(fut))
332+
333+
def test_run_awaitable(self):
334+
class MyAwaitable:
335+
def __await__(self):
336+
return self.run().__await__()
337+
338+
@staticmethod
339+
async def run():
340+
return 'done'
341+
342+
with asyncio.Runner() as runner:
343+
self.assertEqual('done', runner.run(MyAwaitable()))
335344

336345
def test_explicit_close(self):
337346
runner = asyncio.Runner()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow :meth:`asyncio.Runner.run` to accept :term:`awaitable`
2+
objects instead of simply :term:`coroutine`\s.

0 commit comments

Comments
 (0)