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