| # -*- coding: utf-8 -*- |
| """ |
| webapp2_extras.routes |
| ===================== |
| |
| Extra route classes for webapp2. |
| |
| :copyright: 2011 by tipfy.org. |
| :license: Apache Sotware License, see LICENSE for details. |
| """ |
| import re |
| import urllib |
| |
| from webob import exc |
| |
| import webapp2 |
| |
| |
| class MultiRoute(object): |
| """Base class for routes with nested routes.""" |
| |
| routes = None |
| children = None |
| match_children = None |
| build_children = None |
| |
| def __init__(self, routes): |
| self.routes = routes |
| |
| def get_children(self): |
| if self.children is None: |
| self.children = [] |
| for route in self.routes: |
| for r in route.get_routes(): |
| self.children.append(r) |
| |
| for rv in self.children: |
| yield rv |
| |
| def get_match_children(self): |
| if self.match_children is None: |
| self.match_children = [] |
| for route in self.get_children(): |
| for r in route.get_match_routes(): |
| self.match_children.append(r) |
| |
| for rv in self.match_children: |
| yield rv |
| |
| def get_build_children(self): |
| if self.build_children is None: |
| self.build_children = {} |
| for route in self.get_children(): |
| for n, r in route.get_build_routes(): |
| self.build_children[n] = r |
| |
| for rv in self.build_children.iteritems(): |
| yield rv |
| |
| get_routes = get_children |
| get_match_routes = get_match_children |
| get_build_routes = get_build_children |
| |
| |
| class DomainRoute(MultiRoute): |
| """A route used to restrict route matches to a given domain or subdomain. |
| |
| For example, to restrict routes to a subdomain of the appspot domain:: |
| |
| app = WSGIApplication([ |
| DomainRoute('<subdomain>.app-id.appspot.com', [ |
| Route('/foo', 'FooHandler', 'subdomain-thing'), |
| ]), |
| Route('/bar', 'BarHandler', 'normal-thing'), |
| ]) |
| |
| The template follows the same syntax used by :class:`webapp2.Route` and |
| must define named groups if any value must be added to the match results. |
| In the example above, an extra `subdomain` keyword is passed to the |
| handler, but if the regex didn't define any named groups, nothing would |
| be added. |
| """ |
| |
| def __init__(self, template, routes): |
| """Initializes a URL route. |
| |
| :param template: |
| A route template to match against ``environ['SERVER_NAME']``. |
| See a syntax description in :meth:`webapp2.Route.__init__`. |
| :param routes: |
| A list of :class:`webapp2.Route` instances. |
| """ |
| super(DomainRoute, self).__init__(routes) |
| self.template = template |
| |
| def get_match_routes(self): |
| # This route will do pre-matching before matching the nested routes! |
| yield self |
| |
| def match(self, request): |
| # Use SERVER_NAME to ignore port number that comes with request.host? |
| # host_match = self.regex.match(request.host.split(':', 1)[0]) |
| host_match = self.regex.match(request.environ['SERVER_NAME']) |
| |
| if host_match: |
| args, kwargs = webapp2._get_route_variables(host_match) |
| return _match_routes(self.get_match_children, request, None, |
| kwargs) |
| |
| @webapp2.cached_property |
| def regex(self): |
| regex, reverse_template, args_count, kwargs_count, variables = \ |
| webapp2._parse_route_template(self.template, |
| default_sufix='[^\.]+') |
| return regex |
| |
| |
| class NamePrefixRoute(MultiRoute): |
| """The idea of this route is to set a base name for other routes:: |
| |
| app = WSGIApplication([ |
| NamePrefixRoute('user-', [ |
| Route('/users/<user:\w+>/', UserOverviewHandler, 'overview'), |
| Route('/users/<user:\w+>/profile', UserProfileHandler, |
| 'profile'), |
| Route('/users/<user:\w+>/projects', UserProjectsHandler, |
| 'projects'), |
| ]), |
| ]) |
| |
| The example above is the same as setting the following routes, just more |
| convenient as you can reuse the name prefix:: |
| |
| app = WSGIApplication([ |
| Route('/users/<user:\w+>/', UserOverviewHandler, 'user-overview'), |
| Route('/users/<user:\w+>/profile', UserProfileHandler, |
| 'user-profile'), |
| Route('/users/<user:\w+>/projects', UserProjectsHandler, |
| 'user-projects'), |
| ]) |
| """ |
| |
| _attr = 'name' |
| |
| def __init__(self, prefix, routes): |
| """Initializes a URL route. |
| |
| :param prefix: |
| The prefix to be prepended. |
| :param routes: |
| A list of :class:`webapp2.Route` instances. |
| """ |
| super(NamePrefixRoute, self).__init__(routes) |
| self.prefix = prefix |
| # Prepend a prefix to a route attribute. |
| for route in self.get_routes(): |
| setattr(route, self._attr, prefix + getattr(route, self._attr)) |
| |
| |
| class HandlerPrefixRoute(NamePrefixRoute): |
| """Same as :class:`NamePrefixRoute`, but prefixes the route handler.""" |
| |
| _attr = 'handler' |
| |
| |
| class PathPrefixRoute(NamePrefixRoute): |
| """Same as :class:`NamePrefixRoute`, but prefixes the route path. |
| |
| For example, imagine we have these routes:: |
| |
| app = WSGIApplication([ |
| Route('/users/<user:\w+>/', UserOverviewHandler, |
| 'user-overview'), |
| Route('/users/<user:\w+>/profile', UserProfileHandler, |
| 'user-profile'), |
| Route('/users/<user:\w+>/projects', UserProjectsHandler, |
| 'user-projects'), |
| ]) |
| |
| We could refactor them to reuse the common path prefix:: |
| |
| app = WSGIApplication([ |
| PathPrefixRoute('/users/<user:\w+>', [ |
| Route('/', UserOverviewHandler, 'user-overview'), |
| Route('/profile', UserProfileHandler, 'user-profile'), |
| Route('/projects', UserProjectsHandler, 'user-projects'), |
| ]), |
| ]) |
| |
| This is not only convenient, but also performs better: the nested routes |
| will only be tested if the path prefix matches. |
| """ |
| |
| _attr = 'template' |
| |
| def __init__(self, prefix, routes): |
| """Initializes a URL route. |
| |
| :param prefix: |
| The prefix to be prepended. It must start with a slash but not |
| end with a slash. |
| :param routes: |
| A list of :class:`webapp2.Route` instances. |
| """ |
| assert prefix.startswith('/') and not prefix.endswith('/'), \ |
| 'Path prefixes must start with a slash but not end with a slash.' |
| super(PathPrefixRoute, self).__init__(prefix, routes) |
| |
| def get_match_routes(self): |
| # This route will do pre-matching before matching the nested routes! |
| yield self |
| |
| def match(self, request): |
| if not self.regex.match(urllib.unquote(request.path)): |
| return None |
| |
| return _match_routes(self.get_match_children, request) |
| |
| @webapp2.cached_property |
| def regex(self): |
| regex, reverse_template, args_count, kwargs_count, variables = \ |
| webapp2._parse_route_template(self.prefix + '<:/.*>') |
| return regex |
| |
| |
| class RedirectRoute(webapp2.Route): |
| """A convenience route class for easy redirects. |
| |
| It adds redirect_to, redirect_to_name and strict_slash options to |
| :class:`webapp2.Route`. |
| """ |
| |
| def __init__(self, template, handler=None, name=None, defaults=None, |
| build_only=False, handler_method=None, methods=None, |
| schemes=None, redirect_to=None, redirect_to_name=None, |
| strict_slash=False): |
| """Initializes a URL route. Extra arguments compared to |
| :meth:`webapp2.Route.__init__`: |
| |
| :param redirect_to: |
| A URL string or a callable that returns a URL. If set, this route |
| is used to redirect to it. The callable is called passing |
| ``(handler, *args, **kwargs)`` as arguments. This is a |
| convenience to use :class:`RedirectHandler`. These two are |
| equivalent:: |
| |
| route = Route('/foo', handler=webapp2.RedirectHandler, |
| defaults={'_uri': '/bar'}) |
| route = Route('/foo', redirect_to='/bar') |
| |
| :param redirect_to_name: |
| Same as `redirect_to`, but the value is the name of a route to |
| redirect to. In the example below, accessing '/hello-again' will |
| redirect to the route named 'hello':: |
| |
| route = Route('/hello', handler=HelloHandler, name='hello') |
| route = Route('/hello-again', redirect_to_name='hello') |
| |
| :param strict_slash: |
| If True, redirects access to the same URL with different trailing |
| slash to the strict path defined in the route. For example, take |
| these routes:: |
| |
| route = Route('/foo', FooHandler, strict_slash=True) |
| route = Route('/bar/', BarHandler, strict_slash=True) |
| |
| Because **strict_slash** is True, this is what will happen: |
| |
| - Access to ``/foo`` will execute ``FooHandler`` normally. |
| - Access to ``/bar/`` will execute ``BarHandler`` normally. |
| - Access to ``/foo/`` will redirect to ``/foo``. |
| - Access to ``/bar`` will redirect to ``/bar/``. |
| """ |
| super(RedirectRoute, self).__init__( |
| template, handler=handler, name=name, defaults=defaults, |
| build_only=build_only, handler_method=handler_method, |
| methods=methods, schemes=schemes) |
| |
| if strict_slash and not name: |
| raise ValueError('Routes with strict_slash must have a name.') |
| |
| self.strict_slash = strict_slash |
| self.redirect_to_name = redirect_to_name |
| |
| if redirect_to is not None: |
| assert redirect_to_name is None |
| self.handler = webapp2.RedirectHandler |
| self.defaults['_uri'] = redirect_to |
| |
| def get_match_routes(self): |
| """Generator to get all routes that can be matched from a route. |
| |
| :yields: |
| This route or all nested routes that can be matched. |
| """ |
| if self.redirect_to_name: |
| main_route = self._get_redirect_route(name=self.redirect_to_name) |
| else: |
| main_route = self |
| |
| if not self.build_only: |
| if self.strict_slash is True: |
| if self.template.endswith('/'): |
| template = self.template[:-1] |
| else: |
| template = self.template + '/' |
| |
| yield main_route |
| yield self._get_redirect_route(template=template) |
| else: |
| yield main_route |
| |
| def _get_redirect_route(self, template=None, name=None): |
| template = template or self.template |
| name = name or self.name |
| defaults = self.defaults.copy() |
| defaults.update({ |
| '_uri': self._redirect, |
| '_name': name, |
| }) |
| new_route = webapp2.Route(template, webapp2.RedirectHandler, |
| defaults=defaults) |
| return new_route |
| |
| def _redirect(self, handler, *args, **kwargs): |
| # Get from request because args is empty if named routes are set? |
| # args, kwargs = (handler.request.route_args, |
| # handler.request.route_kwargs) |
| kwargs.pop('_uri', None) |
| kwargs.pop('_code', None) |
| return handler.uri_for(kwargs.pop('_name'), *args, **kwargs) |
| |
| |
| def _match_routes(iter_func, request, extra_args=None, extra_kwargs=None): |
| """Tries to match a route given an iterator.""" |
| method_not_allowed = False |
| for route in iter_func(): |
| try: |
| match = route.match(request) |
| if match: |
| route, args, kwargs = match |
| if extra_args: |
| args += extra_args |
| |
| if extra_kwargs: |
| kwargs.update(extra_kwargs) |
| |
| return route, args, kwargs |
| except exc.HTTPMethodNotAllowed: |
| method_not_allowed = True |
| |
| if method_not_allowed: |
| raise exc.HTTPMethodNotAllowed() |