]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Unify WSGI support with the HTTPConnection abstraction.
authorBen Darnell <ben@bendarnell.com>
Sun, 16 Mar 2014 02:30:23 +0000 (22:30 -0400)
committerBen Darnell <ben@bendarnell.com>
Sun, 16 Mar 2014 02:30:23 +0000 (22:30 -0400)
Deprecate WSGIApplication in favor of a WSGIAdapter that can wrap
an ordinary Application.

Remove the wsgi module's separate HTTPRequest variant and tornado.web's
wsgi special cases.

demos/appengine/blog.py
docs/wsgi.rst
tornado/httpserver.py
tornado/httputil.py
tornado/test/wsgi_test.py
tornado/web.py
tornado/wsgi.py

index 546586391737892210edb7fa5f7631fc5a1dcda8..20020dc07494a57d077db07afca39882a9cdacf5 100644 (file)
@@ -153,10 +153,12 @@ settings = {
     "ui_modules": {"Entry": EntryModule},
     "xsrf_cookies": True,
 }
-application = tornado.wsgi.WSGIApplication([
+application = tornado.web.Application([
     (r"/", HomeHandler),
     (r"/archive", ArchiveHandler),
     (r"/feed", FeedHandler),
     (r"/entry/([^/]+)", EntryHandler),
     (r"/compose", ComposeHandler),
 ], **settings)
+
+application = tornado.wsgi.WSGIAdapter(application)
index d0a72cdb0c6d2324d74eabdac250b9668739f3be..a54b7aaae7db729992d1019fb8370d4678445912 100644 (file)
@@ -3,17 +3,17 @@
 
 .. automodule:: tornado.wsgi
 
-   WSGIApplication
-   ---------------
+   Running Tornado apps on WSGI servers
+   ------------------------------------
 
-   .. autoclass:: WSGIApplication
+   .. autoclass:: WSGIAdapter
       :members:
 
-   .. autoclass:: HTTPRequest
+   .. autoclass:: WSGIApplication
       :members:
 
-   WSGIContainer
-   -------------
+   Running WSGI apps on Tornado servers
+   ------------------------------------
   
    .. autoclass:: WSGIContainer
       :members:
index 84b67cd56e91e2b293cf210a900aef6b62efe5db..db24d5ef934ca93336e2d0973dc64a60f4bec2cd 100644 (file)
@@ -202,15 +202,7 @@ class _ServerRequestAdapter(httputil.HTTPMessageDelegate):
         self.request.body = chunk
 
     def finish(self):
-        if self.request.method in ("POST", "PATCH", "PUT"):
-            httputil.parse_body_arguments(
-                self.request.headers.get("Content-Type", ""), self.request.body,
-                self.request.body_arguments, self.request.files,
-                self.request.headers)
-
-            for k, v in self.request.body_arguments.items():
-                self.request.arguments.setdefault(k, []).extend(v)
-
+        self.request._parse_body()
         self.server.request_callback(self.request)
 
 
index f36284674ed23a615cacb560b714d07382d41a91..a64a3ce36b22166989837b4e50ebb7e9991d69ca 100644 (file)
@@ -405,6 +405,16 @@ class HTTPServerRequest(object):
         except SSLError:
             return None
 
+    def _parse_body(self):
+        if self.method in ("POST", "PATCH", "PUT"):
+            parse_body_arguments(
+                self.headers.get("Content-Type", ""), self.body,
+                self.body_arguments, self.files,
+                self.headers)
+
+            for k, v in self.body_arguments.items():
+                self.arguments.setdefault(k, []).extend(v)
+
     def __repr__(self):
         attrs = ("protocol", "host", "method", "uri", "version", "remote_ip")
         args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
index 8dc356501b7ae494739d5bad4c2a9a8367319e4b..42d74b88281509893bfd10a70d63e50008c7ce18 100644 (file)
@@ -5,8 +5,8 @@ from tornado.escape import json_decode
 from tornado.test.httpserver_test import TypeCheckHandler
 from tornado.testing import AsyncHTTPTestCase
 from tornado.util import u
-from tornado.web import RequestHandler
-from tornado.wsgi import WSGIApplication, WSGIContainer
+from tornado.web import RequestHandler, Application
+from tornado.wsgi import WSGIApplication, WSGIContainer, WSGIAdapter
 
 
 class WSGIContainerTest(AsyncHTTPTestCase):
@@ -74,14 +74,27 @@ class WSGIConnectionTest(httpserver_test.HTTPConnectionTest):
         return WSGIContainer(validator(WSGIApplication(self.get_handlers())))
 
 
-def wrap_web_tests():
+def wrap_web_tests_application():
     result = {}
     for cls in web_test.wsgi_safe_tests:
-        class WSGIWrappedTest(cls):
+        class WSGIApplicationWrappedTest(cls):
             def get_app(self):
                 self.app = WSGIApplication(self.get_handlers(),
                                            **self.get_app_kwargs())
                 return WSGIContainer(validator(self.app))
-        result["WSGIWrapped_" + cls.__name__] = WSGIWrappedTest
+        result["WSGIApplication_" + cls.__name__] = WSGIApplicationWrappedTest
     return result
-globals().update(wrap_web_tests())
+globals().update(wrap_web_tests_application())
+
+
+def wrap_web_tests_adapter():
+    result = {}
+    for cls in web_test.wsgi_safe_tests:
+        class WSGIAdapterWrappedTest(cls):
+            def get_app(self):
+                self.app = Application(self.get_handlers(),
+                                       **self.get_app_kwargs())
+                return WSGIContainer(validator(WSGIAdapter(self.app)))
+        result["WSGIAdapter_" + cls.__name__] = WSGIAdapterWrappedTest
+    return result
+globals().update(wrap_web_tests_adapter())
index 8ff22ca9bcac72800172551f9c6d8d3d6a3f81d6..31da41bee3f350c9e7d1922bf4d72cdb6c21ef5e 100644 (file)
@@ -141,10 +141,7 @@ class RequestHandler(object):
                                                     application.ui_modules)
         self.ui["modules"] = self.ui["_tt_modules"]
         self.clear()
-        # Check since connection is not available in WSGI
-        if getattr(self.request, "connection", None):
-            self.request.connection.set_close_callback(
-                self.on_connection_close)
+        self.request.connection.set_close_callback(self.on_connection_close)
         self.initialize(**kwargs)
 
     def initialize(self):
@@ -772,13 +769,6 @@ class RequestHandler(object):
         if another flush occurs before the previous flush's callback
         has been run, the previous callback will be discarded.
         """
-        if self.application._wsgi:
-            # WSGI applications cannot usefully support flush, so just make
-            # it a no-op (and run the callback immediately).
-            if callback is not None:
-                callback()
-            return
-
         chunk = b"".join(self._write_buffer)
         self._write_buffer = []
         if not self._headers_written:
@@ -842,10 +832,9 @@ class RequestHandler(object):
             # are keepalive connections)
             self.request.connection.set_close_callback(None)
 
-        if not self.application._wsgi:
-            self.flush(include_footers=True)
-            self.request.finish()
-            self._log()
+        self.flush(include_footers=True)
+        self.request.finish()
+        self._log()
         self._finished = True
         self.on_finish()
         # Break up a reference cycle between this handler and the
@@ -1364,8 +1353,6 @@ def asynchronous(method):
     from tornado.ioloop import IOLoop
     @functools.wraps(method)
     def wrapper(self, *args, **kwargs):
-        if self.application._wsgi:
-            raise Exception("@asynchronous is not supported for WSGI apps")
         self._auto_finish = False
         with stack_context.ExceptionStackContext(
                 self._stack_context_handle_exception):
@@ -1488,7 +1475,7 @@ class Application(object):
 
     """
     def __init__(self, handlers=None, default_host="", transforms=None,
-                 wsgi=False, **settings):
+                 **settings):
         if transforms is None:
             self.transforms = []
             if settings.get("gzip"):
