244 lines
8.0 KiB
Python
244 lines
8.0 KiB
Python
# 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 import util
|
|
from milla.controllers import FaviconController
|
|
from webob.exc import HTTPNotFound, WSGIHTTPException, HTTPMethodNotAllowed
|
|
import milla.dispatch.traversal
|
|
import os
|
|
import sys
|
|
|
|
|
|
__all__ = [
|
|
'Application',
|
|
'BaseApplication',
|
|
]
|
|
|
|
|
|
class BaseApplication(object):
|
|
'''Base class for Milla applications
|
|
|
|
This class can be used by applications that need to customize the
|
|
behavior of the framework. In most cases, :py:class:`Application`
|
|
instances can be created directly and a sublcass is not necessary.
|
|
'''
|
|
|
|
DEFAULT_CONFIG = {}
|
|
|
|
def __init__(self):
|
|
self.config = self.DEFAULT_CONFIG.copy()
|
|
self.setup_routes()
|
|
|
|
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
|
|
|
|
def update_config(self, filename):
|
|
'''Update application configuration from a file
|
|
|
|
:param filename: Path to configuration file
|
|
|
|
This method will update the application configuration using
|
|
the values found in the specified configuration file. If the
|
|
specified file does not exist or is not accessible, no
|
|
changes will be made.
|
|
|
|
The configuration file will be read using
|
|
:py:func:`milla.util.read_config`.
|
|
'''
|
|
|
|
if filename and os.access(filename, os.R_OK):
|
|
self.config.update(util.read_config(filename))
|
|
|
|
def setup_routes(self):
|
|
'''Initialize the request dispatcher
|
|
|
|
The default implementation of this method does nothing, but it
|
|
is called when an instance of :py:class:`BaseApplication` is
|
|
created, and can be used by subclasses to set up the request
|
|
dispatcher.
|
|
'''
|
|
|
|
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
|
|
util.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 Application(BaseApplication):
|
|
'''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``.
|
|
'''
|
|
|
|
DEFAULT_CONFIG = {
|
|
'milla.favicon': True,
|
|
}
|
|
|
|
def __init__(self, obj):
|
|
super(Application, self).__init__()
|
|
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
|
|
|
|
|
|
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)
|