app: Refactor Application class
This commit breaks up the `Application.__call__` method into smaller methods that can be overridden by subclasses. These methods allow customization of various steps of the request/response handling process: * `make_request`: Create the `Request` object from the WSGI environment dictionary. The default implementation creates a `milla.Request` object, copies the application configuration to its `config` attribute, and handles "emulated" HTTP methods from POST data. * `resolve_path`: Locates a controller callable from the given path info. The default implementation calls the `resolve` method on the application's `dispatcher` attribute. If `UnresolvePath` is raised, it returns a callable that raises `HTTPNotFound`. * `handle_error`: Called inside the exception handler when a controller callable raises an exception. The method should return a callable WSGI application (such as a `Response` or `WSGIHTTPException` object). To access the exception that was raised, use the `sys.exc_info` function. The default implementation returns the exception if it is an instance of `WSGIHTTPException`, or re-raises the exception otherwise. This allows middleware applications to handle the exception, if desired.master
parent
922a82e4e8
commit
da406fcce8
|
@ -28,6 +28,22 @@ except ImportError: # pragma: no cover
|
|||
urllib.parse.urlencode = urllib.urlencode
|
||||
|
||||
|
||||
class _AllowAll(object):
|
||||
|
||||
def __contains__(self, other):
|
||||
return True
|
||||
|
||||
|
||||
ALL_METHODS = _AllowAll()
|
||||
'''Allow all HTTP methods (even non-standard ones)'''
|
||||
|
||||
DEFAULT_METHODS = ['GET', 'HEAD']
|
||||
'''Default methods allowed on controller callables'''
|
||||
|
||||
STANDARD_METHODS = ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'TRACE']
|
||||
'''All standard HTTP methods'''
|
||||
|
||||
|
||||
def allow(*methods):
|
||||
'''Specify the allowed HTTP verbs for a controller callable
|
||||
|
||||
|
|
104
src/milla/app.py
104
src/milla/app.py
|
@ -25,6 +25,8 @@ from milla.controllers import FaviconController
|
|||
from milla.util import asbool
|
||||
from webob.exc import HTTPNotFound, WSGIHTTPException, HTTPMethodNotAllowed
|
||||
import milla.dispatch.traversal
|
||||
import sys
|
||||
|
||||
|
||||
__all__ = ['Application']
|
||||
|
||||
|
@ -47,8 +49,6 @@ class Application(object):
|
|||
configuration is copied and assigned to ``request.config``.
|
||||
'''
|
||||
|
||||
DEFAULT_ALLOWED_METHODS = ['GET', 'HEAD']
|
||||
|
||||
def __init__(self, obj):
|
||||
if not hasattr(obj, 'resolve'):
|
||||
# Object is not a dispatcher, but the root object for traversal
|
||||
|
@ -57,48 +57,31 @@ class Application(object):
|
|||
self.config = {'milla.favicon': True}
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
path_info = environ['PATH_INFO']
|
||||
try:
|
||||
func = self.dispatcher.resolve(path_info)
|
||||
except milla.dispatch.UnresolvedPath:
|
||||
if asbool(self.config.get('milla.favicon')) and \
|
||||
path_info == '/favicon.ico':
|
||||
func = FaviconController()
|
||||
else:
|
||||
return HTTPNotFound()(environ, start_response)
|
||||
|
||||
request = milla.Request(environ)
|
||||
request.config = self.config.copy()
|
||||
|
||||
# Sometimes, hacky applications will try to "emulate" some HTTP
|
||||
# method like POST or DELETE by specifying an _method parameter
|
||||
# in a POST request.
|
||||
if request.method == 'POST' and '_method' in request.POST:
|
||||
request.method = request.POST.pop('_method')
|
||||
start_response = StartResponseWrapper(start_response)
|
||||
func = self.resolve_path(environ['PATH_INFO'])
|
||||
request = self.make_request(environ)
|
||||
request.__dict__['start_response'] = start_response
|
||||
|
||||
try:
|
||||
allowed_methods = self._find_attr(func, 'allowed_methods')
|
||||
except AttributeError:
|
||||
allowed_methods = self.DEFAULT_ALLOWED_METHODS
|
||||
allowed_methods = milla.DEFAULT_METHODS
|
||||
if request.method not in allowed_methods:
|
||||
allow_header = {'Allow': ', '.join(allowed_methods)}
|
||||
if request.method == 'OPTIONS':
|
||||
def options_response(request, *args, **kwargs):
|
||||
def func(request):
|
||||
response = request.ResponseClass()
|
||||
response.headers = allow_header
|
||||
return response
|
||||
func = options_response
|
||||
else:
|
||||
func = HTTPMethodNotAllowed(headers=allow_header)
|
||||
return func(environ, start_response)
|
||||
def func(request):
|
||||
raise HTTPMethodNotAllowed(headers=allow_header)
|
||||
|
||||
start_response_wrapper = StartResponseWrapper(start_response)
|
||||
request.start_response = start_response_wrapper
|
||||
try:
|
||||
self._call_before(func)(request)
|
||||
response = func(request)
|
||||
except WSGIHTTPException as e:
|
||||
return e(environ, start_response)
|
||||
except:
|
||||
response = self.handle_error(request)
|
||||
finally:
|
||||
self._call_after(func)(request)
|
||||
|
||||
|
@ -113,12 +96,11 @@ class Application(object):
|
|||
if isinstance(response, _string) or not response:
|
||||
response = request.ResponseClass(response)
|
||||
|
||||
if not start_response_wrapper.called:
|
||||
start_response(response.status, response.headerlist)
|
||||
if environ['REQUEST_METHOD'] == 'HEAD':
|
||||
start_response(response.status, response.headerlist)
|
||||
return ''
|
||||
else:
|
||||
return response.app_iter
|
||||
return response(environ, start_response)
|
||||
|
||||
def _call_after(self, func):
|
||||
try:
|
||||
|
@ -145,6 +127,61 @@ class Application(object):
|
|||
return self._find_attr(obj.func, attr)
|
||||
raise
|
||||
|
||||
def make_request(self, environ):
|
||||
'''Create a :py:class:`~milla.Request` from a WSGI environment
|
||||
|
||||
:param environ: WSGI environment dictionary
|
||||
:returns: :py:class:`milla.Request` object for this request
|
||||
'''
|
||||
|
||||
request = milla.Request(environ)
|
||||
request.__dict__['config'] = self.config.copy()
|
||||
# Sometimes, hacky applications will try to "emulate" some HTTP
|
||||
# methods like PUT or DELETE by specifying an _method parameter
|
||||
# in a POST request.
|
||||
if request.method == 'POST' and '_method' in request.POST:
|
||||
request.method = request.POST.pop('_method')
|
||||
return request
|
||||
|
||||
def resolve_path(self, path_info):
|
||||
'''Find the controller for a given path
|
||||
|
||||
:param path_info: The request path, relative to the application
|
||||
:returns: Controller callable
|
||||
|
||||
If no controller could be resolved for the path, a function
|
||||
that raises :py:exc:`HTTPNotFound` will be returned.
|
||||
'''
|
||||
|
||||
def path_not_found(request):
|
||||
raise HTTPNotFound
|
||||
|
||||
path_not_found.allowed_methods = milla.ALL_METHODS
|
||||
|
||||
try:
|
||||
return self.dispatcher.resolve(path_info)
|
||||
except milla.dispatch.UnresolvedPath:
|
||||
if (path_info == '/favicon.ico' and
|
||||
asbool(self.config.get('milla.favicon'))):
|
||||
return FaviconController()
|
||||
else:
|
||||
return path_not_found
|
||||
|
||||
def handle_error(self, request):
|
||||
'''Handle errors raised by controller callables
|
||||
|
||||
Subclasses can override this method to customize the error
|
||||
handling behavior of applications. The default implementation
|
||||
only handles :py:exc:`WSGIHTTPException` exceptions, by calling
|
||||
them as WSGI applications
|
||||
'''
|
||||
|
||||
typ, value, tb = sys.exc_info()
|
||||
if issubclass(typ, WSGIHTTPException):
|
||||
return value
|
||||
raise
|
||||
|
||||
|
||||
class StartResponseWrapper():
|
||||
|
||||
def __init__(self, start_response):
|
||||
|
@ -152,5 +189,6 @@ class StartResponseWrapper():
|
|||
self.called = False
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if not self.called:
|
||||
self.called = True
|
||||
return self.start_response(*args, **kwargs)
|
||||
self.start_response(*args, **kwargs)
|
||||
|
|
|
@ -206,7 +206,7 @@ def test_return_unicode():
|
|||
response.finish_response(app_iter)
|
||||
assert response.body == unicode('Hello, world'), response.body
|
||||
|
||||
@nose.tools.raises(AttributeError)
|
||||
@nose.tools.raises(TypeError)
|
||||
@python3_only
|
||||
def test_return_bytes():
|
||||
'''Controllers cannot return bytes objects
|
||||
|
|
Loading…
Reference in New Issue