@@ -1505,7 +1492,6 @@ class Application(object):
                            'Template': TemplateModule,
                            }
         self.ui_methods = {}
-        self._wsgi = wsgi
         self._load_ui_modules(settings.get("ui_modules", {}))
         self._load_ui_methods(settings.get("ui_methods", {}))
         if self.settings.get("static_path"):
@@ -1531,7 +1517,7 @@ class Application(object):
             self.settings.setdefault('serve_traceback', True)
 
         # Automatically reload modified modules
-        if self.settings.get('autoreload') and not wsgi:
+        if self.settings.get('autoreload'):
             from tornado import autoreload
             autoreload.start()
 
index a803e714d1eb153edf69f43834f4969563c4eb0b..ff8170117b7fd0c3e25f964fc00ea9de449e87ce 100644 (file)
@@ -20,9 +20,9 @@ WSGI is the Python standard for web servers, and allows for interoperability
 between Tornado and other Python web frameworks and servers.  This module
 provides WSGI support in two ways:
 
-* `WSGIApplication` is a version of `tornado.web.Application` that can run
-  inside a WSGI server.  This is useful for running a Tornado app on another
-  HTTP server, such as Google App Engine.  See the `WSGIApplication` class
+* `WSGIAdapter` converts a `tornado.web.Application` to the WSGI application
+  interface.  This is useful for running a Tornado app on another
+  HTTP server, such as Google App Engine.  See the `WSGIAdapter` class
   documentation for limitations that apply.
 * `WSGIContainer` lets you run other WSGI applications and frameworks on the
   Tornado HTTP server.  For example, with this class you can mix Django
