summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYury Selivanov <yury@magic.io>2017-12-14 14:42:21 (GMT)
committerGitHub <noreply@github.com>2017-12-14 14:42:21 (GMT)
commit02a0a19206da6902c3855a1fa09e60b208474cfa (patch)
treedf9a24bf2a131693ef4f3dad55849c22a1567991
parenteadad1b97f64619bfd246b9d3b60d25f456e0592 (diff)
downloadcpython-02a0a19206da6902c3855a1fa09e60b208474cfa.zip
cpython-02a0a19206da6902c3855a1fa09e60b208474cfa.tar.gz
cpython-02a0a19206da6902c3855a1fa09e60b208474cfa.tar.bz2
bpo-32314: Implement asyncio.run() (#4852)
-rw-r--r--Doc/library/asyncio-task.rst31
-rw-r--r--Lib/asyncio/__init__.py2
-rw-r--r--Lib/asyncio/runners.py48
-rw-r--r--Lib/test/test_asyncio/test_runners.py100
-rw-r--r--Misc/NEWS.d/next/Library/2017-12-13-16-47-38.bpo-32314.W4_U2j.rst1
5 files changed, 173 insertions, 9 deletions
diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst
index a8a0a8e..0d0569f0 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 dd6686d..23ea055 100644
--- a/Lib/asyncio/__init__.py
+++ b/Lib/asyncio/__init__.py
@@ -11,6 +11,7 @@ from .events import *
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 @@ __all__ = (base_events.__all__ +
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 0000000..94d9409
--- /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 0000000..c52bd94
--- /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 0000000..416906c
--- /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().