[Python-checkins] bpo-32314: Implement asyncio.run() (#4852)

Yury Selivanov webhook-mailer at python.org
Thu Dec 14 09:42:24 EST 2017


https://github.com/python/cpython/commit/02a0a19206da6902c3855a1fa09e60b208474cfa
commit: 02a0a19206da6902c3855a1fa09e60b208474cfa
branch: master
author: Yury Selivanov <yury at magic.io>
committer: GitHub <noreply at github.com>
date: 2017-12-14T09:42:21-05:00
summary:

bpo-32314: Implement asyncio.run() (#4852)

files:
A Lib/asyncio/runners.py
A Lib/test/test_asyncio/test_runners.py
A Misc/NEWS.d/next/Library/2017-12-13-16-47-38.bpo-32314.W4_U2j.rst
M Doc/library/asyncio-task.rst
M Lib/asyncio/__init__.py

diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst
index a8a0a8e85ef..0d0569f0ba1 100644
--- a/Doc/library/asyncio-task.rst
+++ b/Doc/library/asyncio-task.rst
@@ -92,6 +92,24 @@ Coroutines (and tasks) can only run when the event loop is running.
     used in a callback-style code, wrap its result with :func:`ensure_future`.
 
 
+.. function:: asyncio.run(coro, \*, debug=False)
+
+    This function runs the passed coroutine, taking care of
+    managing the asyncio event loop and finalizing asynchronous
+    generators.
+
+    This function cannot be called when another asyncio event loop is
+    running in the same thread.
+
+    If debug is True, the event loop will be run in debug mode.
+
+    This function always creates a new event loop and closes it at
+    the end.  It should be used as a main entry point for asyncio
+    programs, and should ideally only be called once.
+
+    .. versionadded:: 3.7
+
+
 .. _asyncio-hello-world-coroutine:
 
 Example: Hello World coroutine
@@ -104,10 +122,7 @@ Example of coroutine displaying ``"Hello World"``::
     async def hello_world():
         print("Hello World!")
 
-    loop = asyncio.get_event_loop()
-    # Blocking call which returns when the hello_world() coroutine is done
-    loop.run_until_complete(hello_world())
-    loop.close()
+    asyncio.run(hello_world())
 
 .. seealso::
 
@@ -127,7 +142,8 @@ using the :meth:`sleep` function::
     import asyncio
     import datetime
 
-    async def display_date(loop):
+    async def display_date():
+        loop = asyncio.get_running_loop()
         end_time = loop.time() + 5.0
         while True:
             print(datetime.datetime.now())
@@ -135,10 +151,7 @@ using the :meth:`sleep` function::
                 break
             await asyncio.sleep(1)
 
-    loop = asyncio.get_event_loop()
-    # Blocking call which returns when the display_date() coroutine is done
-    loop.run_until_complete(display_date(loop))
-    loop.close()
+    asyncio.run(display_date())
 
 .. seealso::
 
diff --git a/Lib/asyncio/__init__.py b/Lib/asyncio/__init__.py
index dd6686de840..23ea055912e 100644
--- a/Lib/asyncio/__init__.py
+++ b/Lib/asyncio/__init__.py
@@ -11,6 +11,7 @@
 from .futures import *
 from .locks import *
 from .protocols import *
+from .runners import *
 from .queues import *
 from .streams import *
 from .subprocess import *
@@ -23,6 +24,7 @@
            futures.__all__ +
            locks.__all__ +
            protocols.__all__ +
+           runners.__all__ +
            queues.__all__ +
            streams.__all__ +
            subprocess.__all__ +
diff --git a/Lib/asyncio/runners.py b/Lib/asyncio/runners.py
new file mode 100644
index 00000000000..94d94097ab9
--- /dev/null
+++ b/Lib/asyncio/runners.py
@@ -0,0 +1,48 @@
+__all__ = 'run',
+
+from . import coroutines
+from . import events
+
+
+def run(main, *, debug=False):
+    """Run a coroutine.
+
+    This function runs the passed coroutine, taking care of
+    managing the asyncio event loop and finalizing asynchronous
+    generators.
+
+    This function cannot be called when another asyncio event loop is
+    running in the same thread.
+
+    If debug is True, the event loop will be run in debug mode.
+
+    This function always creates a new event loop and closes it at the end.
+    It should be used as a main entry point for asyncio programs, and should
+    ideally only be called once.
+
+    Example:
+
+        async def main():
+            await asyncio.sleep(1)
+            print('hello')
+
+        asyncio.run(main())
+    """
+    if events._get_running_loop() is not None:
+        raise RuntimeError(
+            "asyncio.run() cannot be called from a running event loop")
+
+    if not coroutines.iscoroutine(main):
+        raise ValueError("a coroutine was expected, got {!r}".format(main))
+
+    loop = events.new_event_loop()
+    try:
+        events.set_event_loop(loop)
+        loop.set_debug(debug)
+        return loop.run_until_complete(main)
+    finally:
+        try:
+            loop.run_until_complete(loop.shutdown_asyncgens())
+        finally:
+            events.set_event_loop(None)
+            loop.close()
diff --git a/Lib/test/test_asyncio/test_runners.py b/Lib/test/test_asyncio/test_runners.py
new file mode 100644
index 00000000000..c52bd9443ea
--- /dev/null
+++ b/Lib/test/test_asyncio/test_runners.py
@@ -0,0 +1,100 @@
+import asyncio
+import unittest
+
+from unittest import mock
+
+
+class TestPolicy(asyncio.AbstractEventLoopPolicy):
+
+    def __init__(self, loop_factory):
+        self.loop_factory = loop_factory
+        self.loop = None
+
+    def get_event_loop(self):
+        # shouldn't ever be called by asyncio.run()
+        raise RuntimeError
+
+    def new_event_loop(self):
+        return self.loop_factory()
+
+    def set_event_loop(self, loop):
+        if loop is not None:
+            # we want to check if the loop is closed
+            # in BaseTest.tearDown
+            self.loop = loop
+
+
+class BaseTest(unittest.TestCase):
+
+    def new_loop(self):
+        loop = asyncio.BaseEventLoop()
+        loop._process_events = mock.Mock()
+        loop._selector = mock.Mock()
+        loop._selector.select.return_value = ()
+        loop.shutdown_ag_run = False
+
+        async def shutdown_asyncgens():
+            loop.shutdown_ag_run = True
+        loop.shutdown_asyncgens = shutdown_asyncgens
+
+        return loop
+
+    def setUp(self):
+        super().setUp()
+
+        policy = TestPolicy(self.new_loop)
+        asyncio.set_event_loop_policy(policy)
+
+    def tearDown(self):
+        policy = asyncio.get_event_loop_policy()
+        if policy.loop is not None:
+            self.assertTrue(policy.loop.is_closed())
+            self.assertTrue(policy.loop.shutdown_ag_run)
+
+        asyncio.set_event_loop_policy(None)
+        super().tearDown()
+
+
+class RunTests(BaseTest):
+
+    def test_asyncio_run_return(self):
+        async def main():
+            await asyncio.sleep(0)
+            return 42
+
+        self.assertEqual(asyncio.run(main()), 42)
+
+    def test_asyncio_run_raises(self):
+        async def main():
+            await asyncio.sleep(0)
+            raise ValueError('spam')
+
+        with self.assertRaisesRegex(ValueError, 'spam'):
+            asyncio.run(main())
+
+    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'):
+                asyncio.run(o)
+
+    def test_asyncio_run_debug(self):
+        async def main(expected):
+            loop = asyncio.get_event_loop()
+            self.assertIs(loop.get_debug(), expected)
+
+        asyncio.run(main(False))
+        asyncio.run(main(True), debug=True)
+
+    def test_asyncio_run_from_running_loop(self):
+        async def main():
+            coro = main()
+            try:
+                asyncio.run(coro)
+            finally:
+                coro.close()  # Suppress ResourceWarning
+
+        with self.assertRaisesRegex(RuntimeError,
+                                    'cannot be called from a running'):
+            asyncio.run(main())
diff --git a/Misc/NEWS.d/next/Library/2017-12-13-16-47-38.bpo-32314.W4_U2j.rst b/Misc/NEWS.d/next/Library/2017-12-13-16-47-38.bpo-32314.W4_U2j.rst
new file mode 100644
index 00000000000..416906c9662
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-12-13-16-47-38.bpo-32314.W4_U2j.rst
@@ -0,0 +1 @@
+Implement asyncio.run().



More information about the Python-checkins mailing list