summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormarkmcclain <mark.mcclain@dreamhost.com>2013-05-07 08:11:07 -0700
committermarkmcclain <mark.mcclain@dreamhost.com>2013-05-07 08:11:07 -0700
commit32fb9224ae5a0f74ca82efe7a2e8c1302eaf05e7 (patch)
tree0f7c49c05ddde690d55468d6399dccfe6b2b767f
parent57d1073ac4492bc21fe414ef5f9bd51670d5910a (diff)
parent28437cc297fd0eddcea833b1e1bd90ab8022be16 (diff)
downloadpecan-32fb9224ae5a0f74ca82efe7a2e8c1302eaf05e7.tar.gz
Merge pull request #198 from ryanpetrello/rest-controller-lookup-support
Add support for ``_lookup`` methods as a fallback in RestController.
-rw-r--r--pecan/rest.py37
-rw-r--r--pecan/routing.py37
-rw-r--r--pecan/tests/test_rest.py219
3 files changed, 276 insertions, 17 deletions
diff --git a/pecan/rest.py b/pecan/rest.py
index 08b5efb..542ccd5 100644
--- a/pecan/rest.py
+++ b/pecan/rest.py
@@ -1,8 +1,10 @@
from inspect import getargspec, ismethod
+from webob import exc
+
from core import abort, request
from decorators import expose
-from routing import lookup_controller
+from routing import lookup_controller, handle_lookup_traversal
from util import iscontroller
@@ -53,11 +55,42 @@ class RestController(object):
# handle the request
handler = getattr(self, '_handle_%s' % method, self._handle_custom)
- result = handler(method, args)
+
+ try:
+ result = handler(method, args)
+
+ #
+ # If the signature of the handler does not match the number
+ # of remaining positional arguments, attempt to handle
+ # a _lookup method (if it exists)
+ #
+ argspec = getargspec(result[0])
+ num_args = len(argspec[0][1:])
+ if num_args < len(args):
+ _lookup_result = self._handle_lookup(args)
+ if _lookup_result:
+ return _lookup_result
+ except exc.HTTPNotFound:
+ #
+ # If the matching handler results in a 404, attempt to handle
+ # a _lookup method (if it exists)
+ #
+ _lookup_result = self._handle_lookup(args)
+ if _lookup_result:
+ return _lookup_result
+ raise
# return the result
return result
+ def _handle_lookup(self, args):
+ # check for lookup controllers
+ lookup = getattr(self, '_lookup', None)
+ if args and iscontroller(lookup):
+ result = handle_lookup_traversal(lookup, args)
+ if result:
+ return lookup_controller(*result)
+
def _find_controller(self, *args):
'''
Returns the appropriate controller for routing a custom action.
diff --git a/pecan/routing.py b/pecan/routing.py
index d38c22e..a7a6d4f 100644
--- a/pecan/routing.py
+++ b/pecan/routing.py
@@ -1,3 +1,5 @@
+import warnings
+
from webob import exc
from secure import handle_security, cross_boundary
@@ -42,25 +44,30 @@ def lookup_controller(obj, url_path):
else:
# Notfound handler is an internal redirect, so continue
# traversal
- try:
- result = obj(*remainder)
- if result:
- prev_obj = obj
- obj, remainder = result
- # crossing controller boundary
- cross_boundary(prev_obj, obj)
- break
- except TypeError, te:
- import warnings
- msg = 'Got exception calling lookup(): %s (%s)'
- warnings.warn(
- msg % (te, te.args),
- RuntimeWarning
- )
+ result = handle_lookup_traversal(obj, remainder)
+ if result:
+ return lookup_controller(*result)
else:
raise exc.HTTPNotFound
+def handle_lookup_traversal(obj, args):
+ try:
+ result = obj(*args)
+ if result:
+ prev_obj = obj
+ obj, remainder = result
+ # crossing controller boundary
+ cross_boundary(prev_obj, obj)
+ return result
+ except TypeError as te:
+ msg = 'Got exception calling lookup(): %s (%s)'
+ warnings.warn(
+ msg % (te, te.args),
+ RuntimeWarning
+ )
+
+
def find_object(obj, remainder, notfound_handlers):
'''
'Walks' the url path in search of an action for which a controller is
diff --git a/pecan/tests/test_rest.py b/pecan/tests/test_rest.py
index 6cd7bba..d5e7f5e 100644
--- a/pecan/tests/test_rest.py
+++ b/pecan/tests/test_rest.py
@@ -943,3 +943,222 @@ class TestRestController(PecanTestCase):
assert r.status_int == 200
assert r.namespace['foo'] == 'bar'
assert r.namespace['spam'] == 'eggs'
+
+ def test_nested_rest_with_lookup(self):
+
+ class SubController(RestController):
+
+ @expose()
+ def get_all(self):
+ return "SUB"
+
+ class FinalController(RestController):
+
+ def __init__(self, id_):
+ self.id_ = id_
+
+ @expose()
+ def get_all(self):
+ return "FINAL-%s" % self.id_
+
+ @expose()
+ def post(self):
+ return "POST-%s" % self.id_
+
+ class LookupController(RestController):
+
+ sub = SubController()
+
+ def __init__(self, id_):
+ self.id_ = id_
+
+ @expose()
+ def _lookup(self, id_, *remainder):
+ return FinalController(id_), remainder
+
+ @expose()
+ def get_all(self):
+ raise AssertionError("Never Reached")
+
+ @expose()
+ def post(self):
+ return "POST-LOOKUP-%s" % self.id_
+
+ @expose()
+ def put(self, id_):
+ return "PUT-LOOKUP-%s-%s" % (self.id_, id_)
+
+ @expose()
+ def delete(self, id_):
+ return "DELETE-LOOKUP-%s-%s" % (self.id_, id_)
+
+ class FooController(RestController):
+
+ @expose()
+ def _lookup(self, id_, *remainder):
+ return LookupController(id_), remainder
+
+ @expose()
+ def get_one(self, id_):
+ return "GET ONE"
+
+ @expose()
+ def get_all(self):
+ return "INDEX"
+
+ @expose()
+ def post(self):
+ return "POST"
+
+ @expose()
+ def put(self, id_):
+ return "PUT-%s" % id_
+
+ @expose()
+ def delete(self, id_):
+ return "DELETE-%s" % id_
+
+ class RootController(RestController):
+ foo = FooController()
+
+ app = TestApp(make_app(RootController()))
+
+ r = app.get('/foo')
+ assert r.status_int == 200
+ assert r.body == 'INDEX'
+
+ r = app.post('/foo')
+ assert r.status_int == 200
+ assert r.body == 'POST'
+
+ r = app.get('/foo/1')
+ assert r.status_int == 200
+ assert r.body == 'GET ONE'
+
+ r = app.post('/foo/1')
+ assert r.status_int == 200
+ assert r.body == 'POST-LOOKUP-1'
+
+ r = app.put('/foo/1')
+ assert r.status_int == 200
+ assert r.body == 'PUT-1'
+
+ r = app.delete('/foo/1')
+ assert r.status_int == 200
+ assert r.body == 'DELETE-1'
+
+ r = app.put('/foo/1/2')
+ assert r.status_int == 200
+ assert r.body == 'PUT-LOOKUP-1-2'
+
+ r = app.delete('/foo/1/2')
+ assert r.status_int == 200
+ assert r.body == 'DELETE-LOOKUP-1-2'
+
+ r = app.get('/foo/1/2')
+ assert r.status_int == 200
+ assert r.body == 'FINAL-2'
+
+ r = app.post('/foo/1/2')
+ assert r.status_int == 200
+ assert r.body == 'POST-2'
+
+ def test_dynamic_rest_lookup(self):
+ class BarController(RestController):
+ @expose()
+ def get_all(self):
+ return "BAR"
+
+ @expose()
+ def put(self):
+ return "PUT_BAR"
+
+ @expose()
+ def delete(self):
+ return "DELETE_BAR"
+
+ class BarsController(RestController):
+ @expose()
+ def _lookup(self, id_, *remainder):
+ return BarController(), remainder
+
+ @expose()
+ def get_all(self):
+ return "BARS"
+
+ @expose()
+ def post(self):
+ return "POST_BARS"
+
+ class FooController(RestController):
+ bars = BarsController()
+
+ @expose()
+ def get_all(self):
+ return "FOO"
+
+ @expose()
+ def put(self):
+ return "PUT_FOO"
+
+ @expose()
+ def delete(self):
+ return "DELETE_FOO"
+
+ class FoosController(RestController):
+ @expose()
+ def _lookup(self, id_, *remainder):
+ return FooController(), remainder
+
+ @expose()
+ def get_all(self):
+ return "FOOS"
+
+ @expose()
+ def post(self):
+ return "POST_FOOS"
+
+ class RootController(RestController):
+ foos = FoosController()
+
+ app = TestApp(make_app(RootController()))
+
+ r = app.get('/foos')
+ assert r.status_int == 200
+ assert r.body == 'FOOS'
+
+ r = app.post('/foos')
+ assert r.status_int == 200
+ assert r.body == 'POST_FOOS'
+
+ r = app.get('/foos/foo')
+ assert r.status_int == 200
+ assert r.body == 'FOO'
+
+ r = app.put('/foos/foo')
+ assert r.status_int == 200
+ assert r.body == 'PUT_FOO'
+
+ r = app.delete('/foos/foo')
+ assert r.status_int == 200
+ assert r.body == 'DELETE_FOO'
+
+ r = app.get('/foos/foo/bars')
+ assert r.status_int == 200
+ assert r.body == 'BARS'
+
+ r = app.post('/foos/foo/bars')
+ assert r.status_int == 200
+ assert r.body == 'POST_BARS'
+
+ r = app.get('/foos/foo/bars/bar')
+ assert r.status_int == 200
+ assert r.body == 'BAR'
+
+ r = app.put('/foos/foo/bars/bar')
+ assert r.status_int == 200
+ assert r.body == 'PUT_BAR'
+
+ r = app.delete('/foos/foo/bars/bar')
+ assert r.status_int == 200
+ assert r.body == 'DELETE_BAR'