diff options
author | markmcclain <mark.mcclain@dreamhost.com> | 2013-05-07 08:11:07 -0700 |
---|---|---|
committer | markmcclain <mark.mcclain@dreamhost.com> | 2013-05-07 08:11:07 -0700 |
commit | 32fb9224ae5a0f74ca82efe7a2e8c1302eaf05e7 (patch) | |
tree | 0f7c49c05ddde690d55468d6399dccfe6b2b767f /pecan | |
parent | 57d1073ac4492bc21fe414ef5f9bd51670d5910a (diff) | |
parent | 28437cc297fd0eddcea833b1e1bd90ab8022be16 (diff) | |
download | pecan-32fb9224ae5a0f74ca82efe7a2e8c1302eaf05e7.tar.gz |
Merge pull request #198 from ryanpetrello/rest-controller-lookup-support
Add support for ``_lookup`` methods as a fallback in RestController.
Diffstat (limited to 'pecan')
-rw-r--r-- | pecan/rest.py | 37 | ||||
-rw-r--r-- | pecan/routing.py | 37 | ||||
-rw-r--r-- | pecan/tests/test_rest.py | 219 |
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' |