]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-120284: Enhance `asyncio.run` to accept awaitable objects (#120566)
authorRon Frederick <ronf@timeheart.net>
Thu, 26 Sep 2024 06:15:08 +0000 (23:15 -0700)
committerGitHub <noreply@github.com>
Thu, 26 Sep 2024 06:15:08 +0000 (11:45 +0530)
Co-authored-by: Kumar Aditya <kumaraditya@python.org>
Doc/library/asyncio-runner.rst
Lib/asyncio/runners.py
Lib/test/test_asyncio/test_runners.py
Misc/NEWS.d/next/Library/2024-06-15-23-38-36.gh-issue-120284.HwsAtY.rst [new file with mode: 0644]

index 8312e55126a7c524a226c0f62d0251449015e72f..28d5aaf3692baa3a98b370b9100a71b01b5512e0 100644 (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.
+
+      The argument can be any awaitable object.
 
-      Return the coroutine's result or raise its exception.
+      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.
index 1b89236599aad7052f7e13b2e826a5d1d13875de..0e63c34f60f4d9e3b124e78f36baae97c48f819a 100644 (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()
index 266f057f0776c3dfeaa728b2f7685dd96a9d2d63..45f70d09a2083a0b2fffff0da5d9d57ed9c6ced5 100644 (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 = runner.get_loop().create_future()
+            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()
diff --git a/Misc/NEWS.d/next/Library/2024-06-15-23-38-36.gh-issue-120284.HwsAtY.rst b/Misc/NEWS.d/next/Library/2024-06-15-23-38-36.gh-issue-120284.HwsAtY.rst
new file mode 100644 (file)
index 0000000..a2a6883
--- /dev/null
@@ -0,0 +1,2 @@
+Allow :meth:`asyncio.Runner.run` to accept :term:`awaitable`
+objects instead of simply :term:`coroutine`\s.