Repository: tornadoweb/tornado
Pull request #2072 (2017-06-04 01:56:54)

ioloop: Make asyncio the default when available
In addition to changing the configurable default, add a special case
in IOLoop.current() so that the current IOLoop will be backed by
the main asyncio event loop.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 4b2ac2bc..d2b54921 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -61,13 +61,12 @@ script:
     - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then python -bb $TARGET; fi
     - if [[ $TRAVIS_PYTHON_VERSION != pypy* ]]; then python $TARGET --resolver=tornado.netutil.ThreadedResolver; fi
     - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then python $TARGET --httpclient=tornado.curl_httpclient.CurlAsyncHTTPClient; fi
-    - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then python $TARGET --ioloop_time_monotonic; fi
     - if [[ $TRAVIS_PYTHON_VERSION != 'pypy'* ]]; then python $TARGET --ioloop=tornado.platform.twisted.TwistedIOLoop; fi
-    - if [[ $TRAVIS_PYTHON_VERSION == 3.4 || $TRAVIS_PYTHON_VERSION == 3.5 || $TRAVIS_PYTHON_VERSION == 3.6 ]]; then python $TARGET --ioloop=tornado.platform.asyncio.AsyncIOLoop; fi
+    - if [[ $TRAVIS_PYTHON_VERSION == 3.4 || $TRAVIS_PYTHON_VERSION == 3.5 || $TRAVIS_PYTHON_VERSION == 3.6 ]]; then python $TARGET --ioloop=tornado.ioloop.PollIOLoop; fi
     - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then python $TARGET --ioloop=tornado.platform.asyncio.AsyncIOLoop; fi
     - if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then python $TARGET --resolver=tornado.platform.twisted.TwistedResolver; fi
+    - if [[ $TRAVIS_PYTHON_VERSION != pypy* ]]; then python $TARGET --ioloop=tornado.ioloop.PollIOLoop --ioloop_time_monotonic; fi
     #- if [[ $TRAVIS_PYTHON_VERSION != pypy* ]]; then python $TARGET --resolver=tornado.platform.caresresolver.CaresResolver; fi
-    - if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then python $TARGET --ioloop_time_monotonic; fi
     - if [[ $TRAVIS_PYTHON_VERSION != 'pypy3' ]]; then ../nodeps/bin/python -m tornado.test.runtests; fi
     # make coverage reports for Codecov to find
     - if [[ $TRAVIS_PYTHON_VERSION != nightly ]]; then coverage xml; fi
diff --git a/tornado/ioloop.py b/tornado/ioloop.py
index 46a60fb3..0527e922 100644
--- a/tornado/ioloop.py
+++ b/tornado/ioloop.py
@@ -61,6 +61,11 @@ if PY3:
 else:
     import thread
 
+try:
+    import asyncio
+except ImportError:
+    asyncio = None
+
 
 _POLL_TIMEOUT = 3600.0
 
