diff options
author | Yury Selivanov <yury@magic.io> | 2017-12-14 14:42:21 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-12-14 14:42:21 (GMT) |
commit | 02a0a19206da6902c3855a1fa09e60b208474cfa (patch) | |
tree | df9a24bf2a131693ef4f3dad55849c22a1567991 | |
parent | eadad1b97f64619bfd246b9d3b60d25f456e0592 (diff) | |
download | cpython-02a0a19206da6902c3855a1fa09e60b208474cfa.zip cpython-02a0a19206da6902c3855a1fa09e60b208474cfa.tar.gz cpython-02a0a19206da6902c3855a1fa09e60b208474cfa.tar.bz2 |
bpo-32314: Implement asyncio.run() (#4852)
-rw-r--r-- | Doc/library/asyncio-task.rst | 31 | ||||
-rw-r--r-- | Lib/asyncio/__init__.py | 2 | ||||
-rw-r--r-- | Lib/asyncio/runners.py | 48 | ||||
-rw-r--r-- | Lib/test/test_asyncio/test_runners.py | 100 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2017-12-13-16-47-38.bpo-32314.W4_U2j.rst | 1 |
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(). |