@@ -32,15 +32,13 @@ provides WSGI support in two ways:
 from __future__ import absolute_import, division, print_function, with_statement
 
 import sys
-import time
-import copy
 import tornado
 
 from tornado import escape
 from tornado import httputil
 from tornado.log import access_log
 from tornado import web
-from tornado.escape import native_str, parse_qs_bytes
+from tornado.escape import native_str
 from tornado.util import bytes_type, unicode_type
 
 try:
@@ -48,11 +46,6 @@ try:
 except ImportError:
     from cStringIO import StringIO as BytesIO  # python 2
 
-try:
-    import Cookie  # py2
-except ImportError:
-    import http.cookies as Cookie  # py3
-
 try:
     import urllib.parse as urllib_parse  # py3
 except ImportError:
@@ -83,11 +76,46 @@ else:
 class WSGIApplication(web.Application):
     """A WSGI equivalent of `tornado.web.Application`.
 
-    `WSGIApplication` is very similar to `tornado.web.Application`,
-    except no asynchronous methods are supported (since WSGI does not
-    support non-blocking requests properly). If you call
-    ``self.flush()`` or other asynchronous methods in your request
-    handlers running in a `WSGIApplication`, we throw an exception.
+    .. deprecated: 3.3::
+
+       Use a regular `.Application` and wrap it in `WSGIAdapter` instead.
+    """
+    def __init__(self, handlers=None, default_host="", **settings):
+        web.Application.__init__(self, handlers, default_host, transforms=[],
+                                 **settings)
+        self._adapter = WSGIAdapter(self)
+
+    def __call__(self, environ, start_response):
+        return self._adapter.__call__(environ, start_response)
+
+
+class _WSGIConnection(object):
+    def __init__(self, start_response):
+        self.start_response = start_response
+        self._write_buffer = []
+        self._finished = False
+
+    def set_close_callback(self, callback):
+        # WSGI has no facility for detecting a closed connection mid-request,
+        # so we can simply ignore the callback.
+        pass
+
+    def write_headers(self, start_line, headers):
+        self.start_response(
+            '%s %s' % (start_line.code, start_line.reason),
+            [(native_str(k), native_str(v)) for (k, v) in headers.get_all()])
+
+    def write(self, chunk, callback=None):
+        self._write_buffer.append(chunk)
+        if callback is not None:
+            callback()
+
+    def finish(self):
+        self._finished = True
+
+
+class WSGIAdapter(object):
+    """Converts a `tornado.web.Application` instance into a WSGI application.
 
     Example usage::
 
@@ -100,10 +128,11 @@ class WSGIApplication(web.Application):
                 self.write("Hello, world")
 
         if __name__ == "__main__":
-            application = tornado.wsgi.WSGIApplication([
+            application = tornado.web.Application([
                 (r"/", MainHandler),
             ])
-            server = wsgiref.simple_server.make_server('', 8888, application)
+            wsgi_app = tornado.wsgi.WSGIAdapter(application)
+            server = wsgiref.simple_server.make_server('', 8888, wsgi_app)
             server.serve_forever()
 
     See the `appengine demo
@@ -111,106 +140,53 @@ class WSGIApplication(web.Application):
     for an example of using this module to run a Tornado app on Google
     App Engine.
 
-    WSGI applications use the same `.RequestHandler` class, but not
-    ``@asynchronous`` methods or ``flush()``.  This means that it is
-    not possible to use `.AsyncHTTPClient`, or the `tornado.auth` or
-    `tornado.websocket` modules.
+    In WSGI mode asynchronous methods are not supported.  This means
+    that it is not possible to use `.AsyncHTTPClient`, or the
+    `tornado.auth` or `tornado.websocket` modules.
+
     """
-    def __init__(self, handlers=None, default_host="", **settings):
-        web.Application.__init__(self, handlers, default_host, transforms=[],
-                                 wsgi=True, **settings)
+    def __init__(self, application):
+        if isinstance(application, WSGIApplication):
+            self.application = lambda request: web.Application.__call__(
+                application, request)
+        else:
+            self.application = application
 
     def __call__(self, environ, start_response):
