Some reorganization, more tests, improved handling of errors, and

support for prioritized hooks.
This commit is contained in:
Jonathan LaCour
2010-09-30 10:11:21 -04:00
parent 31c2d31565
commit ccde2c07c8
14 changed files with 372 additions and 138 deletions

View File

@@ -2,7 +2,7 @@ Metadata-Version: 1.0
Name: pecan
Version: 0.1dev
Summary: A WSGI object-dispatching web framework, in the spirit of TurboGears, only much much smaller, with many fewer dependancies.
Home-page: http://code.google.com/p/pecan
Home-page: http://sf.net/p/pecan
Author: Jonathan LaCour
Author-email: jonathan@cleverdevil.org
License: BSD

View File

@@ -1,9 +1,13 @@
README
setup.cfg
setup.py
pecan/__init__.py
pecan/decorators.py
pecan/hooks.py
pecan/jsonify.py
pecan/pecan.py
pecan/routing.py
pecan/secure.py
pecan/templating.py
pecan.egg-info/PKG-INFO
pecan.egg-info/SOURCES.txt
@@ -11,4 +15,5 @@ pecan.egg-info/dependency_links.txt
pecan.egg-info/entry_points.txt
pecan.egg-info/requires.txt
pecan.egg-info/top_level.txt
pecan.egg-info/zip-safe
pecan.egg-info/zip-safe
tests/templates/__init__.py

View File

@@ -1,6 +1,8 @@
WebOb >= 0.9.8
simplejson >= 2.0.9
simplegeneric >= 0.7
Genshi >= 0.6
Kajiki >= 0.2.2
Mako >= 0.3
Mako >= 0.3
py >= 1.3.4
WebTest >= 1.2.2
Paste >= 1.7.5.1

View File

@@ -1 +1,2 @@
tests
pecan

View File

@@ -1,25 +1,16 @@
from paste.urlparser import StaticURLParser
from paste.cascade import Cascade
from pecan import Pecan, request, override_template
from pecan import Pecan, request, response, override_template
from decorators import expose
__all__ = [
'make_app', 'Pecan', 'request', 'response', 'override_template', 'expose'
]
def make_app(root, renderers = None,
default_renderer = None,
template_path = None,
hooks = None,
static_root = None):
kw = {}
if renderers is not None: kw['renderers'] = renderers
if default_renderer is not None: kw['default_renderer'] = default_renderer
if template_path is not None: kw['template_path'] = template_path
if hooks is not None: kw['hooks'] = hooks
def make_app(root, static_root=None, **kw):
app = Pecan(root, **kw)
if static_root:
app = Cascade([StaticURLParser(static_root), app])
return app

View File

@@ -1,4 +1,39 @@
from inspect import getmembers, ismethod
from routing import iscontroller
__all__ = ['PecanHook', 'TransactionHook', 'HookController']
def walk_controller(root_class, controller, hooks):
if hasattr(controller, '_lookup'):
# TODO: what about this?
pass
if not isinstance(controller, (int, dict)):
for name, value in controller.__dict__.iteritems():
if name == 'controller': continue
if name.startswith('__') and name.endswith('__'): continue
if iscontroller(value):
for hook in hooks:
value.pecan.setdefault('hooks', []).append(hook)
elif hasattr(value, '__class__'):
if name.startswith('__') and name.endswith('__'): continue
walk_controller(root_class, value, hooks)
class HookController(object):
__hooks__ = []
class __metaclass__(type):
def __init__(cls, name, bases, dict_):
walk_controller(cls, cls, dict_['__hooks__'])
class PecanHook(object):
priority = 100
def before(self, state):
pass

View File

@@ -1,7 +1,7 @@
try:
from json import JSONEncoder, dumps
except ImportError:
from simplejson import JSONEncoder, dumps
from simplejson import JSONEncoder, dumps
from datetime import datetime, date
from decimal import Decimal

View File

