From 6c7eac6c835cdcc8a5c0809b247be60703f6efc7 Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sat, 15 Mar 2014 22:30:23 -0400 Subject: [PATCH] Unify WSGI support with the HTTPConnection abstraction. 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 | 4 +- docs/wsgi.rst | 14 +-- tornado/httpserver.py | 10 +- tornado/httputil.py | 10 ++ tornado/test/wsgi_test.py | 25 +++-- tornado/web.py | 26 ++--- tornado/wsgi.py | 195 +++++++++++++++++--------------------- 7 files changed, 133 insertions(+), 151 deletions(-) diff --git a/demos/appengine/blog.py b/demos/appengine/blog.py index 54658639..20020dc0 100644 --- a/demos/appengine/blog.py +++ b/demos/appengine/blog.py @@ -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) diff --git a/docs/wsgi.rst b/docs/wsgi.rst index d0a72cdb..a54b7aaa 100644 --- a/docs/wsgi.rst +++ b/docs/wsgi.rst @@ -3,17 +3,17 @@ .. automodule:: tornado.wsgi - WSGIApplication - --------------- + Running Tornado apps on WSGI servers + ------------------------------------ + + .. autoclass:: WSGIAdapter + :members: .. autoclass:: WSGIApplication :members: - .. autoclass:: HTTPRequest - :members: - - WSGIContainer - ------------- + Running WSGI apps on Tornado servers + ------------------------------------ .. autoclass:: WSGIContainer :members: diff --git a/tornado/httpserver.py b/tornado/httpserver.py index 84b67cd5..db24d5ef 100644 --- a/tornado/httpserver.py +++ b/tornado/httpserver.py @@ -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) diff --git a/tornado/httputil.py b/tornado/httputil.py index f3628467..a64a3ce3 100644 --- a/tornado/httputil.py +++ b/tornado/httputil.py @@ -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]) diff --git a/tornado/test/wsgi_test.py b/tornado/test/wsgi_test.py index 8dc35650..42d74b88 100644 --- a/tornado/test/wsgi_test.py +++ b/tornado/test/wsgi_test.py @@ -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()) diff --git a/tornado/web.py b/tornado/web.py index 8ff22ca9..31da41be 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -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() diff --git a/tornado/wsgi.py b/tornado/wsgi.py index a803e714..ff817011 100644 --- a/tornado/wsgi.py +++ b/tornado/wsgi.py @@ -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