1987 lines
67 KiB
Python
Executable file
1987 lines
67 KiB
Python
Executable file
# -*- coding: utf-8 -*-
|
|
"""
|
|
webapp2
|
|
=======
|
|
|
|
Taking Google App Engine's webapp to the next level!
|
|
|
|
:copyright: 2011 by tipfy.org.
|
|
:license: Apache Sotware License, see LICENSE for details.
|
|
"""
|
|
from __future__ import with_statement
|
|
|
|
import cgi
|
|
import inspect
|
|
import logging
|
|
import os
|
|
import re
|
|
import sys
|
|
import threading
|
|
import traceback
|
|
import urllib
|
|
import urlparse
|
|
from wsgiref import handlers
|
|
|
|
import webob
|
|
from webob import exc
|
|
|
|
_webapp = _webapp_util = _local = None
|
|
|
|
try: # pragma: no cover
|
|
# WebOb < 1.0 (App Engine Python 2.5).
|
|
from webob.statusreasons import status_reasons
|
|
from webob.headerdict import HeaderDict as BaseResponseHeaders
|
|
except ImportError: # pragma: no cover
|
|
# WebOb >= 1.0.
|
|
from webob.util import status_reasons
|
|
from webob.headers import ResponseHeaders as BaseResponseHeaders
|
|
|
|
# google.appengine.ext.webapp imports webapp2 in the
|
|
# App Engine Python 2.7 runtime.
|
|
if os.environ.get('APPENGINE_RUNTIME') != 'python27': # pragma: no cover
|
|
try:
|
|
from google.appengine.ext import webapp as _webapp
|
|
except ImportError: # pragma: no cover
|
|
# Running webapp2 outside of GAE.
|
|
pass
|
|
|
|
try: # pragma: no cover
|
|
# Thread-local variables container.
|
|
from webapp2_extras import local
|
|
_local = local.Local()
|
|
except ImportError: # pragma: no cover
|
|
logging.warning("webapp2_extras.local is not available "
|
|
"so webapp2 won't be thread-safe!")
|
|
|
|
|
|
__version_info__ = (2, 5, 1)
|
|
__version__ = '.'.join(str(n) for n in __version_info__)
|
|
|
|
#: Base HTTP exception, set here as public interface.
|
|
HTTPException = exc.HTTPException
|
|
|
|
#: Regex for route definitions.
|
|
_route_re = re.compile(r"""
|
|
\< # The exact character "<"
|
|
([a-zA-Z_]\w*)? # The optional variable name
|
|
(?:\:([^\>]*))? # The optional :regex part
|
|
\> # The exact character ">"
|
|
""", re.VERBOSE)
|
|
#: Regex extract charset from environ.
|
|
_charset_re = re.compile(r';\s*charset=([^;]*)', re.I)
|
|
|
|
#: To show exceptions in debug mode.
|
|
_debug_template = """<html>
|
|
<head>
|
|
<title>Internal Server Error</title>
|
|
<style>
|
|
body {
|
|
padding: 20px;
|
|
font-family: arial, sans-serif;
|
|
font-size: 14px;
|
|
}
|
|
pre {
|
|
background: #F2F2F2;
|
|
padding: 10px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Internal Server Error</h1>
|
|
<p>The server has either erred or is incapable of performing
|
|
the requested operation.</p>
|
|
<pre>%s</pre>
|
|
</body>
|
|
</html>"""
|
|
|
|
# Set same default messages from webapp plus missing ones.
|
|
_webapp_status_reasons = {
|
|
203: 'Non-Authoritative Information',
|
|
302: 'Moved Temporarily',
|
|
306: 'Unused',
|
|
408: 'Request Time-out',
|
|
414: 'Request-URI Too Large',
|
|
504: 'Gateway Time-out',
|
|
505: 'HTTP Version not supported',
|
|
}
|
|
status_reasons.update(_webapp_status_reasons)
|
|
for code, message in _webapp_status_reasons.iteritems():
|
|
cls = exc.status_map.get(code)
|
|
if cls:
|
|
cls.title = message
|
|
|
|
|
|
class Request(webob.Request):
|
|
"""Abstraction for an HTTP request.
|
|
|
|
Most extra methods and attributes are ported from webapp. Check the
|
|
`WebOb documentation <WebOb>`_ for the ones not listed here.
|
|
"""
|
|
|
|
#: A reference to the active :class:`WSGIApplication` instance.
|
|
app = None
|
|
#: A reference to the active :class:`Response` instance.
|
|
response = None
|
|
#: A reference to the matched :class:`Route`.
|
|
route = None
|
|
#: The matched route positional arguments.
|
|
route_args = None
|
|
#: The matched route keyword arguments.
|
|
route_kwargs = None
|
|
#: A dictionary to register objects used during the request lifetime.
|
|
registry = None
|
|
# Attributes from webapp.
|
|
request_body_tempfile_limit = 0
|
|
uri = property(lambda self: self.url)
|
|
query = property(lambda self: self.query_string)
|
|
|
|
def __init__(self, environ, *args, **kwargs):
|
|
"""Constructs a Request object from a WSGI environment.
|
|
|
|
:param environ:
|
|
A WSGI-compliant environment dictionary.
|
|
"""
|
|
if kwargs.get('charset') is None and not hasattr(webob, '__version__'):
|
|
# webob 0.9 didn't have a __version__ attribute and also defaulted
|
|
# to None rather than UTF-8 if no charset was provided. Providing a
|
|
# default charset is required for backwards compatibility.
|
|
match = _charset_re.search(environ.get('CONTENT_TYPE', ''))
|
|
if match:
|
|
charset = match.group(1).lower().strip().strip('"').strip()
|
|
else:
|
|
charset = 'utf-8'
|
|
kwargs['charset'] = charset
|
|
|
|
super(Request, self).__init__(environ, *args, **kwargs)
|
|
self.registry = {}
|
|
|
|
def get(self, argument_name, default_value='', allow_multiple=False):
|
|
"""Returns the query or POST argument with the given name.
|
|
|
|
We parse the query string and POST payload lazily, so this will be a
|
|
slower operation on the first call.
|
|
|
|
:param argument_name:
|
|
The name of the query or POST argument.
|
|
:param default_value:
|
|
The value to return if the given argument is not present.
|
|
:param allow_multiple:
|
|
Return a list of values with the given name (deprecated).
|
|
:returns:
|
|
If allow_multiple is False (which it is by default), we return
|
|
the first value with the given name given in the request. If it
|
|
is True, we always return a list.
|
|
"""
|
|
param_value = self.get_all(argument_name)
|
|
if allow_multiple:
|
|
logging.warning('allow_multiple is a deprecated param. '
|
|
'Please use the Request.get_all() method instead.')
|
|
|
|
if len(param_value) > 0:
|
|
if allow_multiple:
|
|
return param_value
|
|
|
|
return param_value[0]
|
|
else:
|
|
if allow_multiple and not default_value:
|
|
return []
|
|
|
|
return default_value
|
|
|
|
def get_all(self, argument_name, default_value=None):
|
|
"""Returns a list of query or POST arguments with the given name.
|
|
|
|
We parse the query string and POST payload lazily, so this will be a
|
|
slower operation on the first call.
|
|
|
|
:param argument_name:
|
|
The name of the query or POST argument.
|
|
:param default_value:
|
|
The value to return if the given argument is not present,
|
|
None may not be used as a default, if it is then an empty
|
|
list will be returned instead.
|
|
:returns:
|
|
A (possibly empty) list of values.
|
|
"""
|
|
if self.charset:
|
|
argument_name = argument_name.encode(self.charset)
|
|
|
|
if default_value is None:
|
|
default_value = []
|
|
|
|
param_value = self.params.getall(argument_name)
|
|
|
|
if param_value is None or len(param_value) == 0:
|
|
return default_value
|
|
|
|
for i in xrange(len(param_value)):
|
|
if isinstance(param_value[i], cgi.FieldStorage):
|
|
param_value[i] = param_value[i].value
|
|
|
|
return param_value
|
|
|
|
def arguments(self):
|
|
"""Returns a list of the arguments provided in the query and/or POST.
|
|
|
|
The return value is a list of strings.
|
|
"""
|
|
return list(set(self.params.keys()))
|
|
|
|
def get_range(self, name, min_value=None, max_value=None, default=0):
|
|
"""Parses the given int argument, limiting it to the given range.
|
|
|
|
:param name:
|
|
The name of the argument.
|
|
:param min_value:
|
|
The minimum int value of the argument (if any).
|
|
:param max_value:
|
|
The maximum int value of the argument (if any).
|
|
:param default:
|
|
The default value of the argument if it is not given.
|
|
:returns:
|
|
An int within the given range for the argument.
|
|
"""
|
|
value = self.get(name, default)
|
|
if value is None:
|
|
return value
|
|
|
|
try:
|
|
value = int(value)
|
|
except ValueError:
|
|
value = default
|
|
if value is not None:
|
|
if max_value is not None:
|
|
value = min(value, max_value)
|
|
|
|
if min_value is not None:
|
|
value = max(value, min_value)
|
|
|
|
return value
|
|
|
|
@classmethod
|
|
def blank(cls, path, environ=None, base_url=None,
|
|
headers=None, **kwargs): # pragma: no cover
|
|
"""Adds parameters compatible with WebOb >= 1.0: POST and **kwargs."""
|
|
try:
|
|
return super(Request, cls).blank(path, environ=environ,
|
|
base_url=base_url,
|
|
headers=headers, **kwargs)
|
|
except TypeError:
|
|
if not kwargs:
|
|
raise
|
|
|
|
data = kwargs.pop('POST', None)
|
|
if data is not None:
|
|
from cStringIO import StringIO
|
|
environ = environ or {}
|
|
environ['REQUEST_METHOD'] = 'POST'
|
|
if hasattr(data, 'items'):
|
|
data = data.items()
|
|
if not isinstance(data, str):
|
|
data = urllib.urlencode(data)
|
|
environ['wsgi.input'] = StringIO(data)
|
|
environ['webob.is_body_seekable'] = True
|
|
environ['CONTENT_LENGTH'] = str(len(data))
|
|
environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
|
|
|
base = super(Request, cls).blank(path, environ=environ,
|
|
base_url=base_url, headers=headers)
|
|
if kwargs:
|
|
obj = cls(base.environ, **kwargs)
|
|
obj.headers.update(base.headers)
|
|
return obj
|
|
else:
|
|
return base
|
|
|
|
|
|
class ResponseHeaders(BaseResponseHeaders):
|
|
"""Implements methods from ``wsgiref.headers.Headers``, used by webapp."""
|
|
|
|
get_all = BaseResponseHeaders.getall
|
|
|
|
def add_header(self, _name, _value, **_params):
|
|
"""Extended header setting.
|
|
|
|
_name is the header field to add. keyword arguments can be used to set
|
|
additional parameters for the header field, with underscores converted
|
|
to dashes. Normally the parameter will be added as key="value" unless
|
|
value is None, in which case only the key will be added.
|
|
|
|
Example::
|
|
|
|
h.add_header('content-disposition', 'attachment',
|
|
filename='bud.gif')
|
|
|
|
Note that unlike the corresponding 'email.message' method, this does
|
|
*not* handle '(charset, language, value)' tuples: all values must be
|
|
strings or None.
|
|
"""
|
|
parts = []
|
|
if _value is not None:
|
|
parts.append(_value)
|
|
|
|
for k, v in _params.items():
|
|
k = k.replace('_', '-')
|
|
if v is not None and len(v) > 0:
|
|
v = v.replace('\\', '\\\\').replace('"', r'\"')
|
|
parts.append('%s="%s"' % (k, v))
|
|
else:
|
|
parts.append(k)
|
|
|
|
self.add(_name, '; '.join(parts))
|
|
|
|
def __str__(self):
|
|
"""Returns the formatted headers ready for HTTP transmission."""
|
|
return '\r\n'.join(['%s: %s' % v for v in self.items()] + ['', ''])
|
|
|
|
|
|
class Response(webob.Response):
|
|
"""Abstraction for an HTTP response.
|
|
|
|
Most extra methods and attributes are ported from webapp. Check the
|
|
`WebOb documentation <WebOb>`_ for the ones not listed here.
|
|
|
|
Differences from webapp.Response:
|
|
|
|
- ``out`` is not a ``StringIO.StringIO`` instance. Instead it is the
|
|
response itself, as it has the method ``write()``.
|
|
- As in WebOb, ``status`` is the code plus message, e.g., '200 OK', while
|
|
in webapp it is the integer code. The status code as an integer is
|
|
available in ``status_int``, and the status message is available in
|
|
``status_message``.
|
|
- ``response.headers`` raises an exception when a key that doesn't exist
|
|
is accessed or deleted, differently from ``wsgiref.headers.Headers``.
|
|
"""
|
|
|
|
#: Default charset as in webapp.
|
|
default_charset = 'utf-8'
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Constructs a response with the default settings."""
|
|
super(Response, self).__init__(*args, **kwargs)
|
|
self.headers['Cache-Control'] = 'no-cache'
|
|
|
|
@property
|
|
def out(self):
|
|
"""A reference to the Response instance itself, for compatibility with
|
|
webapp only: webapp uses `Response.out.write()`, so we point `out` to
|
|
`self` and it will use `Response.write()`.
|
|
"""
|
|
return self
|
|
|
|
def write(self, text):
|
|
"""Appends a text to the response body."""
|
|
# webapp uses StringIO as Response.out, so we need to convert anything
|
|
# that is not str or unicode to string to keep same behavior.
|
|
if not isinstance(text, basestring):
|
|
text = unicode(text)
|
|
|
|
if isinstance(text, unicode) and not self.charset:
|
|
self.charset = self.default_charset
|
|
|
|
super(Response, self).write(text)
|
|
|
|
def _set_status(self, value):
|
|
"""The status string, including code and message."""
|
|
message = None
|
|
# Accept long because urlfetch in App Engine returns codes as longs.
|
|
if isinstance(value, (int, long)):
|
|
code = int(value)
|
|
else:
|
|
if isinstance(value, unicode):
|
|
# Status messages have to be ASCII safe, so this is OK.
|
|
value = str(value)
|
|
|
|
if not isinstance(value, str):
|
|
raise TypeError(
|
|
'You must set status to a string or integer (not %s)' %
|
|
type(value))
|
|
|
|
parts = value.split(' ', 1)
|
|
code = int(parts[0])
|
|
if len(parts) == 2:
|
|
message = parts[1]
|
|
|
|
message = message or Response.http_status_message(code)
|
|
self._status = '%d %s' % (code, message)
|
|
|
|
def _get_status(self):
|
|
return self._status
|
|
|
|
status = property(_get_status, _set_status, doc=_set_status.__doc__)
|
|
|
|
def set_status(self, code, message=None):
|
|
"""Sets the HTTP status code of this response.
|
|
|
|
:param code:
|
|
The HTTP status string to use
|
|
:param message:
|
|
A status string. If none is given, uses the default from the
|
|
HTTP/1.1 specification.
|
|
"""
|
|
if message:
|
|
self.status = '%d %s' % (code, message)
|
|
else:
|
|
self.status = code
|
|
|
|
def _get_status_message(self):
|
|
"""The response status message, as a string."""
|
|
return self.status.split(' ', 1)[1]
|
|
|
|
def _set_status_message(self, message):
|
|
self.status = '%d %s' % (self.status_int, message)
|
|
|
|
status_message = property(_get_status_message, _set_status_message,
|
|
doc=_get_status_message.__doc__)
|
|
|
|
def _get_headers(self):
|
|
"""The headers as a dictionary-like object."""
|
|
if self._headers is None:
|
|
self._headers = ResponseHeaders.view_list(self.headerlist)
|
|
|
|
return self._headers
|
|
|
|
def _set_headers(self, value):
|
|
if hasattr(value, 'items'):
|
|
value = value.items()
|
|
elif not isinstance(value, list):
|
|
raise TypeError('Response headers must be a list or dictionary.')
|
|
|
|
self.headerlist = value
|
|
self._headers = None
|
|
|
|
headers = property(_get_headers, _set_headers, doc=_get_headers.__doc__)
|
|
|
|
def has_error(self):
|
|
"""Indicates whether the response was an error response."""
|
|
return self.status_int >= 400
|
|
|
|
def clear(self):
|
|
"""Clears all data written to the output stream so that it is empty."""
|
|
self.body = ''
|
|
|
|
def wsgi_write(self, start_response):
|
|
"""Writes this response using using the given WSGI function.
|
|
|
|
This is only here for compatibility with ``webapp.WSGIApplication``.
|
|
|
|
:param start_response:
|
|
The WSGI-compatible start_response function.
|
|
"""
|
|
if (self.headers.get('Cache-Control') == 'no-cache' and
|
|
not self.headers.get('Expires')):
|
|
self.headers['Expires'] = 'Fri, 01 Jan 1990 00:00:00 GMT'
|
|
self.headers['Content-Length'] = str(len(self.body))
|
|
|
|
write = start_response(self.status, self.headerlist)
|
|
write(self.body)
|
|
|
|
@staticmethod
|
|
def http_status_message(code):
|
|
"""Returns the default HTTP status message for the given code.
|
|
|
|
:param code:
|
|
The HTTP code for which we want a message.
|
|
"""
|
|
message = status_reasons.get(code)
|
|
if not message:
|
|
raise KeyError('Invalid HTTP status code: %d' % code)
|
|
|
|
return message
|
|
|
|
|
|
class RequestHandler(object):
|
|
"""Base HTTP request handler.
|
|
|
|
Implements most of ``webapp.RequestHandler`` interface.
|
|
"""
|
|
|
|
#: A :class:`Request` instance.
|
|
request = None
|
|
#: A :class:`Response` instance.
|
|
response = None
|
|
#: A :class:`WSGIApplication` instance.
|
|
app = None
|
|
|
|
def __init__(self, request=None, response=None):
|
|
"""Initializes this request handler with the given WSGI application,
|
|
Request and Response.
|
|
|
|
When instantiated by ``webapp.WSGIApplication``, request and response
|
|
are not set on instantiation. Instead, initialize() is called right
|
|
after the handler is created to set them.
|
|
|
|
Also in webapp dispatching is done by the WSGI app, while webapp2
|
|
does it here to allow more flexibility in extended classes: handlers
|
|
can wrap :meth:`dispatch` to check for conditions before executing the
|
|
requested method and/or post-process the response.
|
|
|
|
.. note::
|
|
Parameters are optional only to support webapp's constructor which
|
|
doesn't take any arguments. Consider them as required.
|
|
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
:param response:
|
|
A :class:`Response` instance.
|
|
"""
|
|
self.initialize(request, response)
|
|
|
|
def initialize(self, request, response):
|
|
"""Initializes this request handler with the given WSGI application,
|
|
Request and Response.
|
|
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
:param response:
|
|
A :class:`Response` instance.
|
|
"""
|
|
self.request = request
|
|
self.response = response
|
|
self.app = WSGIApplication.active_instance
|
|
|
|
def dispatch(self):
|
|
"""Dispatches the request.
|
|
|
|
This will first check if there's a handler_method defined in the
|
|
matched route, and if not it'll use the method correspondent to the
|
|
request method (``get()``, ``post()`` etc).
|
|
"""
|
|
request = self.request
|
|
method_name = request.route.handler_method
|
|
if not method_name:
|
|
method_name = _normalize_handler_method(request.method)
|
|
|
|
method = getattr(self, method_name, None)
|
|
if method is None:
|
|
# 405 Method Not Allowed.
|
|
# The response MUST include an Allow header containing a
|
|
# list of valid methods for the requested resource.
|
|
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6
|
|
valid = ', '.join(_get_handler_methods(self))
|
|
self.abort(405, headers=[('Allow', valid)])
|
|
|
|
# The handler only receives *args if no named variables are set.
|
|
args, kwargs = request.route_args, request.route_kwargs
|
|
if kwargs:
|
|
args = ()
|
|
|
|
try:
|
|
return method(*args, **kwargs)
|
|
except Exception, e:
|
|
return self.handle_exception(e, self.app.debug)
|
|
|
|
def error(self, code):
|
|
"""Clears the response and sets the given HTTP status code.
|
|
|
|
This doesn't stop code execution; for this, use :meth:`abort`.
|
|
|
|
:param code:
|
|
HTTP status error code (e.g., 501).
|
|
"""
|
|
self.response.status = code
|
|
self.response.clear()
|
|
|
|
def abort(self, code, *args, **kwargs):
|
|
"""Raises an :class:`HTTPException`.
|
|
|
|
This stops code execution, leaving the HTTP exception to be handled
|
|
by an exception handler.
|
|
|
|
:param code:
|
|
HTTP status code (e.g., 404).
|
|
:param args:
|
|
Positional arguments to be passed to the exception class.
|
|
:param kwargs:
|
|
Keyword arguments to be passed to the exception class.
|
|
"""
|
|
abort(code, *args, **kwargs)
|
|
|
|
def redirect(self, uri, permanent=False, abort=False, code=None,
|
|
body=None):
|
|
"""Issues an HTTP redirect to the given relative URI.
|
|
|
|
The arguments are described in :func:`redirect`.
|
|
"""
|
|
return redirect(uri, permanent=permanent, abort=abort, code=code,
|
|
body=body, request=self.request,
|
|
response=self.response)
|
|
|
|
def redirect_to(self, _name, _permanent=False, _abort=False, _code=None,
|
|
_body=None, *args, **kwargs):
|
|
"""Convenience method mixing :meth:`redirect` and :meth:`uri_for`.
|
|
|
|
The arguments are described in :func:`redirect` and :func:`uri_for`.
|
|
"""
|
|
uri = self.uri_for(_name, *args, **kwargs)
|
|
return self.redirect(uri, permanent=_permanent, abort=_abort,
|
|
code=_code, body=_body)
|
|
|
|
def uri_for(self, _name, *args, **kwargs):
|
|
"""Returns a URI for a named :class:`Route`.
|
|
|
|
.. seealso:: :meth:`Router.build`.
|
|
"""
|
|
return self.app.router.build(self.request, _name, args, kwargs)
|
|
# Alias.
|
|
url_for = uri_for
|
|
|
|
def handle_exception(self, exception, debug):
|
|
"""Called if this handler throws an exception during execution.
|
|
|
|
The default behavior is to re-raise the exception to be handled by
|
|
:meth:`WSGIApplication.handle_exception`.
|
|
|
|
:param exception:
|
|
The exception that was thrown.
|
|
:param debug_mode:
|
|
True if the web application is running in debug mode.
|
|
"""
|
|
raise
|
|
|
|
|
|
class RedirectHandler(RequestHandler):
|
|
"""Redirects to the given URI for all GET requests.
|
|
|
|
This is intended to be used when defining URI routes. You must provide at
|
|
least the keyword argument *url* in the route default values. Example::
|
|
|
|
def get_redirect_url(handler, *args, **kwargs):
|
|
return handler.uri_for('new-route-name')
|
|
|
|
app = WSGIApplication([
|
|
Route('/old-url', RedirectHandler, defaults={'_uri': '/new-url'}),
|
|
Route('/other-old-url', RedirectHandler, defaults={
|
|
'_uri': get_redirect_url}),
|
|
])
|
|
|
|
Based on idea from `Tornado`_.
|
|
"""
|
|
|
|
def get(self, *args, **kwargs):
|
|
"""Performs a redirect.
|
|
|
|
Two keyword arguments can be passed through the URI route:
|
|
|
|
- **_uri**: A URI string or a callable that returns a URI. The callable
|
|
is called passing ``(handler, *args, **kwargs)`` as arguments.
|
|
- **_code**: The redirect status code. Default is 301 (permanent
|
|
redirect).
|
|
"""
|
|
uri = kwargs.pop('_uri', '/')
|
|
permanent = kwargs.pop('_permanent', True)
|
|
code = kwargs.pop('_code', None)
|
|
|
|
func = getattr(uri, '__call__', None)
|
|
if func:
|
|
uri = func(self, *args, **kwargs)
|
|
|
|
self.redirect(uri, permanent=permanent, code=code)
|
|
|
|
|
|
class cached_property(object):
|
|
"""A decorator that converts a function into a lazy property.
|
|
|
|
The function wrapped is called the first time to retrieve the result
|
|
and then that calculated result is used the next time you access
|
|
the value::
|
|
|
|
class Foo(object):
|
|
|
|
@cached_property
|
|
def foo(self):
|
|
# calculate something important here
|
|
return 42
|
|
|
|
The class has to have a `__dict__` in order for this property to
|
|
work.
|
|
|
|
.. note:: Implementation detail: this property is implemented as non-data
|
|
descriptor. non-data descriptors are only invoked if there is
|
|
no entry with the same name in the instance's __dict__.
|
|
this allows us to completely get rid of the access function call
|
|
overhead. If one choses to invoke __get__ by hand the property
|
|
will still work as expected because the lookup logic is replicated
|
|
in __get__ for manual invocation.
|
|
|
|
This class was ported from `Werkzeug`_ and `Flask`_.
|
|
"""
|
|
|
|
_default_value = object()
|
|
|
|
def __init__(self, func, name=None, doc=None):
|
|
self.__name__ = name or func.__name__
|
|
self.__module__ = func.__module__
|
|
self.__doc__ = doc or func.__doc__
|
|
self.func = func
|
|
self.lock = threading.RLock()
|
|
|
|
def __get__(self, obj, type=None):
|
|
if obj is None:
|
|
return self
|
|
|
|
with self.lock:
|
|
value = obj.__dict__.get(self.__name__, self._default_value)
|
|
if value is self._default_value:
|
|
value = self.func(obj)
|
|
obj.__dict__[self.__name__] = value
|
|
|
|
return value
|
|
|
|
|
|
class BaseRoute(object):
|
|
"""Interface for URI routes."""
|
|
|
|
#: The regex template.
|
|
template = None
|
|
#: Route name, used to build URIs.
|
|
name = None
|
|
#: True if this route is only used for URI generation and never matches.
|
|
build_only = False
|
|
#: The handler or string in dotted notation to be lazily imported.
|
|
handler = None
|
|
#: The custom handler method, if handler is a class.
|
|
handler_method = None
|
|
#: The handler, imported and ready for dispatching.
|
|
handler_adapter = None
|
|
|
|
def __init__(self, template, handler=None, name=None, build_only=False):
|
|
"""Initializes this route.
|
|
|
|
:param template:
|
|
A regex to be matched.
|
|
:param handler:
|
|
A callable or string in dotted notation to be lazily imported,
|
|
e.g., ``'my.module.MyHandler'`` or ``'my.module.my_function'``.
|
|
:param name:
|
|
The name of this route, used to build URIs based on it.
|
|
:param build_only:
|
|
If True, this route never matches and is used only to build URIs.
|
|
"""
|
|
if build_only and name is None:
|
|
raise ValueError(
|
|
"Route %r is build_only but doesn't have a name." % self)
|
|
|
|
self.template = template
|
|
self.handler = handler
|
|
self.name = name
|
|
self.build_only = build_only
|
|
|
|
def match(self, request):
|
|
"""Matches all routes against a request object.
|
|
|
|
The first one that matches is returned.
|
|
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
:returns:
|
|
A tuple ``(route, args, kwargs)`` if a route matched, or None.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def build(self, request, args, kwargs):
|
|
"""Returns a URI for this route.
|
|
|
|
:param request:
|
|
The current :class:`Request` object.
|
|
:param args:
|
|
Tuple of positional arguments to build the URI.
|
|
:param kwargs:
|
|
Dictionary of keyword arguments to build the URI.
|
|
:returns:
|
|
An absolute or relative URI.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_routes(self):
|
|
"""Generator to get all routes from a route.
|
|
|
|
:yields:
|
|
This route or all nested routes that it contains.
|
|
"""
|
|
yield self
|
|
|
|
def get_match_routes(self):
|
|
"""Generator to get all routes that can be matched from a route.
|
|
|
|
Match routes must implement :meth:`match`.
|
|
|
|
:yields:
|
|
This route or all nested routes that can be matched.
|
|
"""
|
|
if not self.build_only:
|
|
yield self
|
|
|
|
def get_build_routes(self):
|
|
"""Generator to get all routes that can be built from a route.
|
|
|
|
Build routes must implement :meth:`build`.
|
|
|
|
:yields:
|
|
A tuple ``(name, route)`` for all nested routes that can be built.
|
|
"""
|
|
if self.name is not None:
|
|
yield self.name, self
|
|
|
|
|
|
class SimpleRoute(BaseRoute):
|
|
"""A route that is compatible with webapp's routing mechanism.
|
|
|
|
URI building is not implemented as webapp has rudimentar support for it,
|
|
and this is the most unknown webapp feature anyway.
|
|
"""
|
|
|
|
@cached_property
|
|
def regex(self):
|
|
"""Lazy regex compiler."""
|
|
if not self.template.startswith('^'):
|
|
self.template = '^' + self.template
|
|
|
|
if not self.template.endswith('$'):
|
|
self.template += '$'
|
|
|
|
return re.compile(self.template)
|
|
|
|
def match(self, request):
|
|
"""Matches this route against the current request.
|
|
|
|
.. seealso:: :meth:`BaseRoute.match`.
|
|
"""
|
|
match = self.regex.match(urllib.unquote(request.path))
|
|
if match:
|
|
return self, match.groups(), {}
|
|
|
|
def __repr__(self):
|
|
return '<SimpleRoute(%r, %r)>' % (self.template, self.handler)
|
|
|
|
|
|
class Route(BaseRoute):
|
|
"""A route definition that maps a URI path to a handler.
|
|
|
|
The initial concept was based on `Another Do-It-Yourself Framework`_, by
|
|
Ian Bicking.
|
|
"""
|
|
|
|
#: Default parameters values.
|
|
defaults = None
|
|
#: Sequence of allowed HTTP methods. If not set, all methods are allowed.
|
|
methods = None
|
|
#: Sequence of allowed URI schemes. If not set, all schemes are allowed.
|
|
schemes = None
|
|
# Lazy properties extracted from the route template.
|
|
regex = None
|
|
reverse_template = None
|
|
variables = None
|
|
args_count = 0
|
|
kwargs_count = 0
|
|
|
|
def __init__(self, template, handler=None, name=None, defaults=None,
|
|
build_only=False, handler_method=None, methods=None,
|
|
schemes=None):
|
|
"""Initializes this route.
|
|
|
|
:param template:
|
|
A route template to match against the request path. A template
|
|
can have variables enclosed by ``<>`` that define a name, a
|
|
regular expression or both. Examples:
|
|
|
|
================= ==================================
|
|
Format Example
|
|
================= ==================================
|
|
``<name>`` ``'/blog/<year>/<month>'``
|
|
``<:regex>`` ``'/blog/<:\d{4}>/<:\d{2}>'``
|
|
``<name:regex>`` ``'/blog/<year:\d{4}>/<month:\d{2}>'``
|
|
================= ==================================
|
|
|
|
The same template can mix parts with name, regular expression or
|
|
both.
|
|
|
|
If the name is set, the value of the matched regular expression
|
|
is passed as keyword argument to the handler. Otherwise it is
|
|
passed as positional argument.
|
|
|
|
If only the name is set, it will match anything except a slash.
|
|
So these routes are equivalent::
|
|
|
|
Route('/<user_id>/settings', handler=SettingsHandler,
|
|
name='user-settings')
|
|
Route('/<user_id:[^/]+>/settings', handler=SettingsHandler,
|
|
name='user-settings')
|
|
|
|
.. note::
|
|
The handler only receives ``*args`` if no named variables are
|
|
set. Otherwise, the handler only receives ``**kwargs``. This
|
|
allows you to set regular expressions that are not captured:
|
|
just mix named and unnamed variables and the handler will
|
|
only receive the named ones.
|
|
|
|
:param handler:
|
|
A callable or string in dotted notation to be lazily imported,
|
|
e.g., ``'my.module.MyHandler'`` or ``'my.module.my_function'``.
|
|
It is possible to define a method if the callable is a class,
|
|
separating it by a colon: ``'my.module.MyHandler:my_method'``.
|
|
This is a shortcut and has the same effect as defining the
|
|
`handler_method` parameter.
|
|
:param name:
|
|
The name of this route, used to build URIs based on it.
|
|
:param defaults:
|
|
Default or extra keywords to be returned by this route. Values
|
|
also present in the route variables are used to build the URI
|
|
when they are missing.
|
|
:param build_only:
|
|
If True, this route never matches and is used only to build URIs.
|
|
:param handler_method:
|
|
The name of a custom handler method to be called, in case `handler`
|
|
is a class. If not defined, the default behavior is to call the
|
|
handler method correspondent to the HTTP request method in lower
|
|
case (e.g., `get()`, `post()` etc).
|
|
:param methods:
|
|
A sequence of HTTP methods. If set, the route will only match if
|
|
the request method is allowed.
|
|
:param schemes:
|
|
A sequence of URI schemes, e.g., ``['http']`` or ``['https']``.
|
|
If set, the route will only match requests with these schemes.
|
|
"""
|
|
super(Route, self).__init__(template, handler=handler, name=name,
|
|
build_only=build_only)
|
|
self.defaults = defaults or {}
|
|
self.methods = methods
|
|
self.schemes = schemes
|
|
if isinstance(handler, basestring) and ':' in handler:
|
|
if handler_method:
|
|
raise ValueError(
|
|
"If handler_method is defined in a Route, handler "
|
|
"can't have a colon (got %r)." % handler)
|
|
else:
|
|
self.handler, self.handler_method = handler.rsplit(':', 1)
|
|
else:
|
|
self.handler_method = handler_method
|
|
|
|
@cached_property
|
|
def regex(self):
|
|
"""Lazy route template parser."""
|
|
regex, self.reverse_template, self.args_count, self.kwargs_count, \
|
|
self.variables = _parse_route_template(self.template,
|
|
default_sufix='[^/]+')
|
|
return regex
|
|
|
|
def match(self, request):
|
|
"""Matches this route against the current request.
|
|
|
|
:raises:
|
|
``exc.HTTPMethodNotAllowed`` if the route defines :attr:`methods`
|
|
and the request method isn't allowed.
|
|
|
|
.. seealso:: :meth:`BaseRoute.match`.
|
|
"""
|
|
match = self.regex.match(urllib.unquote(request.path))
|
|
if not match or self.schemes and request.scheme not in self.schemes:
|
|
return None
|
|
|
|
if self.methods and request.method not in self.methods:
|
|
# This will be caught by the router, so routes with different
|
|
# methods can be tried.
|
|
raise exc.HTTPMethodNotAllowed()
|
|
|
|
args, kwargs = _get_route_variables(match, self.defaults.copy())
|
|
return self, args, kwargs
|
|
|
|
def build(self, request, args, kwargs):
|
|
"""Returns a URI for this route.
|
|
|
|
.. seealso:: :meth:`Router.build`.
|
|
"""
|
|
scheme = kwargs.pop('_scheme', None)
|
|
netloc = kwargs.pop('_netloc', None)
|
|
anchor = kwargs.pop('_fragment', None)
|
|
full = kwargs.pop('_full', False) and not scheme and not netloc
|
|
|
|
if full or scheme or netloc:
|
|
netloc = netloc or request.host
|
|
scheme = scheme or request.scheme
|
|
|
|
path, query = self._build(args, kwargs)
|
|
return _urlunsplit(scheme, netloc, path, query, anchor)
|
|
|
|
def _build(self, args, kwargs):
|
|
"""Returns the URI path for this route.
|
|
|
|
:returns:
|
|
A tuple ``(path, kwargs)`` with the built URI path and extra
|
|
keywords to be used as URI query arguments.
|
|
"""
|
|
# Access self.regex just to set the lazy properties.
|
|
regex = self.regex
|
|
variables = self.variables
|
|
if self.args_count:
|
|
for index, value in enumerate(args):
|
|
key = '__%d__' % index
|
|
if key in variables:
|
|
kwargs[key] = value
|
|
|
|
values = {}
|
|
for name, regex in variables.iteritems():
|
|
value = kwargs.pop(name, self.defaults.get(name))
|
|
if value is None:
|
|
raise KeyError('Missing argument "%s" to build URI.' % \
|
|
name.strip('_'))
|
|
|
|
if not isinstance(value, basestring):
|
|
value = str(value)
|
|
|
|
if not regex.match(value):
|
|
raise ValueError('URI buiding error: Value "%s" is not '
|
|
'supported for argument "%s".' % (value, name.strip('_')))
|
|
|
|
values[name] = value
|
|
|
|
return (self.reverse_template % values, kwargs)
|
|
|
|
def __repr__(self):
|
|
return '<Route(%r, %r, name=%r, defaults=%r, build_only=%r)>' % \
|
|
(self.template, self.handler, self.name, self.defaults,
|
|
self.build_only)
|
|
|
|
|
|
class BaseHandlerAdapter(object):
|
|
"""A basic adapter to dispatch a handler.
|
|
|
|
This is used when the handler is a simple function: it just calls the
|
|
handler and returns the resulted response.
|
|
"""
|
|
|
|
#: The handler to be dispatched.
|
|
handler = None
|
|
|
|
def __init__(self, handler):
|
|
self.handler = handler
|
|
|
|
def __call__(self, request, response):
|
|
# The handler only receives *args if no named variables are set.
|
|
args, kwargs = request.route_args, request.route_kwargs
|
|
if kwargs:
|
|
args = ()
|
|
|
|
return self.handler(request, *args, **kwargs)
|
|
|
|
|
|
class WebappHandlerAdapter(BaseHandlerAdapter):
|
|
"""An adapter to dispatch a ``webapp.RequestHandler``.
|
|
|
|
Like in webapp, the handler is constructed, then ``initialize()`` is
|
|
called, then the method corresponding to the HTTP request method is called.
|
|
"""
|
|
|
|
def __call__(self, request, response):
|
|
handler = self.handler()
|
|
handler.initialize(request, response)
|
|
method_name = _normalize_handler_method(request.method)
|
|
method = getattr(handler, method_name, None)
|
|
if not method:
|
|
abort(501)
|
|
|
|
# The handler only receives *args if no named variables are set.
|
|
args, kwargs = request.route_args, request.route_kwargs
|
|
if kwargs:
|
|
args = ()
|
|
|
|
try:
|
|
method(*args, **kwargs)
|
|
except Exception, e:
|
|
handler.handle_exception(e, request.app.debug)
|
|
|
|
|
|
class Webapp2HandlerAdapter(BaseHandlerAdapter):
|
|
"""An adapter to dispatch a ``webapp2.RequestHandler``.
|
|
|
|
The handler is constructed then ``dispatch()`` is called.
|
|
"""
|
|
|
|
def __call__(self, request, response):
|
|
handler = self.handler(request, response)
|
|
return handler.dispatch()
|
|
|
|
|
|
class Router(object):
|
|
"""A URI router used to match, dispatch and build URIs."""
|
|
|
|
#: Class used when the route is set as a tuple.
|
|
route_class = SimpleRoute
|
|
#: All routes that can be matched.
|
|
match_routes = None
|
|
#: All routes that can be built.
|
|
build_routes = None
|
|
#: Handler classes imported lazily.
|
|
handlers = None
|
|
|
|
def __init__(self, routes=None):
|
|
"""Initializes the router.
|
|
|
|
:param routes:
|
|
A sequence of :class:`Route` instances or, for simple routes,
|
|
tuples ``(regex, handler)``.
|
|
"""
|
|
self.match_routes = []
|
|
self.build_routes = {}
|
|
self.handlers = {}
|
|
if routes:
|
|
for route in routes:
|
|
self.add(route)
|
|
|
|
def add(self, route):
|
|
"""Adds a route to this router.
|
|
|
|
:param route:
|
|
A :class:`Route` instance or, for simple routes, a tuple
|
|
``(regex, handler)``.
|
|
"""
|
|
if isinstance(route, tuple):
|
|
# Exceptional case: simple routes defined as a tuple.
|
|
route = self.route_class(*route)
|
|
|
|
for r in route.get_match_routes():
|
|
self.match_routes.append(r)
|
|
|
|
for name, r in route.get_build_routes():
|
|
self.build_routes[name] = r
|
|
|
|
def set_matcher(self, func):
|
|
"""Sets the function called to match URIs.
|
|
|
|
:param func:
|
|
A function that receives ``(router, request)`` and returns
|
|
a tuple ``(route, args, kwargs)`` if any route matches, or
|
|
raise ``exc.HTTPNotFound`` if no route matched or
|
|
``exc.HTTPMethodNotAllowed`` if a route matched but the HTTP
|
|
method was not allowed.
|
|
"""
|
|
# Functions are descriptors, so bind it to this instance with __get__.
|
|
self.match = func.__get__(self, self.__class__)
|
|
|
|
def set_builder(self, func):
|
|
"""Sets the function called to build URIs.
|
|
|
|
:param func:
|
|
A function that receives ``(router, request, name, args, kwargs)``
|
|
and returns a URI.
|
|
"""
|
|
self.build = func.__get__(self, self.__class__)
|
|
|
|
def set_dispatcher(self, func):
|
|
"""Sets the function called to dispatch the handler.
|
|
|
|
:param func:
|
|
A function that receives ``(router, request, response)``
|
|
and returns the value returned by the dispatched handler.
|
|
"""
|
|
self.dispatch = func.__get__(self, self.__class__)
|
|
|
|
def set_adapter(self, func):
|
|
"""Sets the function that adapts loaded handlers for dispatching.
|
|
|
|
:param func:
|
|
A function that receives ``(router, handler)`` and returns a
|
|
handler callable.
|
|
"""
|
|
self.adapt = func.__get__(self, self.__class__)
|
|
|
|
def default_matcher(self, request):
|
|
"""Matches all routes against a request object.
|
|
|
|
The first one that matches is returned.
|
|
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
:returns:
|
|
A tuple ``(route, args, kwargs)`` if a route matched, or None.
|
|
:raises:
|
|
``exc.HTTPNotFound`` if no route matched or
|
|
``exc.HTTPMethodNotAllowed`` if a route matched but the HTTP
|
|
method was not allowed.
|
|
"""
|
|
method_not_allowed = False
|
|
for route in self.match_routes:
|
|
try:
|
|
match = route.match(request)
|
|
if match:
|
|
return match
|
|
except exc.HTTPMethodNotAllowed:
|
|
method_not_allowed = True
|
|
|
|
if method_not_allowed:
|
|
raise exc.HTTPMethodNotAllowed()
|
|
|
|
raise exc.HTTPNotFound()
|
|
|
|
def default_builder(self, request, name, args, kwargs):
|
|
"""Returns a URI for a named :class:`Route`.
|
|
|
|
:param request:
|
|
The current :class:`Request` object.
|
|
:param name:
|
|
The route name.
|
|
:param args:
|
|
Tuple of positional arguments to build the URI. All positional
|
|
variables defined in the route must be passed and must conform
|
|
to the format set in the route. Extra arguments are ignored.
|
|
:param kwargs:
|
|
Dictionary of keyword arguments to build the URI. All variables
|
|
not set in the route default values must be passed and must
|
|
conform to the format set in the route. Extra keywords are
|
|
appended as a query string.
|
|
|
|
A few keywords have special meaning:
|
|
|
|
- **_full**: If True, builds an absolute URI.
|
|
- **_scheme**: URI scheme, e.g., `http` or `https`. If defined,
|
|
an absolute URI is always returned.
|
|
- **_netloc**: Network location, e.g., `www.google.com`. If
|
|
defined, an absolute URI is always returned.
|
|
- **_fragment**: If set, appends a fragment (or "anchor") to the
|
|
generated URI.
|
|
:returns:
|
|
An absolute or relative URI.
|
|
"""
|
|
route = self.build_routes.get(name)
|
|
if route is None:
|
|
raise KeyError('Route named %r is not defined.' % name)
|
|
|
|
return route.build(request, args, kwargs)
|
|
|
|
def default_dispatcher(self, request, response):
|
|
"""Dispatches a handler.
|
|
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
:param response:
|
|
A :class:`Response` instance.
|
|
:raises:
|
|
``exc.HTTPNotFound`` if no route matched or
|
|
``exc.HTTPMethodNotAllowed`` if a route matched but the HTTP
|
|
method was not allowed.
|
|
:returns:
|
|
The returned value from the handler.
|
|
"""
|
|
route, args, kwargs = rv = self.match(request)
|
|
request.route, request.route_args, request.route_kwargs = rv
|
|
|
|
if route.handler_adapter is None:
|
|
handler = route.handler
|
|
if isinstance(handler, basestring):
|
|
if handler not in self.handlers:
|
|
self.handlers[handler] = handler = import_string(handler)
|
|
else:
|
|
handler = self.handlers[handler]
|
|
|
|
route.handler_adapter = self.adapt(handler)
|
|
|
|
return route.handler_adapter(request, response)
|
|
|
|
def default_adapter(self, handler):
|
|
"""Adapts a handler for dispatching.
|
|
|
|
Because handlers use or implement different dispatching mechanisms,
|
|
they can be wrapped to use a unified API for dispatching.
|
|
This way webapp2 can support, for example, a :class:`RequestHandler`
|
|
class and function views or, for compatibility purposes, a
|
|
``webapp.RequestHandler`` class. The adapters follow the same router
|
|
dispatching API but dispatch each handler type differently.
|
|
|
|
:param handler:
|
|
A handler callable.
|
|
:returns:
|
|
A wrapped handler callable.
|
|
"""
|
|
if inspect.isclass(handler):
|
|
if _webapp and issubclass(handler, _webapp.RequestHandler):
|
|
# Compatible with webapp.RequestHandler.
|
|
adapter = WebappHandlerAdapter
|
|
else:
|
|
# Default, compatible with webapp2.RequestHandler.
|
|
adapter = Webapp2HandlerAdapter
|
|
else:
|
|
# A "view" function.
|
|
adapter = BaseHandlerAdapter
|
|
|
|
return adapter(handler)
|
|
|
|
def __repr__(self):
|
|
routes = self.match_routes + [v for k, v in \
|
|
self.build_routes.iteritems() if v not in self.match_routes]
|
|
|
|
return '<Router(%r)>' % routes
|
|
|
|
# Default matcher, builder, dispatcher and adapter.
|
|
match = default_matcher
|
|
build = default_builder
|
|
dispatch = default_dispatcher
|
|
adapt = default_adapter
|
|
|
|
|
|
class Config(dict):
|
|
"""A simple configuration dictionary for the :class:`WSGIApplication`."""
|
|
|
|
#: Loaded configurations.
|
|
loaded = None
|
|
|
|
def __init__(self, defaults=None):
|
|
dict.__init__(self, defaults or ())
|
|
self.loaded = []
|
|
|
|
def load_config(self, key, default_values=None, user_values=None,
|
|
required_keys=None):
|
|
"""Returns a configuration for a given key.
|
|
|
|
This can be used by objects that define a default configuration. It
|
|
will update the app configuration with the default values the first
|
|
time it is requested, and mark the key as loaded.
|
|
|
|
:param key:
|
|
A configuration key.
|
|
:param default_values:
|
|
Default values defined by a module or class.
|
|
:param user_values:
|
|
User values, used when an object can be initialized with
|
|
configuration. This overrides the app configuration.
|
|
:param required_keys:
|
|
Keys that can not be None.
|
|
:raises:
|
|
Exception, when a required key is not set or is None.
|
|
"""
|
|
if key in self.loaded:
|
|
config = self[key]
|
|
else:
|
|
config = dict(default_values or ())
|
|
if key in self:
|
|
config.update(self[key])
|
|
|
|
self[key] = config
|
|
self.loaded.append(key)
|
|
if required_keys and not user_values:
|
|
self._validate_required(key, config, required_keys)
|
|
|
|
if user_values:
|
|
config = config.copy()
|
|
config.update(user_values)
|
|
if required_keys:
|
|
self._validate_required(key, config, required_keys)
|
|
|
|
return config
|
|
|
|
def _validate_required(self, key, config, required_keys):
|
|
missing = [k for k in required_keys if config.get(k) is None]
|
|
if missing:
|
|
raise Exception(
|
|
'Missing configuration keys for %r: %r.' % (key, missing))
|
|
|
|
|
|
class RequestContext(object):
|
|
"""Context for a single request.
|
|
|
|
The context is responsible for setting and cleaning global variables for
|
|
a request.
|
|
"""
|
|
|
|
#: A :class:`WSGIApplication` instance.
|
|
app = None
|
|
#: WSGI environment dictionary.
|
|
environ = None
|
|
|
|
def __init__(self, app, environ):
|
|
"""Initializes the request context.
|
|
|
|
:param app:
|
|
An :class:`WSGIApplication` instance.
|
|
:param environ:
|
|
A WSGI environment dictionary.
|
|
"""
|
|
self.app = app
|
|
self.environ = environ
|
|
|
|
def __enter__(self):
|
|
"""Enters the request context.
|
|
|
|
:returns:
|
|
A tuple ``(request, response)``.
|
|
"""
|
|
# Build request and response.
|
|
request = self.app.request_class(self.environ)
|
|
response = self.app.response_class()
|
|
# Make active app and response available through the request object.
|
|
request.app = self.app
|
|
request.response = response
|
|
# Register global variables.
|
|
self.app.set_globals(app=self.app, request=request)
|
|
return request, response
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
"""Exits the request context.
|
|
|
|
This release the context locals except if an exception is caught
|
|
in debug mode. In this case they are kept to be inspected.
|
|
"""
|
|
if exc_type is None or not self.app.debug:
|
|
# Unregister global variables.
|
|
self.app.clear_globals()
|
|
|
|
|
|
class WSGIApplication(object):
|
|
"""A WSGI-compliant application."""
|
|
|
|
#: Allowed request methods.
|
|
allowed_methods = frozenset(('GET', 'POST', 'HEAD', 'OPTIONS', 'PUT',
|
|
'DELETE', 'TRACE'))
|
|
#: Class used for the request object.
|
|
request_class = Request
|
|
#: Class used for the response object.
|
|
response_class = Response
|
|
#: Class used for the router object.
|
|
router_class = Router
|
|
#: Class used for the request context object.
|
|
request_context_class = RequestContext
|
|
#: Class used for the configuration object.
|
|
config_class = Config
|
|
#: A general purpose flag to indicate development mode: if True, uncaught
|
|
#: exceptions are raised instead of using ``HTTPInternalServerError``.
|
|
debug = False
|
|
#: A :class:`Router` instance with all URIs registered for the application.
|
|
router = None
|
|
#: A :class:`Config` instance with the application configuration.
|
|
config = None
|
|
#: A dictionary to register objects used during the app lifetime.
|
|
registry = None
|
|
#: A dictionary mapping HTTP error codes to callables to handle those
|
|
#: HTTP exceptions. See :meth:`handle_exception`.
|
|
error_handlers = None
|
|
#: Active :class:`WSGIApplication` instance. See :meth:`set_globals`.
|
|
app = None
|
|
#: Active :class:`Request` instance. See :meth:`set_globals`.
|
|
request = None
|
|
#: Same as :attr:`app`, for webapp compatibility. See :meth:`set_globals`.
|
|
active_instance = None
|
|
|
|
def __init__(self, routes=None, debug=False, config=None):
|
|
"""Initializes the WSGI application.
|
|
|
|
:param routes:
|
|
A sequence of :class:`Route` instances or, for simple routes,
|
|
tuples ``(regex, handler)``.
|
|
:param debug:
|
|
True to enable debug mode, False otherwise.
|
|
:param config:
|
|
A configuration dictionary for the application.
|
|
"""
|
|
self.debug = debug
|
|
self.registry = {}
|
|
self.error_handlers = {}
|
|
self.set_globals(app=self)
|
|
self.config = self.config_class(config)
|
|
self.router = self.router_class(routes)
|
|
|
|
def set_globals(self, app=None, request=None):
|
|
"""Registers the global variables for app and request.
|
|
|
|
If :mod:`webapp2_extras.local` is available the app and request
|
|
class attributes are assigned to a proxy object that returns them
|
|
using thread-local, making the application thread-safe. This can also
|
|
be used in environments that don't support threading.
|
|
|
|
If :mod:`webapp2_extras.local` is not available app and request will
|
|
be assigned directly as class attributes. This should only be used in
|
|
non-threaded environments (e.g., App Engine Python 2.5).
|
|
|
|
:param app:
|
|
A :class:`WSGIApplication` instance.
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
"""
|
|
if _local is not None: # pragma: no cover
|
|
_local.app = app
|
|
_local.request = request
|
|
else: # pragma: no cover
|
|
WSGIApplication.app = WSGIApplication.active_instance = app
|
|
WSGIApplication.request = request
|
|
|
|
def clear_globals(self):
|
|
"""Clears global variables. See :meth:`set_globals`."""
|
|
if _local is not None: # pragma: no cover
|
|
_local.__release_local__()
|
|
else: # pragma: no cover
|
|
WSGIApplication.app = WSGIApplication.active_instance = None
|
|
WSGIApplication.request = None
|
|
|
|
def __call__(self, environ, start_response):
|
|
"""Called by WSGI when a request comes in.
|
|
|
|
:param environ:
|
|
A WSGI environment.
|
|
:param start_response:
|
|
A callable accepting a status code, a list of headers and an
|
|
optional exception context to start the response.
|
|
:returns:
|
|
An iterable with the response to return to the client.
|
|
"""
|
|
with self.request_context_class(self, environ) as (request, response):
|
|
try:
|
|
if request.method not in self.allowed_methods:
|
|
# 501 Not Implemented.
|
|
raise exc.HTTPNotImplemented()
|
|
|
|
rv = self.router.dispatch(request, response)
|
|
if rv is not None:
|
|
response = rv
|
|
except Exception, e:
|
|
try:
|
|
# Try to handle it with a custom error handler.
|
|
rv = self.handle_exception(request, response, e)
|
|
if rv is not None:
|
|
response = rv
|
|
except HTTPException, e:
|
|
# Use the HTTP exception as response.
|
|
response = e
|
|
except Exception, e:
|
|
# Error wasn't handled so we have nothing else to do.
|
|
response = self._internal_error(e)
|
|
|
|
try:
|
|
return response(environ, start_response)
|
|
except Exception, e:
|
|
return self._internal_error(e)(environ, start_response)
|
|
|
|
def _internal_error(self, exception):
|
|
"""Last resource error for :meth:`__call__`."""
|
|
logging.exception(exception)
|
|
if self.debug:
|
|
lines = ''.join(traceback.format_exception(*sys.exc_info()))
|
|
html = _debug_template % (cgi.escape(lines, quote=True))
|
|
return Response(body=html, status=500)
|
|
|
|
return exc.HTTPInternalServerError()
|
|
|
|
def handle_exception(self, request, response, e):
|
|
"""Handles a uncaught exception occurred in :meth:`__call__`.
|
|
|
|
Uncaught exceptions can be handled by error handlers registered in
|
|
:attr:`error_handlers`. This is a dictionary that maps HTTP status
|
|
codes to callables that will handle the corresponding error code.
|
|
If the exception is not an ``HTTPException``, the status code 500
|
|
is used.
|
|
|
|
The error handlers receive (request, response, exception) and can be
|
|
a callable or a string in dotted notation to be lazily imported.
|
|
|
|
If no error handler is found, the exception is re-raised.
|
|
|
|
Based on idea from `Flask`_.
|
|
|
|
:param request:
|
|
A :class:`Request` instance.
|
|
:param response:
|
|
A :class:`Response` instance.
|
|
:param e:
|
|
The uncaught exception.
|
|
:returns:
|
|
The returned value from the error handler.
|
|
"""
|
|
if isinstance(e, HTTPException):
|
|
code = e.code
|
|
else:
|
|
code = 500
|
|
|
|
handler = self.error_handlers.get(code)
|
|
if handler:
|
|
if isinstance(handler, basestring):
|
|
self.error_handlers[code] = handler = import_string(handler)
|
|
|
|
return handler(request, response, e)
|
|
else:
|
|
# Re-raise it to be caught by the WSGI app.
|
|
raise
|
|
|
|
def run(self, bare=False):
|
|
"""Runs this WSGI-compliant application in a CGI environment.
|
|
|
|
This uses functions provided by ``google.appengine.ext.webapp.util``,
|
|
if available: ``run_bare_wsgi_app`` and ``run_wsgi_app``.
|
|
|
|
Otherwise, it uses ``wsgiref.handlers.CGIHandler().run()``.
|
|
|
|
:param bare:
|
|
If True, doesn't add registered WSGI middleware: use
|
|
``run_bare_wsgi_app`` instead of ``run_wsgi_app``.
|
|
"""
|
|
if _webapp_util:
|
|
if bare:
|
|
_webapp_util.run_bare_wsgi_app(self)
|
|
else:
|
|
_webapp_util.run_wsgi_app(self)
|
|
else: # pragma: no cover
|
|
handlers.CGIHandler().run(self)
|
|
|
|
def get_response(self, *args, **kwargs):
|
|
"""Creates a request and returns a response for this app.
|
|
|
|
This is a convenience for unit testing purposes. It receives
|
|
parameters to build a request and calls the application, returning
|
|
the resulting response::
|
|
|
|
class HelloHandler(webapp2.RequestHandler):
|
|
def get(self):
|
|
self.response.write('Hello, world!')
|
|
|
|
app = webapp2.WSGIapplication([('/', HelloHandler)])
|
|
|
|
# Test the app, passing parameters to build a request.
|
|
response = app.get_response('/')
|
|
assert response.status_int == 200
|
|
assert response.body == 'Hello, world!'
|
|
|
|
:param args:
|
|
Positional arguments to be passed to ``Request.blank()``.
|
|
:param kwargs:
|
|
Keyword arguments to be passed to ``Request.blank()``.
|
|
:returns:
|
|
A :class:`Response` object.
|
|
"""
|
|
return self.request_class.blank(*args, **kwargs).get_response(self)
|
|
|
|
|
|
_import_string_error = """\
|
|
import_string() failed for %r. Possible reasons are:
|
|
|
|
- missing __init__.py in a package;
|
|
- package or module path not included in sys.path;
|
|
- duplicated package or module name taking precedence in sys.path;
|
|
- missing module, class, function or variable;
|
|
|
|
Original exception:
|
|
|
|
%s: %s
|
|
|
|
Debugged import:
|
|
|
|
%s"""
|
|
|
|
|
|
class ImportStringError(Exception):
|
|
"""Provides information about a failed :func:`import_string` attempt."""
|
|
|
|
#: String in dotted notation that failed to be imported.
|
|
import_name = None
|
|
#: Wrapped exception.
|
|
exception = None
|
|
|
|
def __init__(self, import_name, exception):
|
|
self.import_name = import_name
|
|
self.exception = exception
|
|
msg = _import_string_error
|
|
name = ''
|
|
tracked = []
|
|
for part in import_name.split('.'):
|
|
name += (name and '.') + part
|
|
imported = import_string(name, silent=True)
|
|
if imported:
|
|
tracked.append((name, imported.__file__))
|
|
else:
|
|
track = ['- %r found in %r.' % rv for rv in tracked]
|
|
track.append('- %r not found.' % name)
|
|
msg = msg % (import_name, exception.__class__.__name__,
|
|
str(exception), '\n'.join(track))
|
|
break
|
|
|
|
Exception.__init__(self, msg)
|
|
|
|
|
|
_get_app_error = 'WSGIApplication global variable is not set.'
|
|
_get_request_error = 'Request global variable is not set.'
|
|
|
|
|
|
def get_app():
|
|
"""Returns the active app instance.
|
|
|
|
:returns:
|
|
A :class:`WSGIApplication` instance.
|
|
"""
|
|
if _local:
|
|
assert getattr(_local, 'app', None) is not None, _get_app_error
|
|
else:
|
|
assert WSGIApplication.app is not None, _get_app_error
|
|
|
|
return WSGIApplication.app
|
|
|
|
|
|
def get_request():
|
|
"""Returns the active request instance.
|
|
|
|
:returns:
|
|
A :class:`Request` instance.
|
|
"""
|
|
if _local:
|
|
assert getattr(_local, 'request', None) is not None, _get_request_error
|
|
else:
|
|
assert WSGIApplication.request is not None, _get_request_error
|
|
|
|
return WSGIApplication.request
|
|
|
|
|
|
def uri_for(_name, _request=None, *args, **kwargs):
|
|
"""A standalone uri_for version that can be passed to templates.
|
|
|
|
.. seealso:: :meth:`Router.build`.
|
|
"""
|
|
request = _request or get_request()
|
|
return request.app.router.build(request, _name, args, kwargs)
|
|
|
|
|
|
def redirect(uri, permanent=False, abort=False, code=None, body=None,
|
|
request=None, response=None):
|
|
"""Issues an HTTP redirect to the given relative URI.
|
|
|
|
This won't stop code execution unless **abort** is True. A common
|
|
practice is to return when calling this method::
|
|
|
|
return redirect('/some-path')
|
|
|
|
:param uri:
|
|
A relative or absolute URI (e.g., ``'../flowers.html'``).
|
|
:param permanent:
|
|
If True, uses a 301 redirect instead of a 302 redirect.
|
|
:param abort:
|
|
If True, raises an exception to perform the redirect.
|
|
:param code:
|
|
The redirect status code. Supported codes are 301, 302, 303, 305,
|
|
and 307. 300 is not supported because it's not a real redirect
|
|
and 304 because it's the answer for a request with defined
|
|
``If-Modified-Since`` headers.
|
|
:param body:
|
|
Response body, if any.
|
|
:param request:
|
|
Optional request object. If not set, uses :func:`get_request`.
|
|
:param response:
|
|
Optional response object. If not set, a new response is created.
|
|
:returns:
|
|
A :class:`Response` instance.
|
|
"""
|
|
if uri.startswith(('.', '/')):
|
|
request = request or get_request()
|
|
uri = str(urlparse.urljoin(request.url, uri))
|
|
|
|
if code is None:
|
|
if permanent:
|
|
code = 301
|
|
else:
|
|
code = 302
|
|
|
|
assert code in (301, 302, 303, 305, 307), \
|
|
'Invalid redirect status code.'
|
|
|
|
if abort:
|
|
_abort(code, headers=[('Location', uri)])
|
|
|
|
if response is None:
|
|
request = request or get_request()
|
|
response = request.app.response_class()
|
|
else:
|
|
response.clear()
|
|
|
|
response.headers['Location'] = uri
|
|
response.status = code
|
|
if body is not None:
|
|
response.write(body)
|
|
|
|
return response
|
|
|
|
|
|
def redirect_to(_name, _permanent=False, _abort=False, _code=None,
|
|
_body=None, _request=None, _response=None, *args, **kwargs):
|
|
"""Convenience function mixing :func:`redirect` and :func:`uri_for`.
|
|
|
|
Issues an HTTP redirect to a named URI built using :func:`uri_for`.
|
|
|
|
:param _name:
|
|
The route name to redirect to.
|
|
:param args:
|
|
Positional arguments to build the URI.
|
|
:param kwargs:
|
|
Keyword arguments to build the URI.
|
|
:returns:
|
|
A :class:`Response` instance.
|
|
|
|
The other arguments are described in :func:`redirect`.
|
|
"""
|
|
uri = uri_for(_name, _request=_request, *args, **kwargs)
|
|
return redirect(uri, permanent=_permanent, abort=_abort, code=_code,
|
|
body=_body, request=_request, response=_response)
|
|
|
|
|
|
def abort(code, *args, **kwargs):
|
|
"""Raises an ``HTTPException``.
|
|
|
|
:param code:
|
|
An integer that represents a valid HTTP status code.
|
|
:param args:
|
|
Positional arguments to instantiate the exception.
|
|
:param kwargs:
|
|
Keyword arguments to instantiate the exception.
|
|
"""
|
|
cls = exc.status_map.get(code)
|
|
if not cls:
|
|
raise KeyError('No exception is defined for code %r.' % code)
|
|
|
|
raise cls(*args, **kwargs)
|
|
|
|
|
|
def import_string(import_name, silent=False):
|
|
"""Imports an object based on a string in dotted notation.
|
|
|
|
Simplified version of the function with same name from `Werkzeug`_.
|
|
|
|
:param import_name:
|
|
String in dotted notation of the object to be imported.
|
|
:param silent:
|
|
If True, import or attribute errors are ignored and None is returned
|
|
instead of raising an exception.
|
|
:returns:
|
|
The imported object.
|
|
"""
|
|
import_name = _to_utf8(import_name)
|
|
try:
|
|
if '.' in import_name:
|
|
module, obj = import_name.rsplit('.', 1)
|
|
return getattr(__import__(module, None, None, [obj]), obj)
|
|
else:
|
|
return __import__(import_name)
|
|
except (ImportError, AttributeError), e:
|
|
if not silent:
|
|
raise ImportStringError(import_name, e), None, sys.exc_info()[2]
|
|
|
|
|
|
def _urlunsplit(scheme=None, netloc=None, path=None, query=None,
|
|
fragment=None):
|
|
"""Like ``urlparse.urlunsplit``, but will escape values and urlencode and
|
|
sort query arguments.
|
|
|
|
:param scheme:
|
|
URI scheme, e.g., `http` or `https`.
|
|
:param netloc:
|
|
Network location, e.g., `localhost:8080` or `www.google.com`.
|
|
:param path:
|
|
URI path.
|
|
:param query:
|
|
URI query as an escaped string, or a dictionary or list of key-values
|
|
tuples to build a query.
|
|
:param fragment:
|
|
Fragment identifier, also known as "anchor".
|
|
:returns:
|
|
An assembled absolute or relative URI.
|
|
"""
|
|
if not scheme or not netloc:
|
|
scheme = None
|
|
netloc = None
|
|
|
|
if path:
|
|
path = urllib.quote(_to_utf8(path))
|
|
|
|
if query and not isinstance(query, basestring):
|
|
if isinstance(query, dict):
|
|
query = query.iteritems()
|
|
|
|
# Sort args: commonly needed to build signatures for services.
|
|
query = urllib.urlencode(sorted(query))
|
|
|
|
if fragment:
|
|
fragment = urllib.quote(_to_utf8(fragment))
|
|
|
|
return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
|
|
|
|
|
|
def _get_handler_methods(handler):
|
|
"""Returns a list of HTTP methods supported by a handler.
|
|
|
|
:param handler:
|
|
A :class:`RequestHandler` instance.
|
|
:returns:
|
|
A list of HTTP methods supported by the handler.
|
|
"""
|
|
methods = []
|
|
for method in get_app().allowed_methods:
|
|
if getattr(handler, _normalize_handler_method(method), None):
|
|
methods.append(method)
|
|
|
|
return methods
|
|
|
|
|
|
def _normalize_handler_method(method):
|
|
"""Transforms an HTTP method into a valid Python identifier."""
|
|
return method.lower().replace('-', '_')
|
|
|
|
|
|
def _to_utf8(value):
|
|
"""Encodes a unicode value to UTF-8 if not yet encoded."""
|
|
if isinstance(value, str):
|
|
return value
|
|
|
|
return value.encode('utf-8')
|
|
|
|
|
|
def _parse_route_template(template, default_sufix=''):
|
|
"""Lazy route template parser."""
|
|
variables = {}
|
|
reverse_template = pattern = ''
|
|
args_count = last = 0
|
|
for match in _route_re.finditer(template):
|
|
part = template[last:match.start()]
|
|
name = match.group(1)
|
|
expr = match.group(2) or default_sufix
|
|
last = match.end()
|
|
|
|
if not name:
|
|
name = '__%d__' % args_count
|
|
args_count += 1
|
|
|
|
pattern += '%s(?P<%s>%s)' % (re.escape(part), name, expr)
|
|
reverse_template += '%s%%(%s)s' % (part, name)
|
|
variables[name] = re.compile('^%s$' % expr)
|
|
|
|
part = template[last:]
|
|
kwargs_count = len(variables) - args_count
|
|
reverse_template += part
|
|
regex = re.compile('^%s%s$' % (pattern, re.escape(part)))
|
|
return regex, reverse_template, args_count, kwargs_count, variables
|
|
|
|
|
|
def _get_route_variables(match, default_kwargs=None):
|
|
"""Returns (args, kwargs) for a route match."""
|
|
kwargs = default_kwargs or {}
|
|
kwargs.update(match.groupdict())
|
|
if kwargs:
|
|
args = tuple(value[1] for value in sorted(
|
|
(int(key[2:-2]), kwargs.pop(key)) for key in kwargs.keys() \
|
|
if key.startswith('__') and key.endswith('__')))
|
|
else:
|
|
args = ()
|
|
|
|
return args, kwargs
|
|
|
|
|
|
def _set_thread_safe_app():
|
|
"""Assigns WSGIApplication globals to a proxy pointing to thread-local."""
|
|
if _local is not None: # pragma: no cover
|
|
WSGIApplication.app = WSGIApplication.active_instance = _local('app')
|
|
WSGIApplication.request = _local('request')
|
|
|
|
|
|
Request.ResponseClass = Response
|
|
Response.RequestClass = Request
|
|
# Alias.
|
|
_abort = abort
|
|
# Thread-safety support.
|
|
_set_thread_safe_app()
|
|
|
|
# Defer importing google.appengine.ext.webapp.util until every public symbol
|
|
# has been defined since google.appengine.ext.webapp in App Engine Python 2.7
|
|
# runtime imports this module to provide its public interface.
|
|
try:
|
|
from google.appengine.ext.webapp import util as _webapp_util
|
|
except ImportError: # pragma: no cover
|
|
pass
|