@@ -1,22 +1,27 @@
from templating import renderers
from webob import Request, Response, exc
from threading import local
from routing import lookup_controller
import string
from webob import Request, Response, exc
from threading import local
from itertools import chain
state = local()
class RequestWrapper(object):
def __getattr__(self, attr):
return getattr(state.request, attr)
def __setattr__(self, attr, value):
return setattr(state.request, attr, value)
def proxy(key):
class ObjectProxy(object):
def __getattr__(self, attr):
obj = getattr(state, key)
return getattr(obj, attr)
def __setattr__(self, attr, value):
obj = getattr(state, key)
return setattr(obj, attr, value)
return ObjectProxy()
request = RequestWrapper()
request = proxy('request')
response = proxy('response')
def override_template(template):
@@ -35,10 +40,6 @@ class Pecan(object):
self.default_renderer = default_renderer
self.hooks = hooks
self.template_path = template_path
self.translate = string.maketrans(
string.punctuation,
'_' * len(string.punctuation)
)
def get_content_type(self, format):
return {
@@ -51,71 +52,100 @@ class Pecan(object):
path = path.split('/')[1:]
node, remainder = lookup_controller(node, path)
return node
def __call__(self, environ, start_response):
# create the request object
state.request = Request(environ)
# lookup the controller
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):
for hook in state.hooks:
getattr(hook, hook_type)(*args)
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('.')
override_content_type = True
content_type = self.get_content_type(format)
controller = self.route(self.root, path)
# if we didn't find a controller, issue a 404
if controller is None:
response = Response()
response.status = 404
return response(environ, start_response)
# handle security
if controller.pecan.get('secured', False):
if not controller.pecan['check_permissions']():
raise exc.HTTPUnauthorized
# 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
for hook in self.hooks:
hook.before(state)
self.handle_hooks('before', state)
# get the result from the controller, properly handling wrap hooks
try:
result = controller(**dict(state.request.str_params))
# pull the template out based upon content type
template = controller.pecan.get('content_types', {}).get(content_type)
result = controller(**dict(state.request.str_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)
# handle template overrides
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)
elif ':' 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
if template:
renderer = self.renderers.get(self.default_renderer, self.template_path)
if template == 'json':
renderer = self.renderers.get('json', self.template_path)
elif ':' 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
response = Response(result)
if content_type:
response.content_type = content_type
except Exception, e:
# handle "error" hooks
for hook in self.hooks:
hook.on_error(state, e)
raise
# set the body content
if isinstance(result, unicode):
state.response.unicode_body = result
else:
return response(environ, start_response)
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 = []
# handle the request
try:
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
# handle "error" hooks
self.handle_hooks('on_error', state, e)
finally:
# handle "after" hooks
for hook in self.hooks:
hook.after(state)
del state.request
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

View File

@@ -1,4 +1,3 @@
from webob.exc import HTTPUnauthorized
from inspect import getmembers, ismethod
from routing import iscontroller

View File

