summaryrefslogtreecommitdiff
path: root/pecan/routing.py
diff options
context:
space:
mode:
Diffstat (limited to 'pecan/routing.py')
-rw-r--r--pecan/routing.py157
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__)