comparison env/lib/python3.9/site-packages/routes/mapper.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:4f3585e2f14b
1 """Mapper and Sub-Mapper"""
2 import collections
3 import itertools as it
4 import re
5 import threading
6
7 from repoze.lru import LRUCache
8 import six
9
10 from routes import request_config
11 from routes.util import (
12 controller_scan,
13 RoutesException,
14 as_unicode
15 )
16 from routes.route import Route
17
18
19 COLLECTION_ACTIONS = ['index', 'create', 'new']
20 MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit']
21
22
23 def 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
32 class 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
147 class 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
306 def _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
315 class 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