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