Mercurial > repos > shellac > guppy_basecaller
comparison env/lib/python3.7/site-packages/routes/mapper.py @ 5:9b1c78e6ba9c draft default tip
"planemo upload commit 6c0a8142489327ece472c84e558c47da711a9142"
| author | shellac |
|---|---|
| date | Mon, 01 Jun 2020 08:59:25 -0400 |
| parents | 79f47841a781 |
| children |
comparison
equal
deleted
inserted
replaced
| 4:79f47841a781 | 5:9b1c78e6ba9c |
|---|---|
| 1 """Mapper and Sub-Mapper""" | |
| 2 import re | |
| 3 import threading | |
| 4 | |
| 5 from repoze.lru import LRUCache | |
| 6 import six | |
| 7 | |
| 8 from routes import request_config | |
| 9 from routes.util import ( | |
| 10 controller_scan, | |
| 11 RoutesException, | |
| 12 as_unicode | |
| 13 ) | |
| 14 from routes.route import Route | |
| 15 | |
| 16 | |
| 17 COLLECTION_ACTIONS = ['index', 'create', 'new'] | |
| 18 MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit'] | |
| 19 | |
| 20 | |
| 21 def strip_slashes(name): | |
| 22 """Remove slashes from the beginning and end of a part/URL.""" | |
| 23 if name.startswith('/'): | |
| 24 name = name[1:] | |
| 25 if name.endswith('/'): | |
| 26 name = name[:-1] | |
| 27 return name | |
| 28 | |
| 29 | |
| 30 class SubMapperParent(object): | |
| 31 """Base class for Mapper and SubMapper, both of which may be the parent | |
| 32 of SubMapper objects | |
| 33 """ | |
| 34 | |
| 35 def submapper(self, **kargs): | |
| 36 """Create a partial version of the Mapper with the designated | |
| 37 options set | |
| 38 | |
| 39 This results in a :class:`routes.mapper.SubMapper` object. | |
| 40 | |
| 41 If keyword arguments provided to this method also exist in the | |
| 42 keyword arguments provided to the submapper, their values will | |
| 43 be merged with the saved options going first. | |
| 44 | |
| 45 In addition to :class:`routes.route.Route` arguments, submapper | |
| 46 can also take a ``path_prefix`` argument which will be | |
| 47 prepended to the path of all routes that are connected. | |
| 48 | |
| 49 Example:: | |
| 50 | |
| 51 >>> map = Mapper(controller_scan=None) | |
| 52 >>> map.connect('home', '/', controller='home', action='splash') | |
| 53 >>> map.matchlist[0].name == 'home' | |
| 54 True | |
| 55 >>> m = map.submapper(controller='home') | |
| 56 >>> m.connect('index', '/index', action='index') | |
| 57 >>> map.matchlist[1].name == 'index' | |
| 58 True | |
| 59 >>> map.matchlist[1].defaults['controller'] == 'home' | |
| 60 True | |
| 61 | |
| 62 Optional ``collection_name`` and ``resource_name`` arguments are | |
| 63 used in the generation of route names by the ``action`` and | |
| 64 ``link`` methods. These in turn are used by the ``index``, | |
| 65 ``new``, ``create``, ``show``, ``edit``, ``update`` and | |
| 66 ``delete`` methods which may be invoked indirectly by listing | |
| 67 them in the ``actions`` argument. If the ``formatted`` argument | |
| 68 is set to ``True`` (the default), generated paths are given the | |
| 69 suffix '{.format}' which matches or generates an optional format | |
| 70 extension. | |
| 71 | |
| 72 Example:: | |
| 73 | |
| 74 >>> from routes.util import url_for | |
| 75 >>> map = Mapper(controller_scan=None) | |
| 76 >>> m = map.submapper(path_prefix='/entries', collection_name='entries', resource_name='entry', actions=['index', 'new']) | |
| 77 >>> url_for('entries') == '/entries' | |
| 78 True | |
| 79 >>> url_for('new_entry', format='xml') == '/entries/new.xml' | |
| 80 True | |
| 81 | |
| 82 """ | |
| 83 return SubMapper(self, **kargs) | |
| 84 | |
| 85 def collection(self, collection_name, resource_name, path_prefix=None, | |
| 86 member_prefix='/{id}', controller=None, | |
| 87 collection_actions=COLLECTION_ACTIONS, | |
| 88 member_actions=MEMBER_ACTIONS, member_options=None, | |
| 89 **kwargs): | |
| 90 """Create a submapper that represents a collection. | |
| 91 | |
| 92 This results in a :class:`routes.mapper.SubMapper` object, with a | |
| 93 ``member`` property of the same type that represents the collection's | |
| 94 member resources. | |
| 95 | |
| 96 Its interface is the same as the ``submapper`` together with | |
| 97 ``member_prefix``, ``member_actions`` and ``member_options`` | |
| 98 which are passed to the ``member`` submapper as ``path_prefix``, | |
| 99 ``actions`` and keyword arguments respectively. | |
| 100 | |
| 101 Example:: | |
| 102 | |
| 103 >>> from routes.util import url_for | |
| 104 >>> map = Mapper(controller_scan=None) | |
| 105 >>> c = map.collection('entries', 'entry') | |
| 106 >>> c.member.link('ping', method='POST') | |
| 107 >>> url_for('entries') == '/entries' | |
| 108 True | |
| 109 >>> url_for('edit_entry', id=1) == '/entries/1/edit' | |
| 110 True | |
| 111 >>> url_for('ping_entry', id=1) == '/entries/1/ping' | |
| 112 True | |
| 113 | |
| 114 """ | |
| 115 if controller is None: | |
| 116 controller = resource_name or collection_name | |
| 117 | |
| 118 if path_prefix is None: | |
| 119 if collection_name is None: | |
| 120 path_prefix_str = '' | |
| 121 else: | |
| 122 path_prefix_str = '/{collection_name}' | |
| 123 else: | |
| 124 if collection_name is None: | |
| 125 path_prefix_str = "{pre}" | |
| 126 else: | |
| 127 path_prefix_str = "{pre}/{collection_name}" | |
| 128 | |
| 129 # generate what will be the path prefix for the collection | |
| 130 path_prefix = path_prefix_str.format(pre=path_prefix, | |
| 131 collection_name=collection_name) | |
| 132 | |
| 133 collection = SubMapper(self, collection_name=collection_name, | |
| 134 resource_name=resource_name, | |
| 135 path_prefix=path_prefix, controller=controller, | |
| 136 actions=collection_actions, **kwargs) | |
| 137 | |
| 138 collection.member = SubMapper(collection, path_prefix=member_prefix, | |
| 139 actions=member_actions, | |
| 140 **(member_options or {})) | |
| 141 | |
| 142 return collection | |
| 143 | |
| 144 | |
| 145 class SubMapper(SubMapperParent): | |
| 146 """Partial mapper for use with_options""" | |
| 147 def __init__(self, obj, resource_name=None, collection_name=None, | |
| 148 actions=None, formatted=None, **kwargs): | |
| 149 self.kwargs = kwargs | |
| 150 self.obj = obj | |
| 151 self.collection_name = collection_name | |
| 152 self.member = None | |
| 153 self.resource_name = resource_name \ | |
| 154 or getattr(obj, 'resource_name', None) \ | |
| 155 or kwargs.get('controller', None) \ | |
| 156 or getattr(obj, 'controller', None) | |
| 157 if formatted is not None: | |
| 158 self.formatted = formatted | |
| 159 else: | |
| 160 self.formatted = getattr(obj, 'formatted', None) | |
| 161 if self.formatted is None: | |
| 162 self.formatted = True | |
| 163 self.add_actions(actions or [], **kwargs) | |
| 164 | |
| 165 def connect(self, routename, path=None, **kwargs): | |
| 166 newkargs = {} | |
| 167 _routename = routename | |
| 168 _path = path | |
| 169 for key, value in six.iteritems(self.kwargs): | |
| 170 if key == 'path_prefix': | |
| 171 if path is not None: | |
| 172 # if there's a name_prefix, add it to the route name | |
| 173 # and if there's a path_prefix | |
| 174 _path = ''.join((self.kwargs[key], path)) | |
| 175 else: | |
| 176 _path = ''.join((self.kwargs[key], routename)) | |
| 177 elif key == 'name_prefix': | |
| 178 if path is not None: | |
| 179 # if there's a name_prefix, add it to the route name | |
| 180 # and if there's a path_prefix | |
| 181 _routename = ''.join((self.kwargs[key], routename)) | |
| 182 else: | |
| 183 _routename = None | |
| 184 elif key in kwargs: | |
| 185 if isinstance(value, dict): | |
| 186 newkargs[key] = dict(value, **kwargs[key]) # merge dicts | |
| 187 else: | |
| 188 # Originally used this form: | |
| 189 # newkargs[key] = value + kwargs[key] | |
| 190 # New version avoids the inheritance concatenation issue | |
| 191 # with submappers. Only prefixes concatenate, everything | |
| 192 # else overrides in submappers. | |
| 193 newkargs[key] = kwargs[key] | |
| 194 else: | |
| 195 newkargs[key] = self.kwargs[key] | |
| 196 for key in kwargs: | |
| 197 if key not in self.kwargs: | |
| 198 newkargs[key] = kwargs[key] | |
| 199 | |
| 200 newargs = (_routename, _path) | |
| 201 return self.obj.connect(*newargs, **newkargs) | |
| 202 | |
| 203 def link(self, rel=None, name=None, action=None, method='GET', | |
| 204 formatted=None, **kwargs): | |
| 205 """Generates a named route for a subresource. | |
| 206 | |
| 207 Example:: | |
| 208 | |
| 209 >>> from routes.util import url_for | |
| 210 >>> map = Mapper(controller_scan=None) | |
| 211 >>> c = map.collection('entries', 'entry') | |
| 212 >>> c.link('recent', name='recent_entries') | |
| 213 >>> c.member.link('ping', method='POST', formatted=True) | |
| 214 >>> url_for('entries') == '/entries' | |
| 215 True | |
| 216 >>> url_for('recent_entries') == '/entries/recent' | |
| 217 True | |
| 218 >>> url_for('ping_entry', id=1) == '/entries/1/ping' | |
| 219 True | |
| 220 >>> url_for('ping_entry', id=1, format='xml') == '/entries/1/ping.xml' | |
| 221 True | |
| 222 | |
| 223 """ | |
| 224 if formatted or (formatted is None and self.formatted): | |
| 225 suffix = '{.format}' | |
| 226 else: | |
| 227 suffix = '' | |
| 228 | |
| 229 return self.connect(name or (rel + '_' + self.resource_name), | |
| 230 '/' + (rel or name) + suffix, | |
| 231 action=action or rel or name, | |
| 232 **_kwargs_with_conditions(kwargs, method)) | |
| 233 | |
| 234 def new(self, **kwargs): | |
| 235 """Generates the "new" link for a collection submapper.""" | |
| 236 return self.link(rel='new', **kwargs) | |
| 237 | |
| 238 def edit(self, **kwargs): | |
| 239 """Generates the "edit" link for a collection member submapper.""" | |
| 240 return self.link(rel='edit', **kwargs) | |
| 241 | |
| 242 def action(self, name=None, action=None, method='GET', formatted=None, | |
| 243 **kwargs): | |
| 244 """Generates a named route at the base path of a submapper. | |
| 245 | |
| 246 Example:: | |
| 247 | |
| 248 >>> from routes import url_for | |
| 249 >>> map = Mapper(controller_scan=None) | |
| 250 >>> c = map.submapper(path_prefix='/entries', controller='entry') | |
| 251 >>> c.action(action='index', name='entries', formatted=True) | |
| 252 >>> c.action(action='create', method='POST') | |
| 253 >>> url_for(controller='entry', action='index', method='GET') == '/entries' | |
| 254 True | |
| 255 >>> url_for(controller='entry', action='index', method='GET', format='xml') == '/entries.xml' | |
| 256 True | |
| 257 >>> url_for(controller='entry', action='create', method='POST') == '/entries' | |
| 258 True | |
| 259 | |
| 260 """ | |
| 261 if formatted or (formatted is None and self.formatted): | |
| 262 suffix = '{.format}' | |
| 263 else: | |
| 264 suffix = '' | |
| 265 return self.connect(name or (action + '_' + self.resource_name), | |
| 266 suffix, | |
| 267 action=action or name, | |
| 268 **_kwargs_with_conditions(kwargs, method)) | |
| 269 | |
| 270 def index(self, name=None, **kwargs): | |
| 271 """Generates the "index" action for a collection submapper.""" | |
| 272 return self.action(name=name or self.collection_name, | |
| 273 action='index', method='GET', **kwargs) | |
| 274 | |
| 275 def show(self, name=None, **kwargs): | |
| 276 """Generates the "show" action for a collection member submapper.""" | |
| 277 return self.action(name=name or self.resource_name, | |
| 278 action='show', method='GET', **kwargs) | |
| 279 | |
| 280 def create(self, **kwargs): | |
| 281 """Generates the "create" action for a collection submapper.""" | |
| 282 return self.action(action='create', method='POST', **kwargs) | |
| 283 | |
| 284 def update(self, **kwargs): | |
| 285 """Generates the "update" action for a collection member submapper.""" | |
| 286 return self.action(action='update', method='PUT', **kwargs) | |
| 287 | |
| 288 def delete(self, **kwargs): | |
| 289 """Generates the "delete" action for a collection member submapper.""" | |
| 290 return self.action(action='delete', method='DELETE', **kwargs) | |
| 291 | |
| 292 def add_actions(self, actions, **kwargs): | |
| 293 [getattr(self, action)(**kwargs) for action in actions] | |
| 294 | |
| 295 # Provided for those who prefer using the 'with' syntax in Python 2.5+ | |
| 296 def __enter__(self): | |
| 297 return self | |
| 298 | |
| 299 def __exit__(self, type, value, tb): | |
| 300 pass | |
| 301 | |
| 302 | |
| 303 # Create kwargs with a 'conditions' member generated for the given method | |
| 304 def _kwargs_with_conditions(kwargs, method): | |
| 305 if method and 'conditions' not in kwargs: | |
| 306 newkwargs = kwargs.copy() | |
| 307 newkwargs['conditions'] = {'method': method} | |
| 308 return newkwargs | |
| 309 else: | |
| 310 return kwargs | |
| 311 | |
| 312 | |
| 313 class Mapper(SubMapperParent): | |
| 314 """Mapper handles URL generation and URL recognition in a web | |
| 315 application. | |
| 316 | |
| 317 Mapper is built handling dictionary's. It is assumed that the web | |
| 318 application will handle the dictionary returned by URL recognition | |
| 319 to dispatch appropriately. | |
| 320 | |
| 321 URL generation is done by passing keyword parameters into the | |
| 322 generate function, a URL is then returned. | |
| 323 | |
| 324 """ | |
| 325 def __init__(self, controller_scan=controller_scan, directory=None, | |
| 326 always_scan=False, register=True, explicit=True): | |
| 327 """Create a new Mapper instance | |
| 328 | |
| 329 All keyword arguments are optional. | |
| 330 | |
| 331 ``controller_scan`` | |
| 332 Function reference that will be used to return a list of | |
| 333 valid controllers used during URL matching. If | |
| 334 ``directory`` keyword arg is present, it will be passed | |
| 335 into the function during its call. This option defaults to | |
| 336 a function that will scan a directory for controllers. | |
| 337 | |
| 338 Alternatively, a list of controllers or None can be passed | |
| 339 in which are assumed to be the definitive list of | |
| 340 controller names valid when matching 'controller'. | |
| 341 | |
| 342 ``directory`` | |
| 343 Passed into controller_scan for the directory to scan. It | |
| 344 should be an absolute path if using the default | |
| 345 ``controller_scan`` function. | |
| 346 | |
| 347 ``always_scan`` | |
| 348 Whether or not the ``controller_scan`` function should be | |
| 349 run during every URL match. This is typically a good idea | |
| 350 during development so the server won't need to be restarted | |
| 351 anytime a controller is added. | |
| 352 | |
| 353 ``register`` | |
| 354 Boolean used to determine if the Mapper should use | |
| 355 ``request_config`` to register itself as the mapper. Since | |
| 356 it's done on a thread-local basis, this is typically best | |
| 357 used during testing though it won't hurt in other cases. | |
| 358 | |
| 359 ``explicit`` | |
| 360 Boolean used to determine if routes should be connected | |
| 361 with implicit defaults of:: | |
| 362 | |
| 363 {'controller':'content','action':'index','id':None} | |
| 364 | |
| 365 When set to True, these defaults will not be added to route | |
| 366 connections and ``url_for`` will not use Route memory. | |
| 367 | |
| 368 Additional attributes that may be set after mapper | |
| 369 initialization (ie, map.ATTRIBUTE = 'something'): | |
| 370 | |
| 371 ``encoding`` | |
| 372 Used to indicate alternative encoding/decoding systems to | |
| 373 use with both incoming URL's, and during Route generation | |
| 374 when passed a Unicode string. Defaults to 'utf-8'. | |
| 375 | |
| 376 ``decode_errors`` | |
| 377 How to handle errors in the encoding, generally ignoring | |
| 378 any chars that don't convert should be sufficient. Defaults | |
| 379 to 'ignore'. | |
| 380 | |
| 381 ``minimization`` | |
| 382 Boolean used to indicate whether or not Routes should | |
| 383 minimize URL's and the generated URL's, or require every | |
| 384 part where it appears in the path. Defaults to True. | |
| 385 | |
| 386 ``hardcode_names`` | |
| 387 Whether or not Named Routes result in the default options | |
| 388 for the route being used *or* if they actually force url | |
| 389 generation to use the route. Defaults to False. | |
| 390 | |
| 391 """ | |
| 392 self.matchlist = [] | |
| 393 self.maxkeys = {} | |
| 394 self.minkeys = {} | |
| 395 self.urlcache = LRUCache(1600) | |
| 396 self._created_regs = False | |
| 397 self._created_gens = False | |
| 398 self._master_regexp = None | |
| 399 self.prefix = None | |
| 400 self.req_data = threading.local() | |
| 401 self.directory = directory | |
| 402 self.always_scan = always_scan | |
| 403 self.controller_scan = controller_scan | |
| 404 self._regprefix = None | |
| 405 self._routenames = {} | |
| 406 self.debug = False | |
| 407 self.append_slash = False | |
| 408 self.sub_domains = False | |
| 409 self.sub_domains_ignore = [] | |
| 410 self.domain_match = '[^\.\/]+?\.[^\.\/]+' | |
| 411 self.explicit = explicit | |
| 412 self.encoding = 'utf-8' | |
| 413 self.decode_errors = 'ignore' | |
| 414 self.hardcode_names = True | |
| 415 self.minimization = False | |
| 416 self.create_regs_lock = threading.Lock() | |
| 417 if register: | |
| 418 config = request_config() | |
| 419 config.mapper = self | |
| 420 | |
| 421 def __str__(self): | |
| 422 """Generates a tabular string representation.""" | |
| 423 def format_methods(r): | |
| 424 if r.conditions: | |
| 425 method = r.conditions.get('method', '') | |
| 426 return type(method) is str and method or ', '.join(method) | |
| 427 else: | |
| 428 return '' | |
| 429 | |
| 430 table = [('Route name', 'Methods', 'Path', 'Controller', 'action')] + \ | |
| 431 [(r.name or '', format_methods(r), r.routepath or '', | |
| 432 r.defaults.get('controller', ''), r.defaults.get('action', '')) | |
| 433 for r in self.matchlist] | |
| 434 | |
| 435 widths = [max(len(row[col]) for row in table) | |
| 436 for col in range(len(table[0]))] | |
| 437 | |
| 438 return '\n'.join( | |
| 439 ' '.join(row[col].ljust(widths[col]) | |
| 440 for col in range(len(widths))) | |
| 441 for row in table) | |
| 442 | |
| 443 def _envget(self): | |
| 444 try: | |
| 445 return self.req_data.environ | |
| 446 except AttributeError: | |
| 447 return None | |
| 448 | |
| 449 def _envset(self, env): | |
| 450 self.req_data.environ = env | |
| 451 | |
| 452 def _envdel(self): | |
| 453 del self.req_data.environ | |
| 454 environ = property(_envget, _envset, _envdel) | |
| 455 | |
| 456 def extend(self, routes, path_prefix=''): | |
| 457 """Extends the mapper routes with a list of Route objects | |
| 458 | |
| 459 If a path_prefix is provided, all the routes will have their | |
| 460 path prepended with the path_prefix. | |
| 461 | |
| 462 Example:: | |
| 463 | |
| 464 >>> map = Mapper(controller_scan=None) | |
| 465 >>> map.connect('home', '/', controller='home', action='splash') | |
| 466 >>> map.matchlist[0].name == 'home' | |
| 467 True | |
| 468 >>> routes = [Route('index', '/index.htm', controller='home', | |
| 469 ... action='index')] | |
| 470 >>> map.extend(routes) | |
| 471 >>> len(map.matchlist) == 2 | |
| 472 True | |
| 473 >>> map.extend(routes, path_prefix='/subapp') | |
| 474 >>> len(map.matchlist) == 3 | |
| 475 True | |
| 476 >>> map.matchlist[2].routepath == '/subapp/index.htm' | |
| 477 True | |
| 478 | |
| 479 .. note:: | |
| 480 | |
| 481 This function does not merely extend the mapper with the | |
| 482 given list of routes, it actually creates new routes with | |
| 483 identical calling arguments. | |
| 484 | |
| 485 """ | |
| 486 for route in routes: | |
| 487 if path_prefix and route.minimization: | |
| 488 routepath = '/'.join([path_prefix, route.routepath]) | |
| 489 elif path_prefix: | |
| 490 routepath = path_prefix + route.routepath | |
| 491 else: | |
| 492 routepath = route.routepath | |
| 493 self.connect(route.name, routepath, **route._kargs) | |
| 494 | |
| 495 def make_route(self, *args, **kargs): | |
| 496 """Make a new Route object | |
| 497 | |
| 498 A subclass can override this method to use a custom Route class. | |
| 499 """ | |
| 500 return Route(*args, **kargs) | |
| 501 | |
| 502 def connect(self, *args, **kargs): | |
| 503 """Create and connect a new Route to the Mapper. | |
| 504 | |
| 505 Usage: | |
| 506 | |
| 507 .. code-block:: python | |
| 508 | |
| 509 m = Mapper() | |
| 510 m.connect(':controller/:action/:id') | |
| 511 m.connect('date/:year/:month/:day', controller="blog", | |
| 512 action="view") | |
| 513 m.connect('archives/:page', controller="blog", action="by_page", | |
| 514 requirements = { 'page':'\d{1,2}' }) | |
| 515 m.connect('category_list', 'archives/category/:section', | |
| 516 controller='blog', action='category', | |
| 517 section='home', type='list') | |
| 518 m.connect('home', '', controller='blog', action='view', | |
| 519 section='home') | |
| 520 | |
| 521 """ | |
| 522 routename = None | |
| 523 if len(args) > 1: | |
| 524 routename = args[0] | |
| 525 else: | |
| 526 args = (None,) + args | |
| 527 if '_explicit' not in kargs: | |
| 528 kargs['_explicit'] = self.explicit | |
| 529 if '_minimize' not in kargs: | |
| 530 kargs['_minimize'] = self.minimization | |
| 531 route = self.make_route(*args, **kargs) | |
| 532 | |
| 533 # Apply encoding and errors if its not the defaults and the route | |
| 534 # didn't have one passed in. | |
| 535 if (self.encoding != 'utf-8' or self.decode_errors != 'ignore') and \ | |
| 536 '_encoding' not in kargs: | |
| 537 route.encoding = self.encoding | |
| 538 route.decode_errors = self.decode_errors | |
| 539 | |
| 540 if not route.static: | |
| 541 self.matchlist.append(route) | |
| 542 | |
| 543 if routename: | |
| 544 self._routenames[routename] = route | |
| 545 route.name = routename | |
| 546 if route.static: | |
| 547 return | |
| 548 exists = False | |
| 549 for key in self.maxkeys: | |
| 550 if key == route.maxkeys: | |
| 551 self.maxkeys[key].append(route) | |
| 552 exists = True | |
| 553 break | |
| 554 if not exists: | |
| 555 self.maxkeys[route.maxkeys] = [route] | |
| 556 self._created_gens = False | |
| 557 | |
| 558 def _create_gens(self): | |
| 559 """Create the generation hashes for route lookups""" | |
| 560 # Use keys temporailly to assemble the list to avoid excessive | |
| 561 # list iteration testing with "in" | |
| 562 controllerlist = {} | |
| 563 actionlist = {} | |
| 564 | |
| 565 # Assemble all the hardcoded/defaulted actions/controllers used | |
| 566 for route in self.matchlist: | |
| 567 if route.static: | |
| 568 continue | |
| 569 if 'controller' in route.defaults: | |
| 570 controllerlist[route.defaults['controller']] = True | |
| 571 if 'action' in route.defaults: | |
| 572 actionlist[route.defaults['action']] = True | |
| 573 | |
| 574 # Setup the lists of all controllers/actions we'll add each route | |
| 575 # to. We include the '*' in the case that a generate contains a | |
| 576 # controller/action that has no hardcodes | |
| 577 controllerlist = list(controllerlist.keys()) + ['*'] | |
| 578 actionlist = list(actionlist.keys()) + ['*'] | |
| 579 | |
| 580 # Go through our list again, assemble the controllers/actions we'll | |
| 581 # add each route to. If its hardcoded, we only add it to that dict key. | |
| 582 # Otherwise we add it to every hardcode since it can be changed. | |
| 583 gendict = {} # Our generated two-deep hash | |
| 584 for route in self.matchlist: | |
| 585 if route.static: | |
| 586 continue | |
| 587 clist = controllerlist | |
| 588 alist = actionlist | |
| 589 if 'controller' in route.hardcoded: | |
| 590 clist = [route.defaults['controller']] | |
| 591 if 'action' in route.hardcoded: | |
| 592 alist = [six.text_type(route.defaults['action'])] | |
| 593 for controller in clist: | |
| 594 for action in alist: | |
| 595 actiondict = gendict.setdefault(controller, {}) | |
| 596 actiondict.setdefault(action, ([], {}))[0].append(route) | |
| 597 self._gendict = gendict | |
| 598 self._created_gens = True | |
| 599 | |
| 600 def create_regs(self, *args, **kwargs): | |
| 601 """Atomically creates regular expressions for all connected | |
| 602 routes | |
| 603 """ | |
| 604 self.create_regs_lock.acquire() | |
| 605 try: | |
| 606 self._create_regs(*args, **kwargs) | |
| 607 finally: | |
| 608 self.create_regs_lock.release() | |
| 609 | |
| 610 def _create_regs(self, clist=None): | |
| 611 """Creates regular expressions for all connected routes""" | |
| 612 if clist is None: | |
| 613 if self.directory: | |
| 614 clist = self.controller_scan(self.directory) | |
| 615 elif callable(self.controller_scan): | |
| 616 clist = self.controller_scan() | |
| 617 elif not self.controller_scan: | |
| 618 clist = [] | |
| 619 else: | |
| 620 clist = self.controller_scan | |
| 621 | |
| 622 for key, val in six.iteritems(self.maxkeys): | |
| 623 for route in val: | |
| 624 route.makeregexp(clist) | |
| 625 | |
| 626 regexps = [] | |
| 627 routematches = [] | |
| 628 for route in self.matchlist: | |
| 629 if not route.static: | |
| 630 routematches.append(route) | |
| 631 regexps.append(route.makeregexp(clist, include_names=False)) | |
| 632 self._routematches = routematches | |
| 633 | |
| 634 # Create our regexp to strip the prefix | |
| 635 if self.prefix: | |
| 636 self._regprefix = re.compile(self.prefix + '(.*)') | |
| 637 | |
| 638 # Save the master regexp | |
| 639 regexp = '|'.join(['(?:%s)' % x for x in regexps]) | |
| 640 self._master_reg = regexp | |
| 641 try: | |
| 642 self._master_regexp = re.compile(regexp) | |
| 643 except OverflowError: | |
| 644 self._master_regexp = None | |
| 645 self._created_regs = True | |
| 646 | |
| 647 def _match(self, url, environ): | |
| 648 """Internal Route matcher | |
| 649 | |
| 650 Matches a URL against a route, and returns a tuple of the match | |
| 651 dict and the route object if a match is successfull, otherwise | |
| 652 it returns empty. | |
| 653 | |
| 654 For internal use only. | |
| 655 | |
| 656 """ | |
| 657 if not self._created_regs and self.controller_scan: | |
| 658 self.create_regs() | |
| 659 elif not self._created_regs: | |
| 660 raise RoutesException("You must generate the regular expressions" | |
| 661 " before matching.") | |
| 662 | |
| 663 if self.always_scan: | |
| 664 self.create_regs() | |
| 665 | |
| 666 matchlog = [] | |
| 667 if self.prefix: | |
| 668 if re.match(self._regprefix, url): | |
| 669 url = re.sub(self._regprefix, r'\1', url) | |
| 670 if not url: | |
| 671 url = '/' | |
| 672 else: | |
| 673 return (None, None, matchlog) | |
| 674 | |
| 675 environ = environ or self.environ | |
| 676 sub_domains = self.sub_domains | |
| 677 sub_domains_ignore = self.sub_domains_ignore | |
| 678 domain_match = self.domain_match | |
| 679 debug = self.debug | |
| 680 | |
| 681 if self._master_regexp is not None: | |
| 682 # Check to see if its a valid url against the main regexp | |
| 683 # Done for faster invalid URL elimination | |
| 684 valid_url = re.match(self._master_regexp, url) | |
| 685 else: | |
| 686 # Regex is None due to OverflowError caused by too many routes. | |
| 687 # This will allow larger projects to work but might increase time | |
| 688 # spent invalidating URLs in the loop below. | |
| 689 valid_url = True | |
| 690 if not valid_url: | |
| 691 return (None, None, matchlog) | |
| 692 | |
| 693 for route in self.matchlist: | |
| 694 if route.static: | |
| 695 if debug: | |
| 696 matchlog.append(dict(route=route, static=True)) | |
| 697 continue | |
| 698 match = route.match(url, environ, sub_domains, sub_domains_ignore, | |
| 699 domain_match) | |
| 700 if debug: | |
| 701 matchlog.append(dict(route=route, regexp=bool(match))) | |
| 702 if isinstance(match, dict) or match: | |
| 703 return (match, route, matchlog) | |
| 704 return (None, None, matchlog) | |
| 705 | |
| 706 def match(self, url=None, environ=None): | |
| 707 """Match a URL against against one of the routes contained. | |
| 708 | |
| 709 Will return None if no valid match is found. | |
| 710 | |
| 711 .. code-block:: python | |
| 712 | |
| 713 resultdict = m.match('/joe/sixpack') | |
| 714 | |
| 715 """ | |
| 716 if url is None and not environ: | |
| 717 raise RoutesException('URL or environ must be provided') | |
| 718 | |
| 719 if url is None: | |
| 720 url = environ['PATH_INFO'] | |
| 721 | |
| 722 result = self._match(url, environ) | |
| 723 if self.debug: | |
| 724 return result[0], result[1], result[2] | |
| 725 if isinstance(result[0], dict) or result[0]: | |
| 726 return result[0] | |
| 727 return None | |
| 728 | |
| 729 def routematch(self, url=None, environ=None): | |
| 730 """Match a URL against against one of the routes contained. | |
| 731 | |
| 732 Will return None if no valid match is found, otherwise a | |
| 733 result dict and a route object is returned. | |
| 734 | |
| 735 .. code-block:: python | |
| 736 | |
| 737 resultdict, route_obj = m.match('/joe/sixpack') | |
| 738 | |
| 739 """ | |
| 740 if url is None and not environ: | |
| 741 raise RoutesException('URL or environ must be provided') | |
| 742 | |
| 743 if url is None: | |
| 744 url = environ['PATH_INFO'] | |
| 745 result = self._match(url, environ) | |
| 746 if self.debug: | |
| 747 return result[0], result[1], result[2] | |
| 748 if isinstance(result[0], dict) or result[0]: | |
| 749 return result[0], result[1] | |
| 750 return None | |
| 751 | |
| 752 def generate(self, *args, **kargs): | |
| 753 """Generate a route from a set of keywords | |
| 754 | |
| 755 Returns the url text, or None if no URL could be generated. | |
| 756 | |
| 757 .. code-block:: python | |
| 758 | |
| 759 m.generate(controller='content',action='view',id=10) | |
| 760 | |
| 761 """ | |
| 762 # Generate ourself if we haven't already | |
| 763 if not self._created_gens: | |
| 764 self._create_gens() | |
| 765 | |
| 766 if self.append_slash: | |
| 767 kargs['_append_slash'] = True | |
| 768 | |
| 769 if not self.explicit: | |
| 770 if 'controller' not in kargs: | |
| 771 kargs['controller'] = 'content' | |
| 772 if 'action' not in kargs: | |
| 773 kargs['action'] = 'index' | |
| 774 | |
| 775 environ = kargs.pop('_environ', self.environ) or {} | |
| 776 if 'SCRIPT_NAME' in environ: | |
| 777 script_name = environ['SCRIPT_NAME'] | |
| 778 elif self.environ and 'SCRIPT_NAME' in self.environ: | |
| 779 script_name = self.environ['SCRIPT_NAME'] | |
| 780 else: | |
| 781 script_name = "" | |
| 782 controller = kargs.get('controller', None) | |
| 783 action = kargs.get('action', None) | |
| 784 | |
| 785 # If the URL didn't depend on the SCRIPT_NAME, we'll cache it | |
| 786 # keyed by just by kargs; otherwise we need to cache it with | |
| 787 # both SCRIPT_NAME and kargs: | |
| 788 cache_key = six.text_type(args).encode('utf8') + \ | |
| 789 six.text_type(kargs).encode('utf8') | |
| 790 | |
| 791 if self.urlcache is not None: | |
| 792 if six.PY3: | |
| 793 cache_key_script_name = b':'.join((script_name.encode('utf-8'), | |
| 794 cache_key)) | |
| 795 else: | |
| 796 cache_key_script_name = '%s:%s' % (script_name, cache_key) | |
| 797 | |
| 798 # Check the url cache to see if it exists, use it if it does | |
| 799 val = self.urlcache.get(cache_key_script_name, self) | |
| 800 if val != self: | |
| 801 return val | |
| 802 | |
| 803 controller = as_unicode(controller, self.encoding) | |
| 804 action = as_unicode(action, self.encoding) | |
| 805 | |
| 806 actionlist = self._gendict.get(controller) or self._gendict.get('*', {}) | |
| 807 if not actionlist and not args: | |
| 808 return None | |
| 809 (keylist, sortcache) = actionlist.get(action) or \ | |
| 810 actionlist.get('*', (None, {})) | |
| 811 if not keylist and not args: | |
| 812 return None | |
| 813 | |
| 814 keys = frozenset(kargs.keys()) | |
| 815 cacheset = False | |
| 816 cachekey = six.text_type(keys) | |
| 817 cachelist = sortcache.get(cachekey) | |
| 818 if args: | |
| 819 keylist = args | |
| 820 elif cachelist: | |
| 821 keylist = cachelist | |
| 822 else: | |
| 823 cacheset = True | |
| 824 newlist = [] | |
| 825 for route in keylist: | |
| 826 if len(route.minkeys - route.dotkeys - keys) == 0: | |
| 827 newlist.append(route) | |
| 828 keylist = newlist | |
| 829 | |
| 830 class KeySorter: | |
| 831 | |
| 832 def __init__(self, obj, *args): | |
| 833 self.obj = obj | |
| 834 | |
| 835 def __lt__(self, other): | |
| 836 return self._keysort(self.obj, other.obj) < 0 | |
| 837 | |
| 838 def _keysort(self, a, b): | |
| 839 """Sorts two sets of sets, to order them ideally for | |
| 840 matching.""" | |
| 841 a = a.maxkeys | |
| 842 b = b.maxkeys | |
| 843 | |
| 844 lendiffa = len(keys ^ a) | |
| 845 lendiffb = len(keys ^ b) | |
| 846 # If they both match, don't switch them | |
| 847 if lendiffa == 0 and lendiffb == 0: | |
| 848 return 0 | |
| 849 | |
| 850 # First, if a matches exactly, use it | |
| 851 if lendiffa == 0: | |
| 852 return -1 | |
| 853 | |
| 854 # Or b matches exactly, use it | |
| 855 if lendiffb == 0: | |
| 856 return 1 | |
| 857 | |
| 858 # Neither matches exactly, return the one with the most in | |
| 859 # common | |
| 860 if self._compare(lendiffa, lendiffb) != 0: | |
| 861 return self._compare(lendiffa, lendiffb) | |
| 862 | |
| 863 # Neither matches exactly, but if they both have just as | |
| 864 # much in common | |
| 865 if len(keys & b) == len(keys & a): | |
| 866 # Then we return the shortest of the two | |
| 867 return self._compare(len(a), len(b)) | |
| 868 | |
| 869 # Otherwise, we return the one that has the most in common | |
| 870 else: | |
| 871 return self._compare(len(keys & b), len(keys & a)) | |
| 872 | |
| 873 def _compare(self, obj1, obj2): | |
| 874 if obj1 < obj2: | |
| 875 return -1 | |
| 876 elif obj1 < obj2: | |
| 877 return 1 | |
| 878 else: | |
| 879 return 0 | |
| 880 | |
| 881 keylist.sort(key=KeySorter) | |
| 882 if cacheset: | |
| 883 sortcache[cachekey] = keylist | |
| 884 | |
| 885 # Iterate through the keylist of sorted routes (or a single route if | |
| 886 # it was passed in explicitly for hardcoded named routes) | |
| 887 for route in keylist: | |
| 888 fail = False | |
| 889 for key in route.hardcoded: | |
| 890 kval = kargs.get(key) | |
| 891 if not kval: | |
| 892 continue | |
| 893 kval = as_unicode(kval, self.encoding) | |
| 894 if kval != route.defaults[key] and \ | |
| 895 not callable(route.defaults[key]): | |
| 896 fail = True | |
| 897 break | |
| 898 if fail: | |
| 899 continue | |
| 900 path = route.generate(**kargs) | |
| 901 if path: | |
| 902 if self.prefix: | |
| 903 path = self.prefix + path | |
| 904 external_static = route.static and route.external | |
| 905 if not route.absolute and not external_static: | |
| 906 path = script_name + path | |
| 907 key = cache_key_script_name | |
| 908 else: | |
| 909 key = cache_key | |
| 910 if self.urlcache is not None: | |
| 911 self.urlcache.put(key, str(path)) | |
| 912 return str(path) | |
| 913 else: | |
| 914 continue | |
| 915 return None | |
| 916 | |
| 917 def resource(self, member_name, collection_name, **kwargs): | |
| 918 """Generate routes for a controller resource | |
| 919 | |
| 920 The member_name name should be the appropriate singular version | |
| 921 of the resource given your locale and used with members of the | |
| 922 collection. The collection_name name will be used to refer to | |
| 923 the resource collection methods and should be a plural version | |
| 924 of the member_name argument. By default, the member_name name | |
| 925 will also be assumed to map to a controller you create. | |
| 926 | |
| 927 The concept of a web resource maps somewhat directly to 'CRUD' | |
| 928 operations. The overlying things to keep in mind is that | |
| 929 mapping a resource is about handling creating, viewing, and | |
| 930 editing that resource. | |
| 931 | |
| 932 All keyword arguments are optional. | |
| 933 | |
| 934 ``controller`` | |
| 935 If specified in the keyword args, the controller will be | |
| 936 the actual controller used, but the rest of the naming | |
| 937 conventions used for the route names and URL paths are | |
| 938 unchanged. | |
| 939 | |
| 940 ``collection`` | |
| 941 Additional action mappings used to manipulate/view the | |
| 942 entire set of resources provided by the controller. | |
| 943 | |
| 944 Example:: | |
| 945 | |
| 946 map.resource('message', 'messages', collection={'rss':'GET'}) | |
| 947 # GET /message/rss (maps to the rss action) | |
| 948 # also adds named route "rss_message" | |
| 949 | |
| 950 ``member`` | |
| 951 Additional action mappings used to access an individual | |
| 952 'member' of this controllers resources. | |
| 953 | |
| 954 Example:: | |
| 955 | |
| 956 map.resource('message', 'messages', member={'mark':'POST'}) | |
| 957 # POST /message/1/mark (maps to the mark action) | |
| 958 # also adds named route "mark_message" | |
| 959 | |
| 960 ``new`` | |
| 961 Action mappings that involve dealing with a new member in | |
| 962 the controller resources. | |
| 963 | |
| 964 Example:: | |
| 965 | |
| 966 map.resource('message', 'messages', new={'preview':'POST'}) | |
| 967 # POST /message/new/preview (maps to the preview action) | |
| 968 # also adds a url named "preview_new_message" | |
| 969 | |
| 970 ``path_prefix`` | |
| 971 Prepends the URL path for the Route with the path_prefix | |
| 972 given. This is most useful for cases where you want to mix | |
| 973 resources or relations between resources. | |
| 974 | |
| 975 ``name_prefix`` | |
| 976 Perpends the route names that are generated with the | |
| 977 name_prefix given. Combined with the path_prefix option, | |
| 978 it's easy to generate route names and paths that represent | |
| 979 resources that are in relations. | |
| 980 | |
| 981 Example:: | |
| 982 | |
| 983 map.resource('message', 'messages', controller='categories', | |
| 984 path_prefix='/category/:category_id', | |
| 985 name_prefix="category_") | |
| 986 # GET /category/7/message/1 | |
| 987 # has named route "category_message" | |
| 988 | |
| 989 ``requirements`` | |
| 990 | |
| 991 A dictionary that restricts the matching of a | |
| 992 variable. Can be used when matching variables with path_prefix. | |
| 993 | |
| 994 Example:: | |
| 995 | |
| 996 map.resource('message', 'messages', | |
| 997 path_prefix='{project_id}/', | |
| 998 requirements={"project_id": R"\d+"}) | |
| 999 # POST /01234/message | |
| 1000 # success, project_id is set to "01234" | |
| 1001 # POST /foo/message | |
| 1002 # 404 not found, won't be matched by this route | |
| 1003 | |
| 1004 | |
| 1005 ``parent_resource`` | |
| 1006 A ``dict`` containing information about the parent | |
| 1007 resource, for creating a nested resource. It should contain | |
| 1008 the ``member_name`` and ``collection_name`` of the parent | |
| 1009 resource. This ``dict`` will | |
| 1010 be available via the associated ``Route`` object which can | |
| 1011 be accessed during a request via | |
| 1012 ``request.environ['routes.route']`` | |
| 1013 | |
| 1014 If ``parent_resource`` is supplied and ``path_prefix`` | |
| 1015 isn't, ``path_prefix`` will be generated from | |
| 1016 ``parent_resource`` as | |
| 1017 "<parent collection name>/:<parent member name>_id". | |
| 1018 | |
| 1019 If ``parent_resource`` is supplied and ``name_prefix`` | |
| 1020 isn't, ``name_prefix`` will be generated from | |
| 1021 ``parent_resource`` as "<parent member name>_". | |
| 1022 | |
| 1023 Example:: | |
| 1024 | |
| 1025 >>> from routes.util import url_for | |
| 1026 >>> m = Mapper() | |
| 1027 >>> m.resource('location', 'locations', | |
| 1028 ... parent_resource=dict(member_name='region', | |
| 1029 ... collection_name='regions')) | |
| 1030 >>> # path_prefix is "regions/:region_id" | |
| 1031 >>> # name prefix is "region_" | |
| 1032 >>> url_for('region_locations', region_id=13) | |
| 1033 '/regions/13/locations' | |
| 1034 >>> url_for('region_new_location', region_id=13) | |
| 1035 '/regions/13/locations/new' | |
| 1036 >>> url_for('region_location', region_id=13, id=60) | |
| 1037 '/regions/13/locations/60' | |
| 1038 >>> url_for('region_edit_location', region_id=13, id=60) | |
| 1039 '/regions/13/locations/60/edit' | |
| 1040 | |
| 1041 Overriding generated ``path_prefix``:: | |
| 1042 | |
| 1043 >>> m = Mapper() | |
| 1044 >>> m.resource('location', 'locations', | |
| 1045 ... parent_resource=dict(member_name='region', | |
| 1046 ... collection_name='regions'), | |
| 1047 ... path_prefix='areas/:area_id') | |
| 1048 >>> # name prefix is "region_" | |
| 1049 >>> url_for('region_locations', area_id=51) | |
| 1050 '/areas/51/locations' | |
| 1051 | |
| 1052 Overriding generated ``name_prefix``:: | |
| 1053 | |
| 1054 >>> m = Mapper() | |
| 1055 >>> m.resource('location', 'locations', | |
| 1056 ... parent_resource=dict(member_name='region', | |
| 1057 ... collection_name='regions'), | |
| 1058 ... name_prefix='') | |
| 1059 >>> # path_prefix is "regions/:region_id" | |
| 1060 >>> url_for('locations', region_id=51) | |
| 1061 '/regions/51/locations' | |
| 1062 | |
| 1063 """ | |
| 1064 collection = kwargs.pop('collection', {}) | |
| 1065 member = kwargs.pop('member', {}) | |
| 1066 new = kwargs.pop('new', {}) | |
| 1067 path_prefix = kwargs.pop('path_prefix', None) | |
| 1068 name_prefix = kwargs.pop('name_prefix', None) | |
| 1069 parent_resource = kwargs.pop('parent_resource', None) | |
| 1070 | |
| 1071 # Generate ``path_prefix`` if ``path_prefix`` wasn't specified and | |
| 1072 # ``parent_resource`` was. Likewise for ``name_prefix``. Make sure | |
| 1073 # that ``path_prefix`` and ``name_prefix`` *always* take precedence if | |
| 1074 # they are specified--in particular, we need to be careful when they | |
| 1075 # are explicitly set to "". | |
| 1076 if parent_resource is not None: | |
| 1077 if path_prefix is None: | |
| 1078 path_prefix = '%s/:%s_id' % (parent_resource['collection_name'], | |
| 1079 parent_resource['member_name']) | |
| 1080 if name_prefix is None: | |
| 1081 name_prefix = '%s_' % parent_resource['member_name'] | |
| 1082 else: | |
| 1083 if path_prefix is None: | |
| 1084 path_prefix = '' | |
| 1085 if name_prefix is None: | |
| 1086 name_prefix = '' | |
| 1087 | |
| 1088 # Ensure the edit and new actions are in and GET | |
| 1089 member['edit'] = 'GET' | |
| 1090 new.update({'new': 'GET'}) | |
| 1091 | |
| 1092 # Make new dict's based off the old, except the old values become keys, | |
| 1093 # and the old keys become items in a list as the value | |
| 1094 def swap(dct, newdct): | |
| 1095 """Swap the keys and values in the dict, and uppercase the values | |
| 1096 from the dict during the swap.""" | |
| 1097 for key, val in six.iteritems(dct): | |
| 1098 newdct.setdefault(val.upper(), []).append(key) | |
| 1099 return newdct | |
| 1100 collection_methods = swap(collection, {}) | |
| 1101 member_methods = swap(member, {}) | |
| 1102 new_methods = swap(new, {}) | |
| 1103 | |
| 1104 # Insert create, update, and destroy methods | |
| 1105 collection_methods.setdefault('POST', []).insert(0, 'create') | |
| 1106 member_methods.setdefault('PUT', []).insert(0, 'update') | |
| 1107 member_methods.setdefault('DELETE', []).insert(0, 'delete') | |
| 1108 | |
| 1109 # If there's a path prefix option, use it with the controller | |
| 1110 controller = strip_slashes(collection_name) | |
| 1111 path_prefix = strip_slashes(path_prefix) | |
| 1112 path_prefix = '/' + path_prefix | |
| 1113 if path_prefix and path_prefix != '/': | |
| 1114 path = path_prefix + '/' + controller | |
| 1115 else: | |
| 1116 path = '/' + controller | |
| 1117 collection_path = path | |
| 1118 new_path = path + "/new" | |
| 1119 member_path = path + "/:(id)" | |
| 1120 | |
| 1121 options = { | |
| 1122 'controller': kwargs.get('controller', controller), | |
| 1123 '_member_name': member_name, | |
| 1124 '_collection_name': collection_name, | |
| 1125 '_parent_resource': parent_resource, | |
| 1126 '_filter': kwargs.get('_filter') | |
| 1127 } | |
| 1128 if 'requirements' in kwargs: | |
| 1129 options['requirements'] = kwargs['requirements'] | |
| 1130 | |
| 1131 def requirements_for(meth): | |
| 1132 """Returns a new dict to be used for all route creation as the | |
| 1133 route options""" | |
| 1134 opts = options.copy() | |
| 1135 if method != 'any': | |
| 1136 opts['conditions'] = {'method': [meth.upper()]} | |
| 1137 return opts | |
| 1138 | |
| 1139 # Add the routes for handling collection methods | |
| 1140 for method, lst in six.iteritems(collection_methods): | |
| 1141 primary = (method != 'GET' and lst.pop(0)) or None | |
| 1142 route_options = requirements_for(method) | |
| 1143 for action in lst: | |
| 1144 route_options['action'] = action | |
| 1145 route_name = "%s%s_%s" % (name_prefix, action, collection_name) | |
| 1146 self.connect("formatted_" + route_name, "%s/%s.:(format)" % | |
| 1147 (collection_path, action), **route_options) | |
| 1148 self.connect(route_name, "%s/%s" % (collection_path, action), | |
| 1149 **route_options) | |
| 1150 if primary: | |
| 1151 route_options['action'] = primary | |
| 1152 self.connect("%s.:(format)" % collection_path, **route_options) | |
| 1153 self.connect(collection_path, **route_options) | |
| 1154 | |
| 1155 # Specifically add in the built-in 'index' collection method and its | |
| 1156 # formatted version | |
| 1157 self.connect("formatted_" + name_prefix + collection_name, | |
| 1158 collection_path + ".:(format)", action='index', | |
| 1159 conditions={'method': ['GET']}, **options) | |
| 1160 self.connect(name_prefix + collection_name, collection_path, | |
| 1161 action='index', conditions={'method': ['GET']}, **options) | |
| 1162 | |
| 1163 # Add the routes that deal with new resource methods | |
| 1164 for method, lst in six.iteritems(new_methods): | |
| 1165 route_options = requirements_for(method) | |
| 1166 for action in lst: | |
| 1167 name = "new_" + member_name | |
| 1168 route_options['action'] = action | |
| 1169 if action == 'new': | |
| 1170 path = new_path | |
| 1171 formatted_path = new_path + '.:(format)' | |
| 1172 else: | |
| 1173 path = "%s/%s" % (new_path, action) | |
| 1174 name = action + "_" + name | |
| 1175 formatted_path = "%s/%s.:(format)" % (new_path, action) | |
| 1176 self.connect("formatted_" + name_prefix + name, formatted_path, | |
| 1177 **route_options) | |
| 1178 self.connect(name_prefix + name, path, **route_options) | |
| 1179 | |
| 1180 requirements_regexp = '[^\/]+(?<!\\\)' | |
| 1181 | |
| 1182 # Add the routes that deal with member methods of a resource | |
| 1183 for method, lst in six.iteritems(member_methods): | |
| 1184 route_options = requirements_for(method) | |
| 1185 route_options['requirements'] = {'id': requirements_regexp} | |
| 1186 if method not in ['POST', 'GET', 'any']: | |
| 1187 primary = lst.pop(0) | |
| 1188 else: | |
| 1189 primary = None | |
| 1190 for action in lst: | |
| 1191 route_options['action'] = action | |
| 1192 self.connect("formatted_%s%s_%s" % (name_prefix, action, | |
| 1193 member_name), | |
| 1194 "%s/%s.:(format)" % (member_path, action), | |
| 1195 **route_options) | |
| 1196 self.connect("%s%s_%s" % (name_prefix, action, member_name), | |
| 1197 "%s/%s" % (member_path, action), **route_options) | |
| 1198 if primary: | |
| 1199 route_options['action'] = primary | |
| 1200 self.connect("%s.:(format)" % member_path, **route_options) | |
| 1201 self.connect(member_path, **route_options) | |
| 1202 | |
| 1203 # Specifically add the member 'show' method | |
| 1204 route_options = requirements_for('GET') | |
| 1205 route_options['action'] = 'show' | |
| 1206 route_options['requirements'] = {'id': requirements_regexp} | |
| 1207 self.connect("formatted_" + name_prefix + member_name, | |
| 1208 member_path + ".:(format)", **route_options) | |
| 1209 self.connect(name_prefix + member_name, member_path, **route_options) | |
| 1210 | |
| 1211 def redirect(self, match_path, destination_path, *args, **kwargs): | |
| 1212 """Add a redirect route to the mapper | |
| 1213 | |
| 1214 Redirect routes bypass the wrapped WSGI application and instead | |
| 1215 result in a redirect being issued by the RoutesMiddleware. As | |
| 1216 such, this method is only meaningful when using | |
| 1217 RoutesMiddleware. | |
| 1218 | |
| 1219 By default, a 302 Found status code is used, this can be | |
| 1220 changed by providing a ``_redirect_code`` keyword argument | |
| 1221 which will then be used instead. Note that the entire status | |
| 1222 code string needs to be present. | |
| 1223 | |
| 1224 When using keyword arguments, all arguments that apply to | |
| 1225 matching will be used for the match, while generation specific | |
| 1226 options will be used during generation. Thus all options | |
| 1227 normally available to connected Routes may be used with | |
| 1228 redirect routes as well. | |
| 1229 | |
| 1230 Example:: | |
| 1231 | |
| 1232 map = Mapper() | |
| 1233 map.redirect('/legacyapp/archives/{url:.*}', '/archives/{url}') | |
| 1234 map.redirect('/home/index', '/', | |
| 1235 _redirect_code='301 Moved Permanently') | |
| 1236 | |
| 1237 """ | |
| 1238 both_args = ['_encoding', '_explicit', '_minimize'] | |
| 1239 gen_args = ['_filter'] | |
| 1240 | |
| 1241 status_code = kwargs.pop('_redirect_code', '302 Found') | |
| 1242 gen_dict, match_dict = {}, {} | |
| 1243 | |
| 1244 # Create the dict of args for the generation route | |
| 1245 for key in both_args + gen_args: | |
| 1246 if key in kwargs: | |
| 1247 gen_dict[key] = kwargs[key] | |
| 1248 gen_dict['_static'] = True | |
| 1249 | |
| 1250 # Create the dict of args for the matching route | |
| 1251 for key in kwargs: | |
| 1252 if key not in gen_args: | |
| 1253 match_dict[key] = kwargs[key] | |
| 1254 | |
| 1255 self.connect(match_path, **match_dict) | |
| 1256 match_route = self.matchlist[-1] | |
| 1257 | |
| 1258 self.connect('_redirect_%s' % id(match_route), destination_path, | |
| 1259 **gen_dict) | |
| 1260 match_route.redirect = True | |
| 1261 match_route.redirect_status = status_code |
