Repository: tornadoweb/tornado
Pull request #2437 (2018-07-06 19:04:05)

wsgi: Avoid touching the asyncio event loop at import time
Additional minor changes to tests and demos.


Patch:
diff --git a/demos/blog/README b/demos/blog/README
index d674c876..b4f7d2de 100644
--- a/demos/blog/README
+++ b/demos/blog/README
@@ -20,7 +20,7 @@ its prerequisites can be installed with `docker-compose up`.
 
 3. Create a database and user for the blog.
 
-   Connect to the database with `psql -U postgres` (for PostgreSQL) or
+   Connect to the database with `psql -Upostgres` (for PostgreSQL) or
    `cockroach sql` (for CockroachDB).
 
    Create a database and user, and grant permissions:
diff --git a/demos/chat/static/chat.js b/demos/chat/static/chat.js
index 151a5880..48a63c41 100644
--- a/demos/chat/static/chat.js
+++ b/demos/chat/static/chat.js
@@ -116,7 +116,6 @@ var updater = {
 
     newMessages: function(response) {
         if (!response.messages) return;
-        updater.cursor = response.cursor;
         var messages = response.messages;
         updater.cursor = messages[messages.length - 1].id;
         console.log(messages.length, "new messages, cursor:", updater.cursor);
diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py
index 03cac3d2..60b7956f 100644
--- a/tornado/simple_httpclient.py
+++ b/tornado/simple_httpclient.py
@@ -210,7 +210,6 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient):
 
 class _HTTPConnection(httputil.HTTPMessageDelegate):
     _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
-    _SAFE_METHODS = set(["GET", "HEAD", "OPTIONS"])
 
     def __init__(self, client, request, release_callback,
                  final_callback, max_buffer_size, tcp_client,
@@ -497,8 +496,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
     def _should_follow_redirect(self):
         return (self.request.follow_redirects and
                 self.request.max_redirects > 0 and
-                self.code in (301, 302, 303, 307, 308) and
-                self.headers.get("Location") is not None)
+                self.code in (301, 302, 303, 307, 308))
 
     def finish(self):
         data = b''.join(self.chunks)
@@ -519,9 +517,8 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
             # treat 302 the same as 303, and many servers use 302 for
             # compatibility with pre-HTTP/1.1 user agents which don't
             # understand the 303 status.
-            if self.code in (301, 302, 303):
-                if self.request.method not in self._SAFE_METHODS:
-                    new_request.method = "GET"
+            if self.code in (302, 303):
+                new_request.method = "GET"
                 new_request.body = None
                 for h in ["Content-Length", "Content-Type",
                           "Content-Encoding", "Transfer-Encoding"]:
diff --git a/tornado/wsgi.py b/tornado/wsgi.py
index beda0b9a..e1230da0 100644
--- a/tornado/wsgi.py
+++ b/tornado/wsgi.py
@@ -85,8 +85,10 @@ class WSGIApplication(web.Application):
 
 # WSGI has no facilities for flow control, so just return an already-done
 # Future when the interface requires it.
-_dummy_future = Future()
-_dummy_future.set_result(None)
+def _dummy_future():
+    f = Future()
+    f.set_result(None)
+    return f
 
 
 class _WSGIConnection(httputil.HTTPConnection):
@@ -118,7 +120,7 @@ class _WSGIConnection(httputil.HTTPConnection):
             self.write(chunk, callback)
         elif callback is not None:
             callback()
-        return _dummy_future
+        return _dummy_future()
 
     def write(self, chunk, callback=None):
         if self._expected_content_remaining is not None:
@@ -130,7 +132,7 @@ class _WSGIConnection(httputil.HTTPConnection):
         self._write_buffer.append(chunk)
         if callback is not None:
             callback()
-        return _dummy_future
+        return _dummy_future()
 
     def finish(self):
         if (self._expected_content_remaining is not None and


Test patch:
diff --git a/tornado/test/import_test.py b/tornado/test/import_test.py
index 0e981482..ecc569eb 100644
--- a/tornado/test/import_test.py
+++ b/tornado/test/import_test.py
@@ -1,50 +1,68 @@
 # flake8: noqa
 from __future__ import absolute_import, division, print_function
+
+import subprocess
+import sys
+
 from tornado.test.util import unittest
 
+_import_everything = b"""
+# The event loop is not fork-safe, and it's easy to initialize an asyncio.Future
+# at startup, which in turn creates the default event loop and prevents forking.
+# Explicitly disallow the default event loop so that an error will be raised
+# if something tries to touch it.
+try:
+    import asyncio
+except ImportError:
+    pass
+else:
+    asyncio.set_event_loop(None)
+
+import tornado.auth
+import tornado.autoreload
+import tornado.concurrent
+import tornado.escape
+import tornado.gen
+import tornado.http1connection
+import tornado.httpclient
+import tornado.httpserver
+import tornado.httputil
+import tornado.ioloop
+import tornado.iostream
+import tornado.locale
+import tornado.log
+import tornado.netutil
+import tornado.options
+import tornado.process
+import tornado.simple_httpclient
+import tornado.stack_context
+import tornado.tcpserver
+import tornado.tcpclient
+import tornado.template
+import tornado.testing
+import tornado.util
+import tornado.web
+import tornado.websocket
+import tornado.wsgi
+
+try:
+    import pycurl
+except ImportError:
+    pass
+else:
+    import tornado.curl_httpclient
+"""
+
 
 class ImportTest(unittest.TestCase):
     def test_import_everything(self):
-        # Some of our modules are not otherwise tested.  Import them
-        # all (unless they have external dependencies) here to at
-        # least ensure that there are no syntax errors.
-        import tornado.auth
-        import tornado.autoreload
-        import tornado.concurrent
-        import tornado.escape
-        import tornado.gen
-        import tornado.http1connection
-        import tornado.httpclient
-        import tornado.httpserver
-        import tornado.httputil
-        import tornado.ioloop
-        import tornado.iostream
-        import tornado.locale
-        import tornado.log
-        import tornado.netutil
-        import tornado.options
-        import tornado.process
-        import tornado.simple_httpclient
-        import tornado.stack_context
-        import tornado.tcpserver
-        import tornado.tcpclient
-        import tornado.template
-        import tornado.testing
-        import tornado.util
-        import tornado.web
-        import tornado.websocket
-        import tornado.wsgi
-
-    # for modules with dependencies, if those dependencies can be loaded,
-    # load them too.
-
-    def test_import_pycurl(self):
-        try:
-            import pycurl  # type: ignore
-        except ImportError:
-            pass
-        else:
-            import tornado.curl_httpclient
+        # Test that all Tornado modules can be imported without side effects,
+        # specifically without initializing the default asyncio event loop.
+        # Since we can't tell which modules may have already beein imported
+        # in our process, do it in a subprocess for a clean slate.
+        proc = subprocess.Popen([sys.executable], stdin=subprocess.PIPE)
+        proc.communicate(_import_everything)
+        self.assertEqual(proc.returncode, 0)
 
     def test_import_aliases(self):
         # Ensure we don't delete formerly-documented aliases accidentally.