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.
This commit is contained in:
parent
361b612663
commit
6c7eac6c83
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
195
tornado/wsgi.py
195
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
|
||||
|
|
Loading…
Reference in New Issue