gh-120284: Enhance `asyncio.run` to accept awaitable objects (#120566)

Co-authored-by: Kumar Aditya <kumaraditya@python.org>
This commit is contained in:
Ron Frederick 2024-09-25 23:15:08 -07:00 committed by GitHub
parent 46f5cbca4c
commit 1229cb8c14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 22 deletions

View File

@ -24,11 +24,13 @@ Running an asyncio Program
.. function:: run(coro, *, debug=None, loop_factory=None)
Execute the :term:`coroutine` *coro* and return the result.
Execute *coro* in an asyncio event loop and return the result.
This function runs the passed coroutine, taking care of
managing the asyncio event loop, *finalizing asynchronous
generators*, and closing the executor.
The argument can be any awaitable object.
This function runs the awaitable, taking care of managing the
asyncio event loop, *finalizing asynchronous generators*, and
closing the executor.
This function cannot be called when another asyncio event loop is
running in the same thread.
@ -70,6 +72,10 @@ Running an asyncio Program
Added *loop_factory* parameter.
.. versionchanged:: 3.14
*coro* can be any awaitable object.
Runner context manager
======================
@ -104,17 +110,25 @@ Runner context manager
.. method:: run(coro, *, context=None)
Run a :term:`coroutine <coroutine>` *coro* in the embedded loop.
Execute *coro* in the embedded event loop.
Return the coroutine's result or raise its exception.
The argument can be any awaitable object.
If the argument is a coroutine, it is wrapped in a Task.
An optional keyword-only *context* argument allows specifying a
custom :class:`contextvars.Context` for the *coro* to run in.
The runner's default context is used if ``None``.
custom :class:`contextvars.Context` for the code to run in.
The runner's default context is used if context is ``None``.
Returns the awaitable's result or raises an exception.
This function cannot be called when another asyncio event loop is
running in the same thread.
.. versionchanged:: 3.14
*coro* can be any awaitable object.
.. method:: close()
Close the runner.

View File

@ -3,6 +3,7 @@ __all__ = ('Runner', 'run')
import contextvars
import enum
import functools
import inspect
import threading
import signal
from . import coroutines
@ -84,10 +85,7 @@ class Runner:
return self._loop
def run(self, coro, *, context=None):
"""Run a coroutine inside the embedded event loop."""
if not coroutines.iscoroutine(coro):
raise ValueError("a coroutine was expected, got {!r}".format(coro))
"""Run code in the embedded event loop."""
if events._get_running_loop() is not None:
# fail fast with short traceback
raise RuntimeError(
@ -95,8 +93,19 @@ class Runner:
self._lazy_init()
if not coroutines.iscoroutine(coro):
if inspect.isawaitable(coro):
async def _wrap_awaitable(awaitable):
return await awaitable
coro = _wrap_awaitable(coro)
else:
raise TypeError('An asyncio.Future, a coroutine or an '
'awaitable is required')
if context is None:
context = self._context
task = self._loop.create_task(coro, context=context)
if (threading.current_thread() is threading.main_thread()

View File

@ -93,8 +93,8 @@ class RunTests(BaseTest):
def test_asyncio_run_only_coro(self):
for o in {1, lambda: None}:
with self.subTest(obj=o), \
self.assertRaisesRegex(ValueError,
'a coroutine was expected'):
self.assertRaisesRegex(TypeError,
'an awaitable is required'):
asyncio.run(o)
def test_asyncio_run_debug(self):
@ -319,19 +319,28 @@ class RunnerTests(BaseTest):
def test_run_non_coro(self):
with asyncio.Runner() as runner:
with self.assertRaisesRegex(
ValueError,
"a coroutine was expected"
TypeError,
"an awaitable is required"
):
runner.run(123)
def test_run_future(self):
with asyncio.Runner() as runner:
with self.assertRaisesRegex(
ValueError,
"a coroutine was expected"
):
fut = runner.get_loop().create_future()
runner.run(fut)
fut.set_result('done')
self.assertEqual('done', runner.run(fut))
def test_run_awaitable(self):
class MyAwaitable:
def __await__(self):
return self.run().__await__()
@staticmethod
async def run():
return 'done'
with asyncio.Runner() as runner:
self.assertEqual('done', runner.run(MyAwaitable()))
def test_explicit_close(self):
runner = asyncio.Runner()

View File

@ -0,0 +1,2 @@
Allow :meth:`asyncio.Runner.run` to accept :term:`awaitable`
objects instead of simply :term:`coroutine`\s.