@@ -1,8 +1,18 @@
from setuptools import setup, find_packages
import sys, os
from setuptools import setup, Command, find_packages
import sys, os, py
version = '0.1'
class PyTest(Command):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
import py
py.cmdline.pytest(py.std.sys.argv[2:])
setup(
name = 'pecan',
version = version,
@@ -17,6 +27,7 @@ setup(
packages = find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data = True,
zip_safe = True,
cmdclass = {'test': PyTest},
install_requires=[
"WebOb >= 0.9.8",
"simplegeneric >= 0.7",
@@ -30,4 +41,4 @@ setup(
entry_points = """
# -*- Entry points: -*-
""",
)
)

0
tests/__init__.py Normal file
View File

View File

@@ -1,4 +1,4 @@
from pecan import Pecan, expose
from pecan import Pecan, expose, request, response
from webtest import TestApp
class TestBase(object):
@@ -10,17 +10,17 @@ class TestBase(object):
return 'Hello, World!'
app = TestApp(Pecan(RootController()))
response = app.get('/')
assert response.status_int == 200
assert response.body == 'Hello, World!'
r = app.get('/')
assert r.status_int == 200
assert r.body == 'Hello, World!'
response = app.get('/index')
assert response.status_int == 200
assert response.body == 'Hello, World!'
r = app.get('/index')
assert r.status_int == 200
assert r.body == 'Hello, World!'
response = app.get('/index.html')
assert response.status_int == 200
assert response.body == 'Hello, World!'
r = app.get('/index.html')
assert r.status_int == 200
assert r.body == 'Hello, World!'
def test_object_dispatch(self):
class SubSubController(object):
@@ -56,9 +56,9 @@ class TestBase(object):
app = TestApp(Pecan(RootController()))
for path in ('/', '/deeper', '/sub', '/sub/deeper', '/sub/sub', '/sub/sub/deeper'):
response = app.get(path)
assert response.status_int == 200
assert response.body == path
r = app.get(path)
assert r.status_int == 200
assert r.body == path
def test_lookup(self):
class LookupController(object):
@@ -83,17 +83,17 @@ class TestBase(object):
return LookupController(someID), remainder
app = TestApp(Pecan(RootController()))
response = app.get('/')
assert response.status_int == 200
assert response.body == '/'
r = app.get('/')
assert r.status_int == 200
assert r.body == '/'
response = app.get('/100')
assert response.status_int == 200
assert response.body == '/100'
r = app.get('/100')
assert r.status_int == 200
assert r.body == '/100'
response = app.get('/100/name')
assert response.status_int == 200
assert response.body == '/100/name'
r = app.get('/100/name')
assert r.status_int == 200
assert r.body == '/100/name'
class TestEngines(object):
@@ -105,14 +105,14 @@ class TestEngines(object):
return dict(name=name)
app = TestApp(Pecan(RootController(), template_path='tests/templates'))
response = app.get('/')
assert response.status_int == 200
assert "<h1>Hello, Jonathan!</h1>" in response.body
r = app.get('/')
assert r.status_int == 200
assert "<h1>Hello, Jonathan!</h1>" in r.body
app = TestApp(Pecan(RootController(), template_path='tests/templates'))
response = app.get('/index.html?name=World')
assert response.status_int == 200
assert "<h1>Hello, World!</h1>" in response.body
r = app.get('/index.html?name=World')
assert r.status_int == 200
assert "<h1>Hello, World!</h1>" in r.body
def test_kajiki(self):
class RootController(object):
@@ -121,14 +121,14 @@ class TestEngines(object):
return dict(name=name)
app = TestApp(Pecan(RootController(), template_path='tests/templates'))
response = app.get('/')
assert response.status_int == 200
assert "<h1>Hello, Jonathan!</h1>" in response.body
r = app.get('/')
assert r.status_int == 200
assert "<h1>Hello, Jonathan!</h1>" in r.body
app = TestApp(Pecan(RootController(), template_path='tests/templates'))
response = app.get('/index.html?name=World')
assert response.status_int == 200
assert "<h1>Hello, World!</h1>" in response.body
r = app.get('/index.html?name=World')
assert r.status_int == 200
assert "<h1>Hello, World!</h1>" in r.body
def test_mako(self):
class RootController(object):
@@ -137,14 +137,14 @@ class TestEngines(object):
return dict(name=name)
app = TestApp(Pecan(RootController(), template_path='tests/templates'))
response = app.get('/')
assert response.status_int == 200
assert "<h1>Hello, Jonathan!</h1>" in response.body
r = app.get('/')
assert r.status_int == 200
assert "<h1>Hello, Jonathan!</h1>" in r.body
app = TestApp(Pecan(RootController(), template_path='tests/templates'))
response = app.get('/index.html?name=World')
assert response.status_int == 200
assert "<h1>Hello, World!</h1>" in response.body
r = app.get('/index.html?name=World')
assert r.status_int == 200
assert "<h1>Hello, World!</h1>" in r.body
def test_json(self):
from simplejson import loads
@@ -157,7 +157,7 @@ class TestEngines(object):
return expected_result
app = TestApp(Pecan(RootController()))
response = app.get('/')
assert response.status_int == 200
result = dict(loads(response.body))
r = app.get('/')
assert r.status_int == 200
result = dict(loads(r.body))
assert result == expected_result

View File

@@ -1,5 +1,5 @@
from pecan import Pecan, expose
from pecan.hooks import PecanHook, TransactionHook
from pecan.hooks import PecanHook, TransactionHook, HookController
from webtest import TestApp
@@ -72,6 +72,64 @@ class TestHooks(object):
assert run_hook[5] == 'after2'
assert run_hook[6] == 'after3'
def test_prioritized_hooks(self):
run_hook = []
class RootController(object):
@expose()
def index(self):
run_hook.append('inside')
return 'Hello, World!'
class SimpleHook(PecanHook):
def __init__(self, id):
self.id = str(id)
def before(self, state):
run_hook.append('before'+self.id)
def after(self, state):
run_hook.append('after'+self.id)
def on_error(self, state, e):
run_hook.append('error'+self.id)
papp = Pecan(RootController(), hooks=[
SimpleHook(1), SimpleHook(2), SimpleHook(3)
])
app = TestApp(papp)
response = app.get('/')
assert response.status_int == 200
assert response.body == 'Hello, World!'
assert len(run_hook) == 7
assert run_hook[0] == 'before1'
assert run_hook[1] == 'before2'
assert run_hook[2] == 'before3'
assert run_hook[3] == 'inside'
assert run_hook[4] == 'after1'
assert run_hook[5] == 'after2'
assert run_hook[6] == 'after3'
for i in range(len(run_hook)): run_hook.pop()
papp.hooks[0].priority = 3
papp.hooks[1].priority = 2
papp.hooks[2].priority = 1
response = app.get('/')
assert response.status_int == 200
assert response.body == 'Hello, World!'
assert len(run_hook) == 7
assert run_hook[0] == 'before3'
assert run_hook[1] == 'before2'
assert run_hook[2] == 'before1'
assert run_hook[3] == 'inside'
assert run_hook[4] == 'after3'
assert run_hook[5] == 'after2'
assert run_hook[6] == 'after1'
def test_transaction_hook(self):
run_hook = []
@@ -142,4 +200,106 @@ class TestHooks(object):
assert run_hook[0] == 'start'
assert run_hook[1] == 'rollback'
assert run_hook[2] == 'clear'
def test_basic_isolated_hook(self):
run_hook = []
class SimpleHook(PecanHook):
def before(self, state):
run_hook.append('before')
def after(self, state):
run_hook.append('after')
def on_error(self, state, e):
run_hook.append('error')
class SubController(HookController):
__hooks__ = [SimpleHook()]
@expose()
def index(self):
run_hook.append('inside_sub')
return 'Inside here!'
class RootController(object):
@expose()
def index(self):
run_hook.append('inside')
return 'Hello, World!'
sub = SubController()
app = TestApp(Pecan(RootController()))
response = app.get('/')
assert response.status_int == 200
assert response.body == 'Hello, World!'
assert len(run_hook) == 1
assert run_hook[0] == 'inside'
for i in range(len(run_hook)): run_hook.pop()
response = app.get('/sub')
assert response.status_int == 200
assert response.body == 'Inside here!'
assert len(run_hook) == 3
assert run_hook[0] == 'before'
assert run_hook[1] == 'inside_sub'
assert run_hook[2] == 'after'
def test_isolated_hook_with_global_hook(self):
run_hook = []
class SimpleHook(PecanHook):
def __init__(self, id):
self.id = str(id)
def before(self, state):
run_hook.append('before'+self.id)
def after(self, state):
run_hook.append('after'+self.id)
def on_error(self, state, e):
run_hook.append('error'+self.id)
class SubController(HookController):
__hooks__ = [SimpleHook(2)]
@expose()
def index(self):
run_hook.append('inside_sub')
return 'Inside here!'
class RootController(object):
@expose()
def index(self):
run_hook.append('inside')
return 'Hello, World!'
sub = SubController()
app = TestApp(Pecan(RootController(), hooks=[SimpleHook(1)]))
response = app.get('/')
assert response.status_int == 200
assert response.body == 'Hello, World!'
assert len(run_hook) == 3
assert run_hook[0] == 'before1'
assert run_hook[1] == 'inside'
assert run_hook[2] == 'after1'
for i in range(len(run_hook)): run_hook.pop()
response = app.get('/sub')
assert response.status_int == 200
assert response.body == 'Inside here!'
assert len(run_hook) == 5
assert run_hook[0] == 'before2'
assert run_hook[1] == 'before1'
assert run_hook[2] == 'inside_sub'
assert run_hook[3] == 'after2'
assert run_hook[4] == 'after1'

View File

@@ -1,7 +1,7 @@
from pecan import expose, make_app
from pecan.secure import secure, unlocked, SecureController
from webob import exc
from webtest import TestApp
from webtest import TestApp, AppError
from py.test import raises
class TestSecure(object):
@@ -50,11 +50,11 @@ class TestSecure(object):
assert response.status_int == 200
assert response.body == 'Sure thing'
with raises(exc.HTTPUnauthorized):
response = app.get('/locked')
response = app.get('/locked', expect_errors=True)
assert response.status_int == 401
with raises(exc.HTTPUnauthorized):
response = app.get('/secret')
response = app.get('/secret', expect_errors=True)
assert response.status_int == 401
response = app.get('/secret/allowed')
assert response.status_int == 200