Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions Doc/library/asyncio-task.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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::

Expand All @@ -127,18 +142,16 @@ 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())
if (loop.time() + 1.0) >= end_time:
break
await asyncio.sleep(1)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a bit more concise

         while (loop.time() + 1.0) < end_time:
              print(datetime.datetime.now())
              await asyncio.sleep(1)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can replace this while loop with for _ in range(5) and the example will only become clearer.

We'll have a separate pass over asyncio docs before 3.7 is released. We'll try to come up with better examples and improve the current ones. So for now, I'd keep this snippet as is.


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::

Expand Down
2 changes: 2 additions & 0 deletions Lib/asyncio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand All @@ -23,6 +24,7 @@
futures.__all__ +
locks.__all__ +
protocols.__all__ +
runners.__all__ +
queues.__all__ +
streams.__all__ +
subprocess.__all__ +
Expand Down
48 changes: 48 additions & 0 deletions Lib/asyncio/runners.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set_event_loop() cannot fail (it has a couple asserts but we can neglect this fact).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure it can. It calls policy.set_event_loop(), and given the fact that users can provide their own buggy policies, we can't really say that set_event_loop is 100% safe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a flag of broken third party loop implementation, not user code problem.
Buggy loop can do weird things, I expect asyncio will be broken somehow anyway.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's your point?

The point of this code is to always close the loop, no matter what. What benefit is there in moving set_event_loop() one line up?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyways, if you think that the code will read better if set_event_loop is outside of the try block I can move it. We call set_event_loop in the finally block along with the loop.close(), so as you say: if these things are broken then nothing will actually work anyways.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it is very minor thing.
From my perspective wrapping loop.run_until_complete(main) only increases code readability a little but I can live with current implementation too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll merge as-is then.

loop.set_debug(debug)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never fails

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, it can, if, say, I have a bug in uvloop.Loop.set_debug method. I prefer to assume nothing, when it's possible to set custom policies and custom event loops.

return loop.run_until_complete(main)
finally:
try:
loop.run_until_complete(loop.shutdown_asyncgens())
finally:
events.set_event_loop(None)
loop.close()
100 changes: 100 additions & 0 deletions Lib/test/test_asyncio/test_runners.py
Original file line number Diff line number Diff line change
@@ -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())
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Implement asyncio.run().