• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2"""
3    webapp2_extras.routes
4    =====================
5
6    Extra route classes for webapp2.
7
8    :copyright: 2011 by tipfy.org.
9    :license: Apache Sotware License, see LICENSE for details.
10"""
11import re
12import urllib
13
14from webob import exc
15
16import webapp2
17
18
19class MultiRoute(object):
20    """Base class for routes with nested routes."""
21
22    routes = None
23    children = None
24    match_children = None
25    build_children = None
26
27    def __init__(self, routes):
28        self.routes = routes
29
30    def get_children(self):
31        if self.children is None:
32            self.children = []
33            for route in self.routes:
34                for r in route.get_routes():
35                    self.children.append(r)
36
37        for rv in self.children:
38            yield rv
39
40    def get_match_children(self):
41        if self.match_children is None:
42            self.match_children = []
43            for route in self.get_children():
44                for r in route.get_match_routes():
45                    self.match_children.append(r)
46
47        for rv in self.match_children:
48            yield rv
49
50    def get_build_children(self):
51        if self.build_children is None:
52            self.build_children = {}
53            for route in self.get_children():
54                for n, r in route.get_build_routes():
55                    self.build_children[n] = r
56
57        for rv in self.build_children.iteritems():
58            yield rv
59
60    get_routes = get_children
61    get_match_routes = get_match_children
62    get_build_routes = get_build_children
63
64
65class DomainRoute(MultiRoute):
66    """A route used to restrict route matches to a given domain or subdomain.
67
68    For example, to restrict routes to a subdomain of the appspot domain::
69
70        app = WSGIApplication([
71            DomainRoute('<subdomain>.app-id.appspot.com', [
72                Route('/foo', 'FooHandler', 'subdomain-thing'),
73            ]),
74            Route('/bar', 'BarHandler', 'normal-thing'),
75        ])
76
77    The template follows the same syntax used by :class:`webapp2.Route` and
78    must define named groups if any value must be added to the match results.
79    In the example above, an extra `subdomain` keyword is passed to the
80    handler, but if the regex didn't define any named groups, nothing would
81    be added.
82    """
83
84    def __init__(self, template, routes):
85        """Initializes a URL route.
86
87        :param template:
88            A route template to match against ``environ['SERVER_NAME']``.
89            See a syntax description in :meth:`webapp2.Route.__init__`.
90        :param routes:
91            A list of :class:`webapp2.Route` instances.
92        """
93        super(DomainRoute, self).__init__(routes)
94        self.template = template
95
96    def get_match_routes(self):
97        # This route will do pre-matching before matching the nested routes!
98        yield self
99
100    def match(self, request):
101        # Use SERVER_NAME to ignore port number that comes with request.host?
102        # host_match = self.regex.match(request.host.split(':', 1)[0])
103        host_match = self.regex.match(request.environ['SERVER_NAME'])
104
105        if host_match:
106            args, kwargs = webapp2._get_route_variables(host_match)
107            return _match_routes(self.get_match_children, request, None,
108                                 kwargs)
109
110    @webapp2.cached_property
111    def regex(self):
112        regex, reverse_template, args_count, kwargs_count, variables = \
113            webapp2._parse_route_template(self.template,
114                                          default_sufix='[^\.]+')
115        return regex
116
117
118class NamePrefixRoute(MultiRoute):
119    """The idea of this route is to set a base name for other routes::
120
121        app = WSGIApplication([
122            NamePrefixRoute('user-', [
123                Route('/users/<user:\w+>/', UserOverviewHandler, 'overview'),
124                Route('/users/<user:\w+>/profile', UserProfileHandler,
125                      'profile'),
126                Route('/users/<user:\w+>/projects', UserProjectsHandler,
127                      'projects'),
128            ]),
129        ])
130
131    The example above is the same as setting the following routes, just more
132    convenient as you can reuse the name prefix::
133
134        app = WSGIApplication([
135            Route('/users/<user:\w+>/', UserOverviewHandler, 'user-overview'),
136            Route('/users/<user:\w+>/profile', UserProfileHandler,
137                  'user-profile'),
138            Route('/users/<user:\w+>/projects', UserProjectsHandler,
139                  'user-projects'),
140        ])
141    """
142
143    _attr = 'name'
144
145    def __init__(self, prefix, routes):
146        """Initializes a URL route.
147
148        :param prefix:
149            The prefix to be prepended.
150        :param routes:
151            A list of :class:`webapp2.Route` instances.
152        """
153        super(NamePrefixRoute, self).__init__(routes)
154        self.prefix = prefix
155        # Prepend a prefix to a route attribute.
156        for route in self.get_routes():
157            setattr(route, self._attr, prefix + getattr(route, self._attr))
158
159
160class HandlerPrefixRoute(NamePrefixRoute):
161    """Same as :class:`NamePrefixRoute`, but prefixes the route handler."""
162
163    _attr = 'handler'
164
165
166class PathPrefixRoute(NamePrefixRoute):
167    """Same as :class:`NamePrefixRoute`, but prefixes the route path.
168
169    For example, imagine we have these routes::
170
171        app = WSGIApplication([
172            Route('/users/<user:\w+>/', UserOverviewHandler,
173                  'user-overview'),
174            Route('/users/<user:\w+>/profile', UserProfileHandler,
175                  'user-profile'),
176            Route('/users/<user:\w+>/projects', UserProjectsHandler,
177                  'user-projects'),
178        ])
179
180    We could refactor them to reuse the common path prefix::
181
182        app = WSGIApplication([
183            PathPrefixRoute('/users/<user:\w+>', [
184                Route('/', UserOverviewHandler, 'user-overview'),
185                Route('/profile', UserProfileHandler, 'user-profile'),
186                Route('/projects', UserProjectsHandler, 'user-projects'),
187            ]),
188        ])
189
190    This is not only convenient, but also performs better: the nested routes
191    will only be tested if the path prefix matches.
192    """
193
194    _attr = 'template'
195
196    def __init__(self, prefix, routes):
197        """Initializes a URL route.
198
199        :param prefix:
200            The prefix to be prepended. It must start with a slash but not
201            end with a slash.
202        :param routes:
203            A list of :class:`webapp2.Route` instances.
204        """
205        assert prefix.startswith('/') and not prefix.endswith('/'), \
206            'Path prefixes must start with a slash but not end with a slash.'
207        super(PathPrefixRoute, self).__init__(prefix, routes)
208
209    def get_match_routes(self):
210        # This route will do pre-matching before matching the nested routes!
211        yield self
212
213    def match(self, request):
214        if not self.regex.match(urllib.unquote(request.path)):
215            return None
216
217        return _match_routes(self.get_match_children, request)
218
219    @webapp2.cached_property
220    def regex(self):
221        regex, reverse_template, args_count, kwargs_count, variables = \
222            webapp2._parse_route_template(self.prefix + '<:/.*>')
223        return regex
224
225
226class RedirectRoute(webapp2.Route):
227    """A convenience route class for easy redirects.
228
229    It adds redirect_to, redirect_to_name and strict_slash options to
230    :class:`webapp2.Route`.
231    """
232
233    def __init__(self, template, handler=None, name=None, defaults=None,
234                 build_only=False, handler_method=None, methods=None,
235                 schemes=None, redirect_to=None, redirect_to_name=None,
236                 strict_slash=False):
237        """Initializes a URL route. Extra arguments compared to
238        :meth:`webapp2.Route.__init__`:
239
240        :param redirect_to:
241            A URL string or a callable that returns a URL. If set, this route
242            is used to redirect to it. The callable is called passing
243            ``(handler, *args, **kwargs)`` as arguments. This is a
244            convenience to use :class:`RedirectHandler`. These two are
245            equivalent::
246
247                route = Route('/foo', handler=webapp2.RedirectHandler,
248                              defaults={'_uri': '/bar'})
249                route = Route('/foo', redirect_to='/bar')
250
251        :param redirect_to_name:
252            Same as `redirect_to`, but the value is the name of a route to
253            redirect to. In the example below, accessing '/hello-again' will
254            redirect to the route named 'hello'::
255
256                route = Route('/hello', handler=HelloHandler, name='hello')
257                route = Route('/hello-again', redirect_to_name='hello')
258
259        :param strict_slash:
260            If True, redirects access to the same URL with different trailing
261            slash to the strict path defined in the route. For example, take
262            these routes::
263
264                route = Route('/foo', FooHandler, strict_slash=True)
265                route = Route('/bar/', BarHandler, strict_slash=True)
266
267            Because **strict_slash** is True, this is what will happen:
268
269            - Access to ``/foo`` will execute ``FooHandler`` normally.
270            - Access to ``/bar/`` will execute ``BarHandler`` normally.
271            - Access to ``/foo/`` will redirect to ``/foo``.
272            - Access to ``/bar`` will redirect to ``/bar/``.
273        """
274        super(RedirectRoute, self).__init__(
275            template, handler=handler, name=name, defaults=defaults,
276            build_only=build_only, handler_method=handler_method,
277            methods=methods, schemes=schemes)
278
279        if strict_slash and not name:
280            raise ValueError('Routes with strict_slash must have a name.')
281
282        self.strict_slash = strict_slash
283        self.redirect_to_name = redirect_to_name
284
285        if redirect_to is not None:
286            assert redirect_to_name is None
287            self.handler = webapp2.RedirectHandler
288            self.defaults['_uri'] = redirect_to
289
290    def get_match_routes(self):
291        """Generator to get all routes that can be matched from a route.
292
293        :yields:
294            This route or all nested routes that can be matched.
295        """
296        if self.redirect_to_name:
297            main_route = self._get_redirect_route(name=self.redirect_to_name)
298        else:
299            main_route = self
300
301        if not self.build_only:
302            if self.strict_slash is True:
303                if self.template.endswith('/'):
304                    template = self.template[:-1]
305                else:
306                    template = self.template + '/'
307
308                yield main_route
309                yield self._get_redirect_route(template=template)
310            else:
311                yield main_route
312
313    def _get_redirect_route(self, template=None, name=None):
314        template = template or self.template
315        name = name or self.name
316        defaults = self.defaults.copy()
317        defaults.update({
318            '_uri': self._redirect,
319            '_name': name,
320        })
321        new_route = webapp2.Route(template, webapp2.RedirectHandler,
322                                  defaults=defaults)
323        return new_route
324
325    def _redirect(self, handler, *args, **kwargs):
326        # Get from request because args is empty if named routes are set?
327        # args, kwargs = (handler.request.route_args,
328        #                 handler.request.route_kwargs)
329        kwargs.pop('_uri', None)
330        kwargs.pop('_code', None)
331        return handler.uri_for(kwargs.pop('_name'), *args, **kwargs)
332
333
334def _match_routes(iter_func, request, extra_args=None, extra_kwargs=None):
335    """Tries to match a route given an iterator."""
336    method_not_allowed = False
337    for route in iter_func():
338        try:
339            match = route.match(request)
340            if match:
341                route, args, kwargs = match
342                if extra_args:
343                    args += extra_args
344
345                if extra_kwargs:
346                    kwargs.update(extra_kwargs)
347
348                return route, args, kwargs
349        except exc.HTTPMethodNotAllowed:
350            method_not_allowed = True
351
352    if method_not_allowed:
353        raise exc.HTTPMethodNotAllowed()
354