# Copyright 2011, 2012, 2014, 2015 Dustin C. Hatch
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
'''Module milla.app
Please give me a docstring!
:Created: Mar 26, 2011
:Author: dustin
:Updated: $Date$
:Updater: $Author$
'''
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']
[docs]class Application(object):
'''Represents a Milla web application
Constructing an ``Application`` instance needs a dispatcher, or
alternatively, a root object that will be passed to a new
:py:class:`milla.dispatch.traversal.Traverser`.
:param obj: An object implementing the dispatcher protocol, or an
object to be used as the root for a Traverser
``Application`` instances are WSGI applications.
.. py:attribute:: config
A mapping of configuration settings. For each request, the
configuration is copied and assigned to ``request.config``.
'''
def __init__(self, obj):
if not hasattr(obj, 'resolve'):
# Object is not a dispatcher, but the root object for traversal
obj = milla.dispatch.traversal.Traverser(obj)
self.dispatcher = obj
self.config = {'milla.favicon': True}
def __call__(self, environ, start_response):
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 = milla.DEFAULT_METHODS
if request.method not in allowed_methods:
allow_header = {'Allow': ', '.join(allowed_methods)}
if request.method == 'OPTIONS':
def func(request):
response = request.ResponseClass()
response.headers = allow_header
return response
else:
def func(request):
raise HTTPMethodNotAllowed(headers=allow_header)
try:
self._call_before(func)(request)
response = func(request)
except:
response = self.handle_error(request)
finally:
self._call_after(func)(request)
# The callable might have returned just a string, which is OK,
# but we need to wrap it in a Response object
try:
# In Python 2, it could be a str or a unicode object
_string = basestring
except NameError:
# In Python 3, we are only interested in str objects
_string = str
if isinstance(response, _string) or not response:
response = request.ResponseClass(response)
if environ['REQUEST_METHOD'] == 'HEAD':
start_response(response.status, response.headerlist)
return ''
else:
return response(environ, start_response)
def _call_after(self, func):
try:
return self._find_attr(func, '__after__')
except AttributeError:
return lambda r: None
def _call_before(self, func):
try:
return self._find_attr(func, '__before__')
except AttributeError:
return lambda r: None
def _find_attr(self, obj, attr):
try:
# Object has the specified attribute itself
return getattr(obj, attr)
except AttributeError:
# Object is a bound method; look for the attribute on the instance
if hasattr(obj, '__self__'):
return self._find_attr(obj.__self__, attr)
# Object is a partial; look for the attribute on the inner function
elif hasattr(obj, 'func'):
return self._find_attr(obj.func, attr)
raise
[docs] 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
[docs] 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
[docs] 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):
self.start_response = start_response
self.called = False
def __call__(self, *args, **kwargs):
if not self.called:
self.called = True
self.start_response(*args, **kwargs)