diff options
Diffstat (limited to 'pecan/routing.py')
-rw-r--r-- | pecan/routing.py | 157 |
1 files changed, 155 insertions, 2 deletions
diff --git a/pecan/routing.py b/pecan/routing.py index 91c9fe3..29a294c 100644 --- a/pecan/routing.py +++ b/pecan/routing.py @@ -1,11 +1,99 @@ +import re import warnings +from inspect import getmembers, ismethod from webob import exc +import six from .secure import handle_security, cross_boundary from .util import iscontroller, getargspec, _cfg -__all__ = ['lookup_controller', 'find_object'] +__all__ = ['lookup_controller', 'find_object', 'route'] +__observed_controllers__ = set() +__custom_routes__ = {} + + +def route(*args): + """ + This function is used to define an explicit route for a path segment. + + You generally only want to use this in situations where your desired path + segment is not a valid Python variable/function name. + + For example, if you wanted to be able to route to: + + /path/with-dashes/ + + ...the following is invalid Python syntax:: + + class Controller(object): + + with-dashes = SubController() + + ...so you would instead define the route explicitly:: + + class Controller(object): + pass + + pecan.route(Controller, 'with-dashes', SubController()) + """ + + def _validate_route(route): + if not isinstance(route, six.string_types): + raise TypeError('%s must be a string' % route) + + if not re.match('^[0-9a-zA-Z-_$\(\),;:]+$', route): + raise ValueError( + '%s must be a valid path segment. Keep in mind ' + 'that path segments should not contain path separators ' + '(e.g., /) ' % route + ) + + if len(args) == 2: + # The handler in this situation is a @pecan.expose'd callable, + # and is generally only used by the @expose() decorator itself. + # + # This sets a special attribute, `custom_route` on the callable, which + # pecan's routing logic knows how to make use of (as a special case) + route, handler = args + if ismethod(handler): + handler = handler.__func__ + if not iscontroller(handler): + raise TypeError( + '%s must be a callable decorated with @pecan.expose' % handler + ) + obj, attr, value = handler, 'custom_route', route + + if handler.__name__ in ('_lookup', '_default', '_route'): + raise ValueError( + '%s is a special method in pecan and cannot be used in ' + 'combination with custom path segments.' % handler.__name__ + ) + elif len(args) == 3: + # This is really just a setattr on the parent controller (with some + # additional validation for the path segment itself) + _, route, handler = args + obj, attr, value = args + + if hasattr(obj, attr): + raise RuntimeError( + ( + "%(module)s.%(class)s already has an " + "existing attribute named \"%(route)s\"." % { + 'module': obj.__module__, + 'class': obj.__name__, + 'route': attr + } + ), + ) + else: + raise TypeError( + 'pecan.route should be called in the format ' + 'route(ParentController, "path-segment", SubController())' + ) + + _validate_route(route) + setattr(obj, attr, value) class PecanNotFound(Exception): @@ -105,7 +193,15 @@ def find_object(obj, remainder, notfound_handlers, request): if obj is None: raise PecanNotFound if iscontroller(obj): - return obj, remainder + if getattr(obj, 'custom_route', None) is None: + return obj, remainder + + _detect_custom_path_segments(obj) + + if remainder: + custom_route = __custom_routes__.get((obj.__class__, remainder[0])) + if custom_route: + return getattr(obj, custom_route), remainder[1:] # are we traversing to another controller cross_boundary(prev_obj, obj) @@ -168,3 +264,60 @@ def find_object(obj, remainder, notfound_handlers, request): if request.method in _cfg(prev_obj.index).get('generic_handlers', {}): return prev_obj.index, prev_remainder + + +def _detect_custom_path_segments(obj): + # Detect custom controller routes (on the initial traversal) + if obj.__class__.__module__ == '__builtin__': + return + + attrs = set(dir(obj)) + + if obj.__class__ not in __observed_controllers__: + for key, val in getmembers(obj): + if iscontroller(val) and isinstance( + getattr(val, 'custom_route', None), + six.string_types + ): + route = val.custom_route + + # Detect class attribute name conflicts + for conflict in attrs.intersection(set((route,))): + raise RuntimeError( + ( + "%(module)s.%(class)s.%(function)s has " + "a custom path segment, \"%(route)s\", " + "but %(module)s.%(class)s already has an " + "existing attribute named \"%(route)s\"." % { + 'module': obj.__class__.__module__, + 'class': obj.__class__.__name__, + 'function': val.__name__, + 'route': conflict + } + ), + ) + + existing = __custom_routes__.get( + (obj.__class__, route) + ) + if existing: + # Detect custom path conflicts between functions + raise RuntimeError( + ( + "%(module)s.%(class)s.%(function)s and " + "%(module)s.%(class)s.%(other)s have a " + "conflicting custom path segment, " + "\"%(route)s\"." % { + 'module': obj.__class__.__module__, + 'class': obj.__class__.__name__, + 'function': val.__name__, + 'other': existing, + 'route': route + } + ), + ) + + __custom_routes__[ + (obj.__class__, route) + ] = key + __observed_controllers__.add(obj.__class__) |