@@ -242,7 +247,13 @@ class IOLoop(Configurable):
         """
         current = getattr(IOLoop._current, "instance", None)
         if current is None and instance:
-            current = IOLoop()
+            current = None
+            if asyncio is not None:
+                from tornado.platform.asyncio import AsyncIOLoop, AsyncIOMainLoop
+                if IOLoop.configured_class() is AsyncIOLoop:
+                    current = AsyncIOMainLoop()
+            if current is None:
+                current = IOLoop()
             if IOLoop._current.instance is not current:
                 raise RuntimeError("new IOLoop did not become current")
         return current
@@ -276,6 +287,9 @@ class IOLoop(Configurable):
 
     @classmethod
     def configurable_default(cls):
+        if asyncio is not None:
+            from tornado.platform.asyncio import AsyncIOLoop
+            return AsyncIOLoop
         return PollIOLoop
 
     def initialize(self, make_current=None):
diff --git a/tox.ini b/tox.ini
index 9e472ae8..409f025e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -30,7 +30,7 @@ envlist =
         {py2,py3}-select,
         {py2,py3}-full-twisted,
         py2-twistedlayered,
-        {py3,py33}-full-asyncio,
+        py3-full-poll,
         py2-full-trollius,
 
         # Alternate Resolvers.
@@ -84,7 +84,6 @@ deps =
      {py2,py27,pypy,py3,pypy3}-full: mock
      # singledispatch became standard in py34.
      {py2,py27,pypy,py3,py33}-full: singledispatch
-     py33-asyncio: asyncio
      trollius: trollius
      py2-monotonic: monotonic
      sphinx: sphinx
@@ -120,13 +119,14 @@ commands =
          # implementations; this flag controls which client all the
          # other tests use.
          curl: --httpclient=tornado.curl_httpclient.CurlAsyncHTTPClient \
+         poll: --ioloop=tornado.ioloop.PollIOLoop \
          select: --ioloop=tornado.platform.select.SelectIOLoop \
          twisted: --ioloop=tornado.platform.twisted.TwistedIOLoop \
          twistedlayered: --ioloop=tornado.test.twisted_test.LayeredTwistedIOLoop --resolver=tornado.platform.twisted.TwistedResolver \
-         {asyncio,trollius}: --ioloop=tornado.platform.asyncio.AsyncIOLoop \
+         trollius: --ioloop=tornado.platform.asyncio.AsyncIOLoop \
          caresresolver: --resolver=tornado.platform.caresresolver.CaresResolver \
          threadedresolver: --resolver=tornado.netutil.ThreadedResolver \
-         monotonic: --ioloop_time_monotonic \
+         monotonic: --ioloop=tornado.ioloop.PollIOLoop --ioloop_time_monotonic \
          # Test with a non-english locale to uncover str/bytes mixing issues.
          locale: --locale=zh_TW \
          {posargs:}


Test patch:
diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py
index 1fa42b7e..8281f39a 100644
--- a/tornado/test/httpclient_test.py
+++ b/tornado/test/httpclient_test.py
@@ -582,16 +582,11 @@ class HTTPResponseTestCase(unittest.TestCase):
 
 class SyncHTTPClientTest(unittest.TestCase):
     def setUp(self):
-        if IOLoop.configured_class().__name__ in ('TwistedIOLoop',
-                                                  'AsyncIOMainLoop'):
+        if IOLoop.configured_class().__name__ == 'TwistedIOLoop':
             # TwistedIOLoop only supports the global reactor, so we can't have
             # separate IOLoops for client and server threads.
-            # AsyncIOMainLoop doesn't work with the default policy
-            # (although it could with some tweaks to this test and a
-            # policy that created loops for non-main threads).
             raise unittest.SkipTest(
-                'Sync HTTPClient not compatible with TwistedIOLoop or '
-                'AsyncIOMainLoop')
+                'Sync HTTPClient not compatible with TwistedIOLoop')
         self.server_ioloop = IOLoop()
 
         @gen.coroutine
diff --git a/tornado/test/ioloop_test.py b/tornado/test/ioloop_test.py
index 1601813f..942e33c2 100644
--- a/tornado/test/ioloop_test.py
+++ b/tornado/test/ioloop_test.py
@@ -6,11 +6,13 @@ import contextlib
 import datetime
 import functools
 import socket
+import subprocess
 import sys
 import threading
 import time
 import types
 
+from tornado.escape import native_str
 from tornado import gen
 from tornado.ioloop import IOLoop, TimeoutError, PollIOLoop, PeriodicCallback
 from tornado.log import app_log
@@ -24,6 +26,16 @@ try:
 except ImportError:
     futures = None
 
+try:
+    import asyncio
+except ImportError:
+    asyncio = None
+
+try:
+    import twisted
+except ImportError:
+    twisted = None
+
 
 class FakeTimeSelect(_Select):
     def __init__(self):
@@ -263,7 +275,9 @@ class TestIOLoop(AsyncTestCase):
         self.io_loop.call_later(0, results.append, 4)
         self.io_loop.call_later(0, self.stop)
         self.wait()
-        self.assertEqual(results, [1, 2, 3, 4])
+        # The asyncio event loop does not guarantee the order of these
+        # callbacks, but PollIOLoop does.
+        self.assertEqual(sorted(results), [1, 2, 3, 4])
 
     def test_add_timeout_return(self):
         # All the timeout methods return non-None handles that can be
@@ -677,5 +691,58 @@ class TestPeriodicCallback(unittest.TestCase):
         self.assertEqual(calls, expected)
 
 
+class TestIOLoopConfiguration(unittest.TestCase):
+    def run_python(self, *statements):
+        statements = [
+            'from tornado.ioloop import IOLoop, PollIOLoop',
+            'classname = lambda x: x.__class__.__name__',
+        ] + list(statements)
+        args = [sys.executable, '-c', '; '.join(statements)]
+        return native_str(subprocess.check_output(args)).strip()
+
+    def test_default(self):
+        if asyncio is not None:
+            # When asyncio is available, it is used by default.
+            cls = self.run_python('print(classname(IOLoop.current()))')
+            self.assertEqual(cls, 'AsyncIOMainLoop')
+            cls = self.run_python('print(classname(IOLoop()))')
+            self.assertEqual(cls, 'AsyncIOLoop')
+        else:
+            # Otherwise, the default is a subclass of PollIOLoop
+            is_poll = self.run_python(
+                'print(isinstance(IOLoop.current(), PollIOLoop))')
+            self.assertEqual(is_poll, 'True')
+
+    def test_explicit_select(self):
+        # SelectIOLoop can always be configured explicitly.
+        default_class = self.run_python(
+            'IOLoop.configure("tornado.platform.select.SelectIOLoop")',
+            'print(classname(IOLoop.current()))')
+        self.assertEqual(default_class, 'SelectIOLoop')
+
+    @unittest.skipIf(asyncio is None, "asyncio module not present")
+    def test_asyncio(self):
+        cls = self.run_python(
+            'IOLoop.configure("tornado.platform.asyncio.AsyncIOLoop")',
+            'print(classname(IOLoop.current()))')
+        self.assertEqual(cls, 'AsyncIOMainLoop')
+
+    @unittest.skipIf(asyncio is None, "asyncio module not present")
+    def test_asyncio_main(self):
+        cls = self.run_python(
+            'from tornado.platform.asyncio import AsyncIOMainLoop',
+            'AsyncIOMainLoop().install()',
+            'print(classname(IOLoop.current()))')
+        self.assertEqual(cls, 'AsyncIOMainLoop')
+
+    @unittest.skipIf(twisted is None, "twisted module not present")
+    def test_twisted(self):
+        cls = self.run_python(
+            'from tornado.platform.twisted import TwistedIOLoop',
+            'TwistedIOLoop().install()',
+            'print(classname(IOLoop.current()))')
+        self.assertEqual(cls, 'TwistedIOLoop')
+
+
 if __name__ == "__main__":
     unittest.main()
diff --git a/tornado/test/process_test.py b/tornado/test/process_test.py
index d8337f5d..8497a7d8 100644
--- a/tornado/test/process_test.py
+++ b/tornado/test/process_test.py
@@ -17,12 +17,15 @@ from tornado.testing import bind_unused_port, ExpectLog, AsyncTestCase, gen_test
 from tornado.test.util import unittest, skipIfNonUnix
 from tornado.web import RequestHandler, Application
 
+try:
+    import asyncio
+except ImportError:
+    asyncio = None
+
 
 def skip_if_twisted():
-    if IOLoop.configured_class().__name__.endswith(('TwistedIOLoop',
-                                                    'AsyncIOMainLoop')):
-        raise unittest.SkipTest("Process tests not compatible with "
-                                "TwistedIOLoop or AsyncIOMainLoop")
+    if IOLoop.configured_class().__name__.endswith('TwistedIOLoop'):
+        raise unittest.SkipTest("Process tests not compatible with TwistedIOLoop")
 
 # Not using AsyncHTTPTestCase because we need control over the IOLoop.
 
@@ -58,8 +61,10 @@ class ProcessTest(unittest.TestCase):
         super(ProcessTest, self).tearDown()
 
     def test_multi_process(self):
-        # This test can't work on twisted because we use the global reactor
-        # and have no way to get it back into a sane state after the fork.
+        # This test doesn't work on twisted because we use the global
+        # reactor and don't restore it to a sane state after the fork
+        # (asyncio has the same issue, but we have a special case in
+        # place for it).
         skip_if_twisted()
         with ExpectLog(gen_log, "(Starting .* processes|child .* exited|uncaught exception)"):
             self.assertFalse(IOLoop.initialized())
@@ -81,6 +86,10 @@ class ProcessTest(unittest.TestCase):
                 sock.close()
                 return
             try:
+                if asyncio is not None:
+                    # Reset the global asyncio event loop, which was put into
+                    # a broken state by the fork.
+                    asyncio.set_event_loop(asyncio.new_event_loop())
                 if id in (0, 1):
                     self.assertEqual(id, task_id())
                     server = HTTPServer(self.get_app())