Created
February 17, 2010 04:28
-
-
Save sj26/306296 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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