# -*- 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()