import warnings from webob import exc from .secure import handle_security, cross_boundary from .util import iscontroller, getargspec, _cfg __all__ = ['lookup_controller', 'find_object'] class PecanNotFound(Exception): pass class NonCanonicalPath(Exception): ''' Exception Raised when a non-canonical path is encountered when 'walking' the URI. This is typically a ``POST`` request which requires a trailing slash. ''' def __init__(self, controller, remainder): self.controller = controller self.remainder = remainder def lookup_controller(obj, remainder, request=None): ''' Traverses the requested url path and returns the appropriate controller object, including default routes. Handles common errors gracefully. ''' if request is None: warnings.warn( ( "The function signature for %s.lookup_controller is changing " "in the next version of pecan.\nPlease update to: " "`lookup_controller(self, obj, remainder, request)`." % ( __name__, ) ), DeprecationWarning ) notfound_handlers = [] while True: try: obj, remainder = find_object(obj, remainder, notfound_handlers, request) handle_security(obj) return obj, remainder except (exc.HTTPNotFound, PecanNotFound): while notfound_handlers: name, obj, remainder = notfound_handlers.pop() if name == '_default': # Notfound handler is, in fact, a controller, so stop # traversal return obj, remainder else: # Notfound handler is an internal redirect, so continue # traversal result = handle_lookup_traversal(obj, remainder) if result: # If no arguments are passed to the _lookup, yet the # argspec requires at least one, raise a 404 if ( remainder == [''] and len(obj._pecan['argspec'].args) > 1 ): raise exc.HTTPNotFound obj_, remainder_ = result return lookup_controller(obj_, remainder_, request) 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, request): ''' 'Walks' the url path in search of an action for which a controller is implemented and returns that controller object along with what's left of the remainder. ''' prev_obj = None while True: if obj is None: raise PecanNotFound if iscontroller(obj): return obj, remainder # are we traversing to another controller cross_boundary(prev_obj, obj) try: next_obj, rest = remainder[0], remainder[1:] if next_obj == '': index = getattr(obj, 'index', None) if iscontroller(index): return index, rest except IndexError: # the URL has hit an index method without a trailing slash index = getattr(obj, 'index', None) if iscontroller(index): raise NonCanonicalPath(index, []) default = getattr(obj, '_default', None) if iscontroller(default): notfound_handlers.append(('_default', default, remainder)) lookup = getattr(obj, '_lookup', None) if iscontroller(lookup): notfound_handlers.append(('_lookup', lookup, remainder)) route = getattr(obj, '_route', None) if iscontroller(route): if len(getargspec(route).args) == 2: warnings.warn( ( "The function signature for %s.%s._route is changing " "in the next version of pecan.\nPlease update to: " "`def _route(self, args, request)`." % ( obj.__class__.__module__, obj.__class__.__name__ ) ), DeprecationWarning ) next_obj, next_remainder = route(remainder) else: next_obj, next_remainder = route(remainder, request) cross_boundary(route, next_obj) return next_obj, next_remainder if not remainder: raise PecanNotFound prev_remainder = remainder prev_obj = obj remainder = rest obj = getattr(obj, next_obj, None) # Last-ditch effort: if there's not a matching subcontroller, no # `_default`, no `_lookup`, and no `_route`, look to see if there's # an `index` that has a generic method defined for the current request # method. if not obj and not notfound_handlers and hasattr(prev_obj, 'index'): if request.method in _cfg(prev_obj.index).get('generic_handlers', {}): return prev_obj.index, prev_remainder