diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.9/site-packages/routes/mapper.py	Mon Mar 22 18:12:50 2021 +0000
@@ -0,0 +1,1277 @@
+"""Mapper and Sub-Mapper"""
+import collections
+import itertools as it
+import re
+import threading
+
+from repoze.lru import LRUCache
+import six
+
+from routes import request_config
+from routes.util import (
+    controller_scan,
+    RoutesException,
+    as_unicode
+)
+from routes.route import Route
+
+
+COLLECTION_ACTIONS = ['index', 'create', 'new']
+MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit']
+
+
+def strip_slashes(name):
+    """Remove slashes from the beginning and end of a part/URL."""
+    if name.startswith('/'):
+        name = name[1:]
+    if name.endswith('/'):
+        name = name[:-1]
+    return name
+
+
+class SubMapperParent(object):
+    """Base class for Mapper and SubMapper, both of which may be the parent
+    of SubMapper objects
+    """
+
+    def submapper(self, **kargs):
+        """Create a partial version of the Mapper with the designated
+        options set
+
+        This results in a :class:`routes.mapper.SubMapper` object.
+
+        If keyword arguments provided to this method also exist in the
+        keyword arguments provided to the submapper, their values will
+        be merged with the saved options going first.
+
+        In addition to :class:`routes.route.Route` arguments, submapper
+        can also take a ``path_prefix`` argument which will be
+        prepended to the path of all routes that are connected.
+
+        Example::
+
+            >>> map = Mapper(controller_scan=None)
+            >>> map.connect('home', '/', controller='home', action='splash')
+            >>> map.matchlist[0].name == 'home'
+            True
+            >>> m = map.submapper(controller='home')
+            >>> m.connect('index', '/index', action='index')
+            >>> map.matchlist[1].name == 'index'
+            True
+            >>> map.matchlist[1].defaults['controller'] == 'home'
+            True
+
+        Optional ``collection_name`` and ``resource_name`` arguments are
+        used in the generation of route names by the ``action`` and
+        ``link`` methods.  These in turn are used by the ``index``,
+        ``new``, ``create``, ``show``, ``edit``, ``update`` and
+        ``delete`` methods which may be invoked indirectly by listing
+        them in the ``actions`` argument.  If the ``formatted`` argument
+        is set to ``True`` (the default), generated paths are given the
+        suffix '{.format}' which matches or generates an optional format
+        extension.
+
+        Example::
+
+            >>> from routes.util import url_for
+            >>> map = Mapper(controller_scan=None)
+            >>> m = map.submapper(path_prefix='/entries', collection_name='entries', resource_name='entry', actions=['index', 'new'])
+            >>> url_for('entries') == '/entries'
+            True
+            >>> url_for('new_entry', format='xml') == '/entries/new.xml'
+            True
+
+        """
+        return SubMapper(self, **kargs)
+
+    def collection(self, collection_name, resource_name, path_prefix=None,
+                   member_prefix='/{id}', controller=None,
+                   collection_actions=COLLECTION_ACTIONS,
+                   member_actions=MEMBER_ACTIONS, member_options=None,
+                   **kwargs):
+        """Create a submapper that represents a collection.
+
+        This results in a :class:`routes.mapper.SubMapper` object, with a
+        ``member`` property of the same type that represents the collection's
+        member resources.
+
+        Its interface is the same as the ``submapper`` together with
+        ``member_prefix``, ``member_actions`` and ``member_options``
+        which are passed to the ``member`` submapper as ``path_prefix``,
+        ``actions`` and keyword arguments respectively.
+
+        Example::
+
+            >>> from routes.util import url_for
+            >>> map = Mapper(controller_scan=None)
+            >>> c = map.collection('entries', 'entry')
+            >>> c.member.link('ping', method='POST')
+            >>> url_for('entries') == '/entries'
+            True
+            >>> url_for('edit_entry', id=1) == '/entries/1/edit'
+            True
+            >>> url_for('ping_entry', id=1) == '/entries/1/ping'
+            True
+
+        """
+        if controller is None:
+            controller = resource_name or collection_name
+
+        if path_prefix is None:
+            if collection_name is None:
+                path_prefix_str = ''
+            else:
+                path_prefix_str = '/{collection_name}'
+        else:
+            if collection_name is None:
+                path_prefix_str = "{pre}"
+            else:
+                path_prefix_str = "{pre}/{collection_name}"
+
+        # generate what will be the path prefix for the collection
+        path_prefix = path_prefix_str.format(pre=path_prefix,
+                                             collection_name=collection_name)
+
+        collection = SubMapper(self, collection_name=collection_name,
+                               resource_name=resource_name,
+                               path_prefix=path_prefix, controller=controller,
+                               actions=collection_actions, **kwargs)
+
+        collection.member = SubMapper(collection, path_prefix=member_prefix,
+                                      actions=member_actions,
+                                      **(member_options or {}))
+
+        return collection
+
+
+class SubMapper(SubMapperParent):
+    """Partial mapper for use with_options"""
+    def __init__(self, obj, resource_name=None, collection_name=None,
+                 actions=None, formatted=None, **kwargs):
+        self.kwargs = kwargs
+        self.obj = obj
+        self.collection_name = collection_name
+        self.member = None
+        self.resource_name = resource_name \
+            or getattr(obj, 'resource_name', None) \
+            or kwargs.get('controller', None) \
+            or getattr(obj, 'controller', None)
+        if formatted is not None:
+            self.formatted = formatted
+        else:
+            self.formatted = getattr(obj, 'formatted', None)
+            if self.formatted is None:
+                self.formatted = True
+        self.add_actions(actions or [], **kwargs)
+
+    def connect(self, routename, path=None, **kwargs):
+        newkargs = {}
+        _routename = routename
+        _path = path
+        for key, value in six.iteritems(self.kwargs):
+            if key == 'path_prefix':
+                if path is not None:
+                    # if there's a name_prefix, add it to the route name
+                    # and if there's a path_prefix
+                    _path = ''.join((self.kwargs[key], path))
+                else:
+                    _path = ''.join((self.kwargs[key], routename))
+            elif key == 'name_prefix':
+                if path is not None:
+                    # if there's a name_prefix, add it to the route name
+                    # and if there's a path_prefix
+                    _routename = ''.join((self.kwargs[key], routename))
+                else:
+                    _routename = None
+            elif key in kwargs:
+                if isinstance(value, dict):
+                    newkargs[key] = dict(value, **kwargs[key])  # merge dicts
+                else:
+                    # Originally used this form:
+                    # newkargs[key] = value + kwargs[key]
+                    # New version avoids the inheritance concatenation issue
+                    # with submappers. Only prefixes concatenate, everything
+                    # else overrides in submappers.
+                    newkargs[key] = kwargs[key]
+            else:
+                newkargs[key] = self.kwargs[key]
+        for key in kwargs:
+            if key not in self.kwargs:
+                newkargs[key] = kwargs[key]
+
+        newargs = (_routename, _path)
+        return self.obj.connect(*newargs, **newkargs)
+
+    def link(self, rel=None, name=None, action=None, method='GET',
+             formatted=None, **kwargs):
+        """Generates a named route for a subresource.
+
+        Example::
+
+            >>> from routes.util import url_for
+            >>> map = Mapper(controller_scan=None)
+            >>> c = map.collection('entries', 'entry')
+            >>> c.link('recent', name='recent_entries')
+            >>> c.member.link('ping', method='POST', formatted=True)
+            >>> url_for('entries') == '/entries'
+            True
+            >>> url_for('recent_entries') == '/entries/recent'
+            True
+            >>> url_for('ping_entry', id=1) == '/entries/1/ping'
+            True
+            >>> url_for('ping_entry', id=1, format='xml') == '/entries/1/ping.xml'
+            True
+
+        """
+        if formatted or (formatted is None and self.formatted):
+            suffix = '{.format}'
+        else:
+            suffix = ''
+
+        return self.connect(name or (rel + '_' + self.resource_name),
+                            '/' + (rel or name) + suffix,
+                            action=action or rel or name,
+                            **_kwargs_with_conditions(kwargs, method))
+
+    def new(self, **kwargs):
+        """Generates the "new" link for a collection submapper."""
+        return self.link(rel='new', **kwargs)
+
+    def edit(self, **kwargs):
+        """Generates the "edit" link for a collection member submapper."""
+        return self.link(rel='edit', **kwargs)
+
+    def action(self, name=None, action=None, method='GET', formatted=None,
+               **kwargs):
+        """Generates a named route at the base path of a submapper.
+
+        Example::
+
+            >>> from routes import url_for
+            >>> map = Mapper(controller_scan=None)
+            >>> c = map.submapper(path_prefix='/entries', controller='entry')
+            >>> c.action(action='index', name='entries', formatted=True)
+            >>> c.action(action='create', method='POST')
+            >>> url_for(controller='entry', action='index', method='GET') == '/entries'
+            True
+            >>> url_for(controller='entry', action='index', method='GET', format='xml') == '/entries.xml'
+            True
+            >>> url_for(controller='entry', action='create', method='POST') == '/entries'
+            True
+
+        """
+        if formatted or (formatted is None and self.formatted):
+            suffix = '{.format}'
+        else:
+            suffix = ''
+        return self.connect(name or (action + '_' + self.resource_name),
+                            suffix,
+                            action=action or name,
+                            **_kwargs_with_conditions(kwargs, method))
+
+    def index(self, name=None, **kwargs):
+        """Generates the "index" action for a collection submapper."""
+        return self.action(name=name or self.collection_name,
+                           action='index', method='GET', **kwargs)
+
+    def show(self, name=None, **kwargs):
+        """Generates the "show" action for a collection member submapper."""
+        return self.action(name=name or self.resource_name,
+                           action='show', method='GET', **kwargs)
+
+    def create(self, **kwargs):
+        """Generates the "create" action for a collection submapper."""
+        return self.action(action='create', method='POST', **kwargs)
+
+    def update(self, **kwargs):
+        """Generates the "update" action for a collection member submapper."""
+        return self.action(action='update', method='PUT', **kwargs)
+
+    def delete(self, **kwargs):
+        """Generates the "delete" action for a collection member submapper."""
+        return self.action(action='delete', method='DELETE', **kwargs)
+
+    def add_actions(self, actions, **kwargs):
+        [getattr(self, action)(**kwargs) for action in actions]
+
+    # Provided for those who prefer using the 'with' syntax in Python 2.5+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, tb):
+        pass
+
+
+# Create kwargs with a 'conditions' member generated for the given method
+def _kwargs_with_conditions(kwargs, method):
+    if method and 'conditions' not in kwargs:
+        newkwargs = kwargs.copy()
+        newkwargs['conditions'] = {'method': method}
+        return newkwargs
+    else:
+        return kwargs
+
+
+class Mapper(SubMapperParent):
+    """Mapper handles URL generation and URL recognition in a web
+    application.
+
+    Mapper is built handling dictionary's. It is assumed that the web
+    application will handle the dictionary returned by URL recognition
+    to dispatch appropriately.
+
+    URL generation is done by passing keyword parameters into the
+    generate function, a URL is then returned.
+
+    """
+    def __init__(self, controller_scan=controller_scan, directory=None,
+                 always_scan=False, register=True, explicit=True):
+        """Create a new Mapper instance
+
+        All keyword arguments are optional.
+
+        ``controller_scan``
+            Function reference that will be used to return a list of
+            valid controllers used during URL matching. If
+            ``directory`` keyword arg is present, it will be passed
+            into the function during its call. This option defaults to
+            a function that will scan a directory for controllers.
+
+            Alternatively, a list of controllers or None can be passed
+            in which are assumed to be the definitive list of
+            controller names valid when matching 'controller'.
+
+        ``directory``
+            Passed into controller_scan for the directory to scan. It
+            should be an absolute path if using the default
+            ``controller_scan`` function.
+
+        ``always_scan``
+            Whether or not the ``controller_scan`` function should be
+            run during every URL match. This is typically a good idea
+            during development so the server won't need to be restarted
+            anytime a controller is added.
+
+        ``register``
+            Boolean used to determine if the Mapper should use
+            ``request_config`` to register itself as the mapper. Since
+            it's done on a thread-local basis, this is typically best
+            used during testing though it won't hurt in other cases.
+
+        ``explicit``
+            Boolean used to determine if routes should be connected
+            with implicit defaults of::
+
+                {'controller':'content','action':'index','id':None}
+
+            When set to True, these defaults will not be added to route
+            connections and ``url_for`` will not use Route memory.
+
+        Additional attributes that may be set after mapper
+        initialization (ie, map.ATTRIBUTE = 'something'):
+
+        ``encoding``
+            Used to indicate alternative encoding/decoding systems to
+            use with both incoming URL's, and during Route generation
+            when passed a Unicode string. Defaults to 'utf-8'.
+
+        ``decode_errors``
+            How to handle errors in the encoding, generally ignoring
+            any chars that don't convert should be sufficient. Defaults
+            to 'ignore'.
+
+        ``minimization``
+            Boolean used to indicate whether or not Routes should
+            minimize URL's and the generated URL's, or require every
+            part where it appears in the path. Defaults to False.
+
+        ``hardcode_names``
+            Whether or not Named Routes result in the default options
+            for the route being used *or* if they actually force url
+            generation to use the route. Defaults to False.
+
+        """
+        self.matchlist = []
+        self.maxkeys = {}
+        self.minkeys = {}
+        self.urlcache = LRUCache(1600)
+        self._created_regs = False
+        self._created_gens = False
+        self._master_regexp = None
+        self.prefix = None
+        self.req_data = threading.local()
+        self.directory = directory
+        self.always_scan = always_scan
+        self.controller_scan = controller_scan
+        self._regprefix = None
+        self._routenames = {}
+        self.debug = False
+        self.append_slash = False
+        self.sub_domains = False
+        self.sub_domains_ignore = []
+        self.domain_match = r'[^\.\/]+?\.[^\.\/]+'
+        self.explicit = explicit
+        self.encoding = 'utf-8'
+        self.decode_errors = 'ignore'
+        self.hardcode_names = True
+        self.minimization = False
+        self.create_regs_lock = threading.Lock()
+        if register:
+            config = request_config()
+            config.mapper = self
+
+    def __str__(self):
+        """Generates a tabular string representation."""
+        def format_methods(r):
+            if r.conditions:
+                method = r.conditions.get('method', '')
+                return type(method) is str and method or ', '.join(method)
+            else:
+                return ''
+
+        table = [('Route name', 'Methods', 'Path', 'Controller', 'action')] + \
+                [(r.name or '', format_methods(r), r.routepath or '',
+                  r.defaults.get('controller', ''), r.defaults.get('action', ''))
+                 for r in self.matchlist]
+
+        widths = [max(len(row[col]) for row in table)
+                  for col in range(len(table[0]))]
+
+        return '\n'.join(
+            ' '.join(row[col].ljust(widths[col])
+                     for col in range(len(widths)))
+            for row in table)
+
+    def _envget(self):
+        try:
+            return self.req_data.environ
+        except AttributeError:
+            return None
+
+    def _envset(self, env):
+        self.req_data.environ = env
+
+    def _envdel(self):
+        del self.req_data.environ
+    environ = property(_envget, _envset, _envdel)
+
+    def extend(self, routes, path_prefix=''):
+        """Extends the mapper routes with a list of Route objects
+
+        If a path_prefix is provided, all the routes will have their
+        path prepended with the path_prefix.
+
+        Example::
+
+            >>> map = Mapper(controller_scan=None)
+            >>> map.connect('home', '/', controller='home', action='splash')
+            >>> map.matchlist[0].name == 'home'
+            True
+            >>> routes = [Route('index', '/index.htm', controller='home',
+            ...                 action='index')]
+            >>> map.extend(routes)
+            >>> len(map.matchlist) == 2
+            True
+            >>> map.extend(routes, path_prefix='/subapp')
+            >>> len(map.matchlist) == 3
+            True
+            >>> map.matchlist[2].routepath == '/subapp/index.htm'
+            True
+
+        .. note::
+
+            This function does not merely extend the mapper with the
+            given list of routes, it actually creates new routes with
+            identical calling arguments.
+
+        """
+        for route in routes:
+            if path_prefix and route.minimization:
+                routepath = '/'.join([path_prefix, route.routepath])
+            elif path_prefix:
+                routepath = path_prefix + route.routepath
+            else:
+                routepath = route.routepath
+            self.connect(route.name,
+                         routepath,
+                         conditions=route.conditions,
+                         **route._kargs
+                         )
+
+    def make_route(self, *args, **kargs):
+        """Make a new Route object
+
+        A subclass can override this method to use a custom Route class.
+        """
+        return Route(*args, **kargs)
+
+    def connect(self, *args, **kargs):
+        """Create and connect a new Route to the Mapper.
+
+        Usage:
+
+        .. code-block:: python
+
+            m = Mapper()
+            m.connect(':controller/:action/:id')
+            m.connect('date/:year/:month/:day', controller="blog",
+                      action="view")
+            m.connect('archives/:page', controller="blog", action="by_page",
+            requirements = { 'page':'\\d{1,2}' })
+            m.connect('category_list', 'archives/category/:section',
+                      controller='blog', action='category',
+                      section='home', type='list')
+            m.connect('home', '', controller='blog', action='view',
+                      section='home')
+
+        """
+        routename = None
+        if len(args) > 1:
+            routename = args[0]
+        else:
+            args = (None,) + args
+        if '_explicit' not in kargs:
+            kargs['_explicit'] = self.explicit
+        if '_minimize' not in kargs:
+            kargs['_minimize'] = self.minimization
+        route = self.make_route(*args, **kargs)
+
+        # Apply encoding and errors if its not the defaults and the route
+        # didn't have one passed in.
+        if (self.encoding != 'utf-8' or self.decode_errors != 'ignore') and \
+           '_encoding' not in kargs:
+            route.encoding = self.encoding
+            route.decode_errors = self.decode_errors
+
+        if not route.static:
+            self.matchlist.append(route)
+
+        if routename:
+            self._routenames[routename] = route
+            route.name = routename
+        if route.static:
+            return
+        exists = False
+        for key in self.maxkeys:
+            if key == route.maxkeys:
+                self.maxkeys[key].append(route)
+                exists = True
+                break
+        if not exists:
+            self.maxkeys[route.maxkeys] = [route]
+        self._created_gens = False
+
+    def _create_gens(self):
+        """Create the generation hashes for route lookups"""
+        # Use keys temporailly to assemble the list to avoid excessive
+        # list iteration testing with "in"
+        controllerlist = {}
+        actionlist = {}
+
+        # Assemble all the hardcoded/defaulted actions/controllers used
+        for route in self.matchlist:
+            if route.static:
+                continue
+            if 'controller' in route.defaults:
+                controllerlist[route.defaults['controller']] = True
+            if 'action' in route.defaults:
+                actionlist[route.defaults['action']] = True
+
+        # Setup the lists of all controllers/actions we'll add each route
+        # to. We include the '*' in the case that a generate contains a
+        # controller/action that has no hardcodes
+        controllerlist = list(controllerlist.keys()) + ['*']
+        actionlist = list(actionlist.keys()) + ['*']
+
+        # Go through our list again, assemble the controllers/actions we'll
+        # add each route to. If its hardcoded, we only add it to that dict key.
+        # Otherwise we add it to every hardcode since it can be changed.
+        gendict = {}  # Our generated two-deep hash
+        for route in self.matchlist:
+            if route.static:
+                continue
+            clist = controllerlist
+            alist = actionlist
+            if 'controller' in route.hardcoded:
+                clist = [route.defaults['controller']]
+            if 'action' in route.hardcoded:
+                alist = [six.text_type(route.defaults['action'])]
+            for controller in clist:
+                for action in alist:
+                    actiondict = gendict.setdefault(controller, {})
+                    actiondict.setdefault(action, ([], {}))[0].append(route)
+        self._gendict = gendict
+        self._created_gens = True
+
+    def create_regs(self, *args, **kwargs):
+        """Atomically creates regular expressions for all connected
+        routes
+        """
+        self.create_regs_lock.acquire()
+        try:
+            self._create_regs(*args, **kwargs)
+        finally:
+            self.create_regs_lock.release()
+
+    def _create_regs(self, clist=None):
+        """Creates regular expressions for all connected routes"""
+        if clist is None:
+            if self.directory:
+                clist = self.controller_scan(self.directory)
+            elif callable(self.controller_scan):
+                clist = self.controller_scan()
+            elif not self.controller_scan:
+                clist = []
+            else:
+                clist = self.controller_scan
+
+        for key, val in six.iteritems(self.maxkeys):
+            for route in val:
+                route.makeregexp(clist)
+
+        regexps = []
+        prefix2routes = collections.defaultdict(list)
+        for route in self.matchlist:
+            if not route.static:
+                regexps.append(route.makeregexp(clist, include_names=False))
+                # Group the routes by static prefix
+                prefix = ''.join(it.takewhile(lambda p: isinstance(p, str),
+                                              route.routelist))
+                if route.minimization and not prefix.startswith('/'):
+                    prefix = '/' + prefix
+                prefix2routes[prefix.rstrip("/")].append(route)
+        self._prefix2routes = prefix2routes
+        # Keep track of all possible prefix lengths in decreasing order
+        self._prefix_lens = sorted(set(len(p) for p in prefix2routes),
+                                   reverse=True)
+
+        # Create our regexp to strip the prefix
+        if self.prefix:
+            self._regprefix = re.compile(self.prefix + '(.*)')
+
+        # Save the master regexp
+        regexp = '|'.join(['(?:%s)' % x for x in regexps])
+        self._master_reg = regexp
+        try:
+            self._master_regexp = re.compile(regexp)
+        except OverflowError:
+            self._master_regexp = None
+        self._created_regs = True
+
+    def _match(self, url, environ):
+        """Internal Route matcher
+
+        Matches a URL against a route, and returns a tuple of the match
+        dict and the route object if a match is successfull, otherwise
+        it returns empty.
+
+        For internal use only.
+
+        """
+        if not self._created_regs and self.controller_scan:
+            self.create_regs()
+        elif not self._created_regs:
+            raise RoutesException("You must generate the regular expressions"
+                                  " before matching.")
+
+        if self.always_scan:
+            self.create_regs()
+
+        matchlog = []
+        if self.prefix:
+            if re.match(self._regprefix, url):
+                url = re.sub(self._regprefix, r'\1', url)
+                if not url:
+                    url = '/'
+            else:
+                return (None, None, matchlog)
+
+        environ = environ or self.environ
+        sub_domains = self.sub_domains
+        sub_domains_ignore = self.sub_domains_ignore
+        domain_match = self.domain_match
+        debug = self.debug
+
+        if self._master_regexp is not None:
+            # Check to see if its a valid url against the main regexp
+            # Done for faster invalid URL elimination
+            valid_url = re.match(self._master_regexp, url)
+        else:
+            # Regex is None due to OverflowError caused by too many routes.
+            # This will allow larger projects to work but might increase time
+            # spent invalidating URLs in the loop below.
+            valid_url = True
+        if not valid_url:
+            return (None, None, matchlog)
+
+        matchlist = it.chain.from_iterable(self._prefix2routes.get(url[:prefix_len], ())
+                                           for prefix_len in self._prefix_lens)
+        for route in matchlist:
+            if route.static:
+                if debug:
+                    matchlog.append(dict(route=route, static=True))
+                continue
+            match = route.match(url, environ, sub_domains, sub_domains_ignore,
+                                domain_match)
+            if debug:
+                matchlog.append(dict(route=route, regexp=bool(match)))
+            if isinstance(match, dict) or match:
+                return (match, route, matchlog)
+        return (None, None, matchlog)
+
+    def match(self, url=None, environ=None):
+        """Match a URL against against one of the routes contained.
+
+        Will return None if no valid match is found.
+
+        .. code-block:: python
+
+            resultdict = m.match('/joe/sixpack')
+
+        """
+        if url is None and not environ:
+            raise RoutesException('URL or environ must be provided')
+
+        if url is None:
+            url = environ['PATH_INFO']
+
+        result = self._match(url, environ)
+        if self.debug:
+            return result[0], result[1], result[2]
+        if isinstance(result[0], dict) or result[0]:
+            return result[0]
+        return None
+
+    def routematch(self, url=None, environ=None):
+        """Match a URL against against one of the routes contained.
+
+        Will return None if no valid match is found, otherwise a
+        result dict and a route object is returned.
+
+        .. code-block:: python
+
+            resultdict, route_obj = m.match('/joe/sixpack')
+
+        """
+        if url is None and not environ:
+            raise RoutesException('URL or environ must be provided')
+
+        if url is None:
+            url = environ['PATH_INFO']
+        result = self._match(url, environ)
+        if self.debug:
+            return result[0], result[1], result[2]
+        if isinstance(result[0], dict) or result[0]:
+            return result[0], result[1]
+        return None
+
+    def generate(self, *args, **kargs):
+        """Generate a route from a set of keywords
+
+        Returns the url text, or None if no URL could be generated.
+
+        .. code-block:: python
+
+            m.generate(controller='content',action='view',id=10)
+
+        """
+        # Generate ourself if we haven't already
+        if not self._created_gens:
+            self._create_gens()
+
+        if self.append_slash:
+            kargs['_append_slash'] = True
+
+        if not self.explicit:
+            if 'controller' not in kargs:
+                kargs['controller'] = 'content'
+            if 'action' not in kargs:
+                kargs['action'] = 'index'
+
+        environ = kargs.pop('_environ', self.environ) or {}
+        if 'SCRIPT_NAME' in environ:
+            script_name = environ['SCRIPT_NAME']
+        elif self.environ and 'SCRIPT_NAME' in self.environ:
+            script_name = self.environ['SCRIPT_NAME']
+        else:
+            script_name = ""
+        controller = kargs.get('controller', None)
+        action = kargs.get('action', None)
+
+        # If the URL didn't depend on the SCRIPT_NAME, we'll cache it
+        # keyed by just by kargs; otherwise we need to cache it with
+        # both SCRIPT_NAME and kargs:
+        cache_key = six.text_type(args).encode('utf8') + \
+            six.text_type(kargs).encode('utf8')
+
+        if self.urlcache is not None:
+            if six.PY3:
+                cache_key_script_name = b':'.join((script_name.encode('utf-8'),
+                                                   cache_key))
+            else:
+                cache_key_script_name = '%s:%s' % (script_name, cache_key)
+
+            # Check the url cache to see if it exists, use it if it does
+            val = self.urlcache.get(cache_key_script_name, self)
+            if val != self:
+                return val
+
+        controller = as_unicode(controller, self.encoding)
+        action = as_unicode(action, self.encoding)
+
+        actionlist = self._gendict.get(controller) or self._gendict.get('*', {})
+        if not actionlist and not args:
+            return None
+        (keylist, sortcache) = actionlist.get(action) or \
+            actionlist.get('*', (None, {}))
+        if not keylist and not args:
+            return None
+
+        keys = frozenset(kargs.keys())
+        cacheset = False
+        cachekey = six.text_type(keys)
+        cachelist = sortcache.get(cachekey)
+        if args:
+            keylist = args
+        elif cachelist:
+            keylist = cachelist
+        else:
+            cacheset = True
+            newlist = []
+            for route in keylist:
+                if len(route.minkeys - route.dotkeys - keys) == 0:
+                    newlist.append(route)
+            keylist = newlist
+
+            class KeySorter:
+
+                def __init__(self, obj, *args):
+                    self.obj = obj
+
+                def __lt__(self, other):
+                    return self._keysort(self.obj, other.obj) < 0
+
+                def _keysort(self, a, b):
+                    """Sorts two sets of sets, to order them ideally for
+                    matching."""
+                    a = a.maxkeys
+                    b = b.maxkeys
+
+                    lendiffa = len(keys ^ a)
+                    lendiffb = len(keys ^ b)
+                    # If they both match, don't switch them
+                    if lendiffa == 0 and lendiffb == 0:
+                        return 0
+
+                    # First, if a matches exactly, use it
+                    if lendiffa == 0:
+                        return -1
+
+                    # Or b matches exactly, use it
+                    if lendiffb == 0:
+                        return 1
+
+                    # Neither matches exactly, return the one with the most in
+                    # common
+                    if self._compare(lendiffa, lendiffb) != 0:
+                        return self._compare(lendiffa, lendiffb)
+
+                    # Neither matches exactly, but if they both have just as
+                    # much in common
+                    if len(keys & b) == len(keys & a):
+                        # Then we return the shortest of the two
+                        return self._compare(len(a), len(b))
+
+                    # Otherwise, we return the one that has the most in common
+                    else:
+                        return self._compare(len(keys & b), len(keys & a))
+
+                def _compare(self, obj1, obj2):
+                    if obj1 < obj2:
+                        return -1
+                    elif obj1 < obj2:
+                        return 1
+                    else:
+                        return 0
+
+            keylist.sort(key=KeySorter)
+            if cacheset:
+                sortcache[cachekey] = keylist
+
+        # Iterate through the keylist of sorted routes (or a single route if
+        # it was passed in explicitly for hardcoded named routes)
+        for route in keylist:
+            fail = False
+            for key in route.hardcoded:
+                kval = kargs.get(key)
+                if not kval:
+                    continue
+                kval = as_unicode(kval, self.encoding)
+                if kval != route.defaults[key] and \
+                        not callable(route.defaults[key]):
+                    fail = True
+                    break
+            if fail:
+                continue
+            path = route.generate(**kargs)
+            if path:
+                if self.prefix:
+                    path = self.prefix + path
+                external_static = route.static and route.external
+                if not route.absolute and not external_static:
+                    path = script_name + path
+                    key = cache_key_script_name
+                else:
+                    key = cache_key
+                if self.urlcache is not None:
+                    self.urlcache.put(key, str(path))
+                return str(path)
+            else:
+                continue
+        return None
+
+    def resource(self, member_name, collection_name, **kwargs):
+        """Generate routes for a controller resource
+
+        The member_name name should be the appropriate singular version
+        of the resource given your locale and used with members of the
+        collection. The collection_name name will be used to refer to
+        the resource collection methods and should be a plural version
+        of the member_name argument. By default, the member_name name
+        will also be assumed to map to a controller you create.
+
+        The concept of a web resource maps somewhat directly to 'CRUD'
+        operations. The overlying things to keep in mind is that
+        mapping a resource is about handling creating, viewing, and
+        editing that resource.
+
+        All keyword arguments are optional.
+
+        ``controller``
+            If specified in the keyword args, the controller will be
+            the actual controller used, but the rest of the naming
+            conventions used for the route names and URL paths are
+            unchanged.
+
+        ``collection``
+            Additional action mappings used to manipulate/view the
+            entire set of resources provided by the controller.
+
+            Example::
+
+                map.resource('message', 'messages', collection={'rss':'GET'})
+                # GET /message/rss (maps to the rss action)
+                # also adds named route "rss_message"
+
+        ``member``
+            Additional action mappings used to access an individual
+            'member' of this controllers resources.
+
+            Example::
+
+                map.resource('message', 'messages', member={'mark':'POST'})
+                # POST /message/1/mark (maps to the mark action)
+                # also adds named route "mark_message"
+
+        ``new``
+            Action mappings that involve dealing with a new member in
+            the controller resources.
+
+            Example::
+
+                map.resource('message', 'messages', new={'preview':'POST'})
+                # POST /message/new/preview (maps to the preview action)
+                # also adds a url named "preview_new_message"
+
+        ``path_prefix``
+            Prepends the URL path for the Route with the path_prefix
+            given. This is most useful for cases where you want to mix
+            resources or relations between resources.
+
+        ``name_prefix``
+            Perpends the route names that are generated with the
+            name_prefix given. Combined with the path_prefix option,
+            it's easy to generate route names and paths that represent
+            resources that are in relations.
+
+            Example::
+
+                map.resource('message', 'messages', controller='categories',
+                    path_prefix='/category/:category_id',
+                    name_prefix="category_")
+                # GET /category/7/message/1
+                # has named route "category_message"
+
+        ``requirements``
+
+           A dictionary that restricts the matching of a
+           variable. Can be used when matching variables with path_prefix.
+
+           Example::
+
+                map.resource('message', 'messages',
+                     path_prefix='{project_id}/',
+                     requirements={"project_id": R"\\d+"})
+                # POST /01234/message
+                #    success, project_id is set to "01234"
+                # POST /foo/message
+                #    404 not found, won't be matched by this route
+
+
+        ``parent_resource``
+            A ``dict`` containing information about the parent
+            resource, for creating a nested resource. It should contain
+            the ``member_name`` and ``collection_name`` of the parent
+            resource. This ``dict`` will
+            be available via the associated ``Route`` object which can
+            be accessed during a request via
+            ``request.environ['routes.route']``
+
+            If ``parent_resource`` is supplied and ``path_prefix``
+            isn't, ``path_prefix`` will be generated from
+            ``parent_resource`` as
+            "<parent collection name>/:<parent member name>_id".
+
+            If ``parent_resource`` is supplied and ``name_prefix``
+            isn't, ``name_prefix`` will be generated from
+            ``parent_resource`` as  "<parent member name>_".
+
+            Example::
+
+                >>> from routes.util import url_for
+                >>> m = Mapper()
+                >>> m.resource('location', 'locations',
+                ...            parent_resource=dict(member_name='region',
+                ...                                 collection_name='regions'))
+                >>> # path_prefix is "regions/:region_id"
+                >>> # name prefix is "region_"
+                >>> url_for('region_locations', region_id=13)
+                '/regions/13/locations'
+                >>> url_for('region_new_location', region_id=13)
+                '/regions/13/locations/new'
+                >>> url_for('region_location', region_id=13, id=60)
+                '/regions/13/locations/60'
+                >>> url_for('region_edit_location', region_id=13, id=60)
+                '/regions/13/locations/60/edit'
+
+            Overriding generated ``path_prefix``::
+
+                >>> m = Mapper()
+                >>> m.resource('location', 'locations',
+                ...            parent_resource=dict(member_name='region',
+                ...                                 collection_name='regions'),
+                ...            path_prefix='areas/:area_id')
+                >>> # name prefix is "region_"
+                >>> url_for('region_locations', area_id=51)
+                '/areas/51/locations'
+
+            Overriding generated ``name_prefix``::
+
+                >>> m = Mapper()
+                >>> m.resource('location', 'locations',
+                ...            parent_resource=dict(member_name='region',
+                ...                                 collection_name='regions'),
+                ...            name_prefix='')
+                >>> # path_prefix is "regions/:region_id"
+                >>> url_for('locations', region_id=51)
+                '/regions/51/locations'
+
+        """
+        collection = kwargs.pop('collection', {})
+        member = kwargs.pop('member', {})
+        new = kwargs.pop('new', {})
+        path_prefix = kwargs.pop('path_prefix', None)
+        name_prefix = kwargs.pop('name_prefix', None)
+        parent_resource = kwargs.pop('parent_resource', None)
+
+        # Generate ``path_prefix`` if ``path_prefix`` wasn't specified and
+        # ``parent_resource`` was. Likewise for ``name_prefix``. Make sure
+        # that ``path_prefix`` and ``name_prefix`` *always* take precedence if
+        # they are specified--in particular, we need to be careful when they
+        # are explicitly set to "".
+        if parent_resource is not None:
+            if path_prefix is None:
+                path_prefix = '%s/:%s_id' % (parent_resource['collection_name'],
+                                             parent_resource['member_name'])
+            if name_prefix is None:
+                name_prefix = '%s_' % parent_resource['member_name']
+        else:
+            if path_prefix is None:
+                path_prefix = ''
+            if name_prefix is None:
+                name_prefix = ''
+
+        # Ensure the edit and new actions are in and GET
+        member['edit'] = 'GET'
+        new.update({'new': 'GET'})
+
+        # Make new dict's based off the old, except the old values become keys,
+        # and the old keys become items in a list as the value
+        def swap(dct, newdct):
+            """Swap the keys and values in the dict, and uppercase the values
+            from the dict during the swap."""
+            for key, val in six.iteritems(dct):
+                newdct.setdefault(val.upper(), []).append(key)
+            return newdct
+        collection_methods = swap(collection, {})
+        member_methods = swap(member, {})
+        new_methods = swap(new, {})
+
+        # Insert create, update, and destroy methods
+        collection_methods.setdefault('POST', []).insert(0, 'create')
+        member_methods.setdefault('PUT', []).insert(0, 'update')
+        member_methods.setdefault('DELETE', []).insert(0, 'delete')
+
+        # If there's a path prefix option, use it with the controller
+        controller = strip_slashes(collection_name)
+        path_prefix = strip_slashes(path_prefix)
+        path_prefix = '/' + path_prefix
+        if path_prefix and path_prefix != '/':
+            path = path_prefix + '/' + controller
+        else:
+            path = '/' + controller
+        collection_path = path
+        new_path = path + "/new"
+        member_path = path + "/:(id)"
+
+        options = {
+            'controller': kwargs.get('controller', controller),
+            '_member_name': member_name,
+            '_collection_name': collection_name,
+            '_parent_resource': parent_resource,
+            '_filter': kwargs.get('_filter')
+        }
+        if 'requirements' in kwargs:
+            options['requirements'] = kwargs['requirements']
+
+        def requirements_for(meth):
+            """Returns a new dict to be used for all route creation as the
+            route options"""
+            opts = options.copy()
+            if method != 'any':
+                opts['conditions'] = {'method': [meth.upper()]}
+            return opts
+
+        # Add the routes for handling collection methods
+        for method, lst in six.iteritems(collection_methods):
+            primary = (method != 'GET' and lst.pop(0)) or None
+            route_options = requirements_for(method)
+            for action in lst:
+                route_options['action'] = action
+                route_name = "%s%s_%s" % (name_prefix, action, collection_name)
+                self.connect("formatted_" + route_name, "%s/%s.:(format)" %
+                             (collection_path, action), **route_options)
+                self.connect(route_name, "%s/%s" % (collection_path, action),
+                             **route_options)
+            if primary:
+                route_options['action'] = primary
+                self.connect("%s.:(format)" % collection_path, **route_options)
+                self.connect(collection_path, **route_options)
+
+        # Specifically add in the built-in 'index' collection method and its
+        # formatted version
+        self.connect("formatted_" + name_prefix + collection_name,
+                     collection_path + ".:(format)", action='index',
+                     conditions={'method': ['GET']}, **options)
+        self.connect(name_prefix + collection_name, collection_path,
+                     action='index', conditions={'method': ['GET']}, **options)
+
+        # Add the routes that deal with new resource methods
+        for method, lst in six.iteritems(new_methods):
+            route_options = requirements_for(method)
+            for action in lst:
+                name = "new_" + member_name
+                route_options['action'] = action
+                if action == 'new':
+                    path = new_path
+                    formatted_path = new_path + '.:(format)'
+                else:
+                    path = "%s/%s" % (new_path, action)
+                    name = action + "_" + name
+                    formatted_path = "%s/%s.:(format)" % (new_path, action)
+                self.connect("formatted_" + name_prefix + name, formatted_path,
+                             **route_options)
+                self.connect(name_prefix + name, path, **route_options)
+
+        requirements_regexp = '[^\\/]+(?<!\\\\)'
+
+        # Add the routes that deal with member methods of a resource
+        for method, lst in six.iteritems(member_methods):
+            route_options = requirements_for(method)
+            route_options['requirements'] = {'id': requirements_regexp}
+            if method not in ['POST', 'GET', 'any']:
+                primary = lst.pop(0)
+            else:
+                primary = None
+            for action in lst:
+                route_options['action'] = action
+                self.connect("formatted_%s%s_%s" % (name_prefix, action,
+                                                    member_name),
+                             "%s/%s.:(format)" % (member_path, action),
+                             **route_options)
+                self.connect("%s%s_%s" % (name_prefix, action, member_name),
+                             "%s/%s" % (member_path, action), **route_options)
+            if primary:
+                route_options['action'] = primary
+                self.connect("%s.:(format)" % member_path, **route_options)
+                self.connect(member_path, **route_options)
+
+        # Specifically add the member 'show' method
+        route_options = requirements_for('GET')
+        route_options['action'] = 'show'
+        route_options['requirements'] = {'id': requirements_regexp}
+        self.connect("formatted_" + name_prefix + member_name,
+                     member_path + ".:(format)", **route_options)
+        self.connect(name_prefix + member_name, member_path, **route_options)
+
+    def redirect(self, match_path, destination_path, *args, **kwargs):
+        """Add a redirect route to the mapper
+
+        Redirect routes bypass the wrapped WSGI application and instead
+        result in a redirect being issued by the RoutesMiddleware. As
+        such, this method is only meaningful when using
+        RoutesMiddleware.
+
+        By default, a 302 Found status code is used, this can be
+        changed by providing a ``_redirect_code`` keyword argument
+        which will then be used instead. Note that the entire status
+        code string needs to be present.
+
+        When using keyword arguments, all arguments that apply to
+        matching will be used for the match, while generation specific
+        options will be used during generation. Thus all options
+        normally available to connected Routes may be used with
+        redirect routes as well.
+
+        Example::
+
+            map = Mapper()
+            map.redirect('/legacyapp/archives/{url:.*}', '/archives/{url}')
+            map.redirect('/home/index', '/',
+                         _redirect_code='301 Moved Permanently')
+
+        """
+        both_args = ['_encoding', '_explicit', '_minimize']
+        gen_args = ['_filter']
+
+        status_code = kwargs.pop('_redirect_code', '302 Found')
+        gen_dict, match_dict = {}, {}
+
+        # Create the dict of args for the generation route
+        for key in both_args + gen_args:
+            if key in kwargs:
+                gen_dict[key] = kwargs[key]
+        gen_dict['_static'] = True
+
+        # Create the dict of args for the matching route
+        for key in kwargs:
+            if key not in gen_args:
+                match_dict[key] = kwargs[key]
+
+        self.connect(match_path, **match_dict)
+        match_route = self.matchlist[-1]
+
+        self.connect('_redirect_%s' % id(match_route), destination_path,
+                     **gen_dict)
+        match_route.redirect = True
+        match_route.redirect_status = status_code