-        handler = web.Application.__call__(self, HTTPRequest(environ))
-        assert handler._finished
-        reason = handler._reason
-        status = str(handler._status_code) + " " + reason
-        headers = list(handler._headers.get_all())
-        if hasattr(handler, "_new_cookie"):
-            for cookie in handler._new_cookie.values():
-                headers.append(("Set-Cookie", cookie.OutputString(None)))
-        start_response(status,
-                       [(native_str(k), native_str(v)) for (k, v) in headers])
-        return handler._write_buffer
-
-
-class HTTPRequest(object):
-    """Mimics `tornado.httputil.HTTPServerRequest` for WSGI applications."""
-    def __init__(self, environ):
-        """Parses the given WSGI environment to construct the request."""
-        self.method = environ["REQUEST_METHOD"]
-        self.path = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", "")))
-        self.path += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", "")))
-        self.uri = self.path
-        self.arguments = {}
-        self.query_arguments = {}
-        self.body_arguments = {}
-        self.query = environ.get("QUERY_STRING", "")
-        if self.query:
-            self.uri += "?" + self.query
-            self.arguments = parse_qs_bytes(native_str(self.query),
-                                            keep_blank_values=True)
-            self.query_arguments = copy.deepcopy(self.arguments)
-        self.version = "HTTP/1.1"
-        self.headers = httputil.HTTPHeaders()
+        method = environ["REQUEST_METHOD"]
+        uri = urllib_parse.quote(from_wsgi_str(environ.get("SCRIPT_NAME", "")))
+        uri += urllib_parse.quote(from_wsgi_str(environ.get("PATH_INFO", "")))
+        if environ.get("QUERY_STRING"):
+            uri += "?" + environ["QUERY_STRING"]
+        headers = httputil.HTTPHeaders()
         if environ.get("CONTENT_TYPE"):
-            self.headers["Content-Type"] = environ["CONTENT_TYPE"]
+            headers["Content-Type"] = environ["CONTENT_TYPE"]
         if environ.get("CONTENT_LENGTH"):
-            self.headers["Content-Length"] = environ["CONTENT_LENGTH"]
+            headers["Content-Length"] = environ["CONTENT_LENGTH"]
         for key in environ:
             if key.startswith("HTTP_"):
-                self.headers[key[5:].replace("_", "-")] = environ[key]
-        if self.headers.get("Content-Length"):
-            self.body = environ["wsgi.input"].read(
-                int(self.headers["Content-Length"]))
+                headers[key[5:].replace("_", "-")] = environ[key]
+        if headers.get("Content-Length"):
+            body = environ["wsgi.input"].read(
+                int(headers["Content-Length"]))
         else:
-            self.body = ""
-        self.protocol = environ["wsgi.url_scheme"]
-        self.remote_ip = environ.get("REMOTE_ADDR", "")
+            body = ""
+        protocol = environ["wsgi.url_scheme"]
+        remote_ip = environ.get("REMOTE_ADDR", "")
         if environ.get("HTTP_HOST"):
-            self.host = environ["HTTP_HOST"]
+            host = environ["HTTP_HOST"]
         else:
-            self.host = environ["SERVER_NAME"]
-
-        # Parse request body
-        self.files = {}
-        httputil.parse_body_arguments(self.headers.get("Content-Type", ""),
-                                      self.body, self.body_arguments,
-                                      self.files, self.headers)
-
-        for k, v in self.body_arguments.items():
-            self.arguments.setdefault(k, []).extend(v)
-
-        self._start_time = time.time()
-        self._finish_time = None
-
-    def supports_http_1_1(self):
-        """Returns True if this request supports HTTP/1.1 semantics"""
-        return self.version == "HTTP/1.1"
-
-    @property
-    def cookies(self):
-        """A dictionary of Cookie.Morsel objects."""
-        if not hasattr(self, "_cookies"):
-            self._cookies = Cookie.SimpleCookie()
-            if "Cookie" in self.headers:
-                try:
-                    self._cookies.load(
-                        native_str(self.headers["Cookie"]))
-                except Exception:
-                    self._cookies = None
-        return self._cookies
-
-    def full_url(self):
-        """Reconstructs the full URL for this request."""
-        return self.protocol + "://" + self.host + self.uri
-
-    def request_time(self):
-        """Returns the amount of time it took for this request to execute."""
-        if self._finish_time is None:
-            return time.time() - self._start_time
-        else:
-            return self._finish_time - self._start_time
+            host = environ["SERVER_NAME"]
+        connection = _WSGIConnection(start_response)
+        request = httputil.HTTPServerRequest(
+            method, uri, "HTTP/1.1",
+            headers=headers, body=body, remote_ip=remote_ip, protocol=protocol,
+            host=host, connection=connection)
+        request._parse_body()
+        self.application(request)
+        if not connection._finished:
+            raise Exception("request did not finish synchronously")
+        return connection._write_buffer
 
 
 class WSGIContainer(object):
@@ -338,3 +314,6 @@ class WSGIContainer(object):
         summary = request.method + " " + request.uri + " (" + \
             request.remote_ip + ")"
         log_method("%d %s %.2fms", status_code, summary, request_time)
+
+
+HTTPRequest = httputil.HTTPServerRequest