Skip to content

Instantly share code, notes, and snippets.

@sj26
Created February 17, 2010 04:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sj26/306296 to your computer and use it in GitHub Desktop.
Save sj26/306296 to your computer and use it in GitHub Desktop.
# Simple routes implementation in Python
# TODO: Decouple from app framework
# TODO: Reverse routes
import re
from exceptions import HTTPException, HTTPNotFound, HTTPMethodNotAllowed
class Route(object):
''' Turns a textual route into a usable dispatching route. '''
class LiteralToken(object):
__slots__ = ('value',)
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
@property
def _re(self):
return re.escape(self.value)
class Token(LiteralToken):
__slots__ = ('greedy',)
_valid_id_re = re.compile('^[^>]$')
def __init__(self, value, greedy=False):
super(Route.Token, self).__init__(value)
self.greedy = greedy
def __str__(self):
s = self.value
return ':' + self.value + (self.greedy and '*' or '')
def __repr__(self):
return ''
@property
def _re(self):
return (self.greedy and r'(?P<%s>.*)' or r'(?P<%s>[^/]*)') % (self.value,)
__slots__ = ('tokens', 'route_re', 'methods', 'handler', 'args', 'kwargs')
tokenizer_re = re.compile(r'(:\(([^\)]+)\)|:(\w+)|\*\(([^\)]+)\)|\*(\w+)|[^\:\*]+)')
def __init__(self, route, methods, handler, *args, **kwargs):
tokens = []
for match in self.tokenizer_re.finditer(route):
groups = match.groups()
literal, ungreedy, greedy = groups[0], groups[1] or groups[2], groups[3] or groups[4]
if ungreedy:
tokens.append(self.Token(ungreedy))
elif greedy:
tokens.append(self.Token(greedy, True))
else:
tokens.append(self.LiteralToken(literal))
self.tokens = tuple(tokens)
self.route_re = re.compile('^' + ''.join([token._re for token in self.tokens]) + '$')
self.methods = methods
self.handler = handler
self.args = args or None
self.kwargs = kwargs or None
def __str__(self):
return ''.join(str(token) for token in self.tokens)
def __repr__(self):
# XXX: repr args and kwargs?
return 'Route(%s%s, %r)' % (str(self), self.methods and (', ' + ', '.join(repr(self.methods))) or '', self.handler)
def match(self, url):
match = self.route_re.match(url)
if match:
args = self.args and tuple(self.args) or tuple()
kwargs = self.kwargs and dict(self.kwargs) or dict()
kwargs.update(match.groupdict())
return True, args, kwargs
return (False, None, None)
class RouteDispatcher(object):
'''
A dispatcher based on routes similar to Rails and Groovie Routes.
Dispatches requests to handlers with a set of routes using the
.route decorator. Raises HTTPMethodNotAllowed as appropriate
and defaults to raising HTTPNotFound as the default handler which
can be overridden.
Routes are a simple way of expressing URL patterns which can be
used for dispatching and linking. The example route
'/article/:name' will match all URLs starting with '/article/'
followed by any word-like characters which become a keyword-
argument to the handler named 'name'.
For keyword arguments containing non-word characters like '_' you
can use parentheses around the argument name e.g.
':(article_name)'. If you would like your argument to match all
non-path-separating characters (anything but '/') add an asterisk
('*') e.g. ':slug*'.
The dispatcher will attempt to find a matching route in the same
order that the routes are added. If you need control over the
order in which routes are tried then you can list the route
handle addition like so:
>>> dispatcher = RouteDispatcher()
>>> dispatcher.route('/special').to(special_handler)
>>> dispatcher.route('/:others').to(normal_handler)
'''
def __init__(self, default_handler=None, error_handler=None, exception_handler=None):
'''
default_handler will be used when no route matches.
'''
self.routes = []
self.handlers = dict()
if default_handler:
self.default_handler = default_handler
if error_handler:
self.error_handler = error_handler
if exception_handler:
self.exception_handler = exception_handler
def route(self, route, *methods, **defaults):
''' Decorator to add handlers to this dispatcher. '''
if not methods:
methods = ('GET', 'HEAD')
elif 'GET' in methods and 'HEAD' not in methods:
methods = tuple(methods + ('HEAD',))
def _decorator(handler):
self.routes.append(Route(route, methods, handler, **defaults))
return handler
_decorator.to = _decorator
return _decorator
def route_default(self, handler):
self.default_handler = handler
def route_errors(self, handler):
self.error_handler = handler
def route_exceptions(self, handler):
self.exception_handler = handler
def dispatch(self, app, request):
# We collect matching routes with difference methods here
# in case we send an HTTPMethodNotAllowed()
try:
methods = set()
for route in self.routes:
(matched, args, kwargs) = route.match(request.path_info)
if matched:
if request.method in route.methods:
return route.handler(app, request, *args, **kwargs)
else:
methods.update(route.methods)
if methods:
raise HTTPMethodNotAllowed(headers=[('Allow', ', '.join(methods))])
return self.default_handler(app, request)
except HTTPException, error:
return self.error_handler(app, request, error)
except Exception, exception:
if self.exception_handler:
return self.exception_handler(app, request, exception)
raise
def default_handler(self, app, request):
raise HTTPNotFound()
def error_handler(self, app, request, error):
return error
exception_handler = None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment