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