Files
deb-python-pecan/pecan/pecan.py
2010-12-01 12:02:11 -05:00

255 lines
8.5 KiB
Python

from templating import renderers
from routing import lookup_controller
from webob import Request, Response, exc
from threading import local
from itertools import chain
from formencode import Invalid
from paste.recursive import ForwardRequestException
try:
from json import loads
except ImportError:
from simplejson import loads
state = local()
def proxy(key):
class ObjectProxy(object):
def __getattr__(self, attr):
obj = getattr(state, key)
if attr == 'validation_error':
return getattr(obj, attr, None)
return getattr(obj, attr)
def __setattr__(self, attr, value):
obj = getattr(state, key)
return setattr(obj, attr, value)
return ObjectProxy()
request = proxy('request')
context = proxy('request.context')
response = proxy('response')
def override_template(template):
request.override_template = template
def redirect(location, internal=False):
if internal:
raise ForwardRequestException(location)
raise exc.HTTPFound(location=location)
def error_for(field):
if request.validation_error is None: return ''
return request.validation_error.error_dict.get(field, '')
class Pecan(object):
def __init__(self, root,
renderers = renderers,
default_renderer = 'kajiki',
template_path = 'templates',
hooks = []):
self.root = root
self.renderers = renderers
self.default_renderer = default_renderer
self.hooks = hooks
self.template_path = template_path
def get_content_type(self, format):
return {
'html' : 'text/html',
'xhtml' : 'text/html',
'json' : 'application/json'
}.get(format, 'text/html')
def route(self, node, path):
path = path.split('/')[1:]
node, remainder = lookup_controller(node, path)
return node, remainder
def handle_security(self, controller):
if controller.pecan.get('secured', False):
if not controller.pecan['check_permissions']():
raise exc.HTTPUnauthorized
def determine_hooks(self, controller):
return list(
sorted(
chain(controller.pecan.get('hooks', []), self.hooks),
lambda x,y: cmp(x.priority, y.priority)
)
)
def handle_hooks(self, hook_type, *args):
if hook_type == 'before':
hooks = state.hooks
else:
hooks = reversed(state.hooks)
for hook in hooks:
getattr(hook, hook_type)(*args)
def get_params(self, all_params, remainder, argspec, im_self):
valid_params = dict()
positional_params = []
if im_self is not None:
positional_params.append(im_self)
# handle params that are POST or GET variables first
for param_name, param_value in all_params.iteritems():
if param_name in argspec[0]:
valid_params[param_name] = param_value
# handle positional arguments
used = set()
for i, value in enumerate(remainder):
if len(argspec.args) > (i+1):
if valid_params.get(argspec.args[i+1]) is None:
used.add(i)
valid_params[argspec.args[i+1]] = value
# handle unconsumed positional arguments
if len(used) < len(remainder) and argspec.varargs is not None:
for i, value in enumerate(remainder):
if i not in used:
positional_params.append(value)
return valid_params, positional_params
def validate(self, schema, params=None, json=False):
to_validate = params
if json:
to_validate = loads(request.body)
return schema.to_python(to_validate)
def handle_request(self):
# lookup the controller, respecting content-type as requested
# by the file extension on the URI
path = state.request.path
content_type = None
if '.' in path.split('/')[-1]:
path, format = path.split('.')
content_type = self.get_content_type(format)
controller, remainder = self.route(self.root, path)
if controller.pecan.get('generic_handler'):
raise exc.HTTPNotFound
# handle generic controllers
im_self = None
if controller.pecan.get('generic'):
im_self = controller.im_self
handlers = controller.pecan['generic_handlers']
controller = handlers.get(request.method, handlers['DEFAULT'])
# determine content type
if content_type is None:
content_type = controller.pecan.get('content_type', 'text/html')
# handle security
self.handle_security(controller)
# get a sorted list of hooks, by priority
state.hooks = self.determine_hooks(controller)
# handle "before" hooks
self.handle_hooks('before', state)
# fetch and validate any parameters
params, positional_params = self.get_params(
dict(state.request.str_params),
remainder,
controller.pecan['argspec'],
im_self
)
if 'schema' in controller.pecan:
request.validation_error = None
try:
params = self.validate(
controller.pecan['schema'],
json = controller.pecan['validate_json'],
params = params
)
except Invalid, e:
request.validation_error = e
if controller.pecan['error_handler'] is not None:
redirect(controller.pecan['error_handler'], internal=True)
if controller.pecan['validate_json']: params = dict(data=params)
# get the result from the controller
result = controller(*positional_params, **params)
# pull the template out based upon content type and handle overrides
template = controller.pecan.get('content_types', {}).get(content_type)
template = getattr(request, 'override_template', template)
# if there is a template, render it
if template:
renderer = self.renderers.get(self.default_renderer, self.template_path)
if template == 'json':
renderer = self.renderers.get('json', self.template_path)
else:
result['error_for'] = error_for
if ':' in template:
renderer = self.renderers.get(template.split(':')[0], self.template_path)
template = template.split(':')[1]
result = renderer.render(template, result)
content_type = renderer.content_type
# set the body content
if isinstance(result, unicode):
state.response.unicode_body = result
else:
state.response.body = result
# set the content type
if content_type:
state.response.content_type = content_type
def __call__(self, environ, start_response):
# create the request and response object
state.request = Request(environ)
state.response = Response()
state.hooks = []
state.app = self
# handle the request
try:
# add context to the request
state.request.context = {}
self.handle_request()
except Exception, e:
# if this is an HTTP Exception, set it as the response
if isinstance(e, exc.HTTPException):
state.response = e
# if this is not an internal redirect, run error hooks
if not isinstance(e, ForwardRequestException):
self.handle_hooks('on_error', state, e)
if not isinstance(e, exc.HTTPException):
raise
finally:
# handle "after" hooks
self.handle_hooks('after', state)
# get the response
try:
return state.response(environ, start_response)
finally:
# clean up state
del state.request
del state.response
del state.hooks