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 """ 11 import re 12 import urllib 13 14 from webob import exc 15 16 import webapp2 17 18 19 class 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 65 class 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 118 class 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 160 class HandlerPrefixRoute(NamePrefixRoute): 161 """Same as :class:`NamePrefixRoute`, but prefixes the route handler.""" 162 163 _attr = 'handler' 164 165 166 class 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 226 class 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 334 def _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