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

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
line wrap: on
line source

import re
import sys

import six
from six.moves.urllib import parse as urlparse

from routes.util import _url_quote as url_quote, _str_encode, as_unicode

class Route(object):
    """The Route object holds a route recognition and generation

    See Route.__init__ docs for usage.

    # reserved keys that don't count
    reserved_keys = ['requirements']

    # special chars to indicate a natural split in the URL
    done_chars = ('/', ',', ';', '.', '#')

    def __init__(self, name, routepath, **kargs):
        """Initialize a route, with a given routepath for

        The set of keyword args will be used as defaults.


            >>> from routes.base import Route
            >>> newroute = Route(None, ':controller/:action/:id')
            >>> sorted(newroute.defaults.items())
            [('action', 'index'), ('id', None)]
            >>> newroute = Route(None, 'date/:year/:month/:day',
            ...     controller="blog", action="view")
            >>> newroute = Route(None, 'archives/:page', controller="blog",
            ...     action="by_page", requirements = { 'page':'\\d{1,2}' })
            >>> newroute.reqs
            {'page': '\\\\d{1,2}'}

        .. Note::
            Route is generally not called directly, a Mapper instance
            connect method should be used to add routes.

        self.routepath = routepath
        self.sub_domains = False
        self.prior = None
        self.redirect = False = name
        self._kargs = kargs
        self.minimization = kargs.pop('_minimize', False)
        self.encoding = kargs.pop('_encoding', 'utf-8')
        self.reqs = kargs.get('requirements', {})
        self.decode_errors = 'replace'

        # Don't bother forming stuff we don't need if its a static route
        self.static = kargs.pop('_static', False)
        self.filter = kargs.pop('_filter', None)
        self.absolute = kargs.pop('_absolute', False)

        # Pull out the member/collection name if present, this applies only to
        # map.resource
        self.member_name = kargs.pop('_member_name', None)
        self.collection_name = kargs.pop('_collection_name', None)
        self.parent_resource = kargs.pop('_parent_resource', None)

        # Pull out route conditions
        self.conditions = kargs.pop('conditions', None)

        # Determine if explicit behavior should be used
        self.explicit = kargs.pop('_explicit', False)

        # Since static need to be generated exactly, treat them as
        # non-minimized
        if self.static:
            self.external = '://' in self.routepath
            self.minimization = False

        # Strip preceding '/' if present, and not minimizing
        if routepath.startswith('/') and self.minimization:
            self.routepath = routepath[1:]

    def _setup_route(self):
        # Build our routelist, and the keys used in the route
        self.routelist = routelist = self._pathkeys(self.routepath)
        routekeys = frozenset(key['name'] for key in routelist
                              if isinstance(key, dict))
        self.dotkeys = frozenset(key['name'] for key in routelist
                                 if isinstance(key, dict) and
                                 key['type'] == '.')

        if not self.minimization:

        # Build a req list with all the regexp requirements for our args
        self.req_regs = {}
        for key, val in six.iteritems(self.reqs):
            self.req_regs[key] = re.compile('^' + val + '$')
        # Update our defaults and set new default keys if needed. defaults
        # needs to be saved
        (self.defaults, defaultkeys) = self._defaults(routekeys,
        # Save the maximum keys we could utilize
        self.maxkeys = defaultkeys | routekeys

        # Populate our minimum keys, and save a copy of our backward keys for
        # quicker generation later
        (self.minkeys, self.routebackwards) = self._minkeys(routelist[:])

        # Populate our hardcoded keys, these are ones that are set and don't
        # exist in the route
        self.hardcoded = frozenset(key for key in self.maxkeys
                                   if key not in routekeys
                                      and self.defaults[key] is not None)

        # Cache our default keys
        self._default_keys = frozenset(self.defaults.keys())

    def make_full_route(self):
        """Make a full routelist string for use with non-minimized
        regpath = ''
        for part in self.routelist:
            if isinstance(part, dict):
                regpath += '%(' + part['name'] + ')s'
                regpath += part
        self.regpath = regpath

    def make_unicode(self, s):
        """Transform the given argument into a unicode string."""
        if isinstance(s, six.text_type):
            return s
        elif isinstance(s, bytes):
            return s.decode(self.encoding)
        elif callable(s):
            return s
            return six.text_type(s)

    def _pathkeys(self, routepath):
        """Utility function to walk the route, and pull out the valid
        dynamic/wildcard keys."""
        collecting = False
        escaping = False
        current = ''
        done_on = ''
        var_type = ''
        just_started = False
        routelist = []
        for char in routepath:
            if escaping:
                if char in ['\\', ':', '*', '{', '}']:
                    current += char
                    current += '\\' + char
                escaping = False
            elif char == '\\':
                escaping = True
            elif char in [':', '*', '{'] and not collecting and not self.static \
               or char in ['{'] and not collecting:
                just_started = True
                collecting = True
                var_type = char
                if char == '{':
                    done_on = '}'
                    just_started = False
                if len(current) > 0:
                    current = ''
            elif collecting and just_started:
                just_started = False
                if char == '(':
                    done_on = ')'
                    current = char
                    done_on = self.done_chars + ('-',)
            elif collecting and char not in done_on:
                current += char
            elif collecting:
                collecting = False
                if var_type == '{':
                    if current[0] == '.':
                        var_type = '.'
                        current = current[1:]
                        var_type = ':'
                    opts = current.split(':')
                    if len(opts) > 1:
                        current = opts[0]
                        self.reqs[current] = opts[1]
                routelist.append(dict(type=var_type, name=current))
                if char in self.done_chars:
                done_on = var_type = current = ''
                current += char
        if collecting:
            routelist.append(dict(type=var_type, name=current))
        elif current:
        return routelist

    def _minkeys(self, routelist):
        """Utility function to walk the route backwards

        Will also determine the minimum keys we can handle to generate
        a working route.

        routelist is a list of the '/' split route path
        defaults is a dict of all the defaults provided for the route

        minkeys = []
        backcheck = routelist[:]

        # If we don't honor minimization, we need all the keys in the
        # route path
        if not self.minimization:
            for part in backcheck:
                if isinstance(part, dict):
            return (frozenset(minkeys), backcheck)

        gaps = False
        for part in backcheck:
            if not isinstance(part, dict) and part not in self.done_chars:
                gaps = True
            elif not isinstance(part, dict):
            key = part['name']
            if key in self.defaults and not gaps:
            gaps = True
        return (frozenset(minkeys), backcheck)

    def _defaults(self, routekeys, reserved_keys, kargs):
        """Creates default set with values stringified

        Put together our list of defaults, stringify non-None values
        and add in our action/id default if they use it and didn't
        specify it.

        defaultkeys is a list of the currently assumed default keys
        routekeys is a list of the keys found in the route path
        reserved_keys is a list of keys that are not

        defaults = {}
        # Add in a controller/action default if they don't exist
        if 'controller' not in routekeys and 'controller' not in kargs \
           and not self.explicit:
            kargs['controller'] = 'content'
        if 'action' not in routekeys and 'action' not in kargs \
           and not self.explicit:
            kargs['action'] = 'index'
        defaultkeys = frozenset(key for key in kargs.keys()
                                if key not in reserved_keys)
        for key in defaultkeys:
            if kargs[key] is not None:
                defaults[key] = self.make_unicode(kargs[key])
                defaults[key] = None
        if 'action' in routekeys and 'action' not in defaults \
           and not self.explicit:
            defaults['action'] = 'index'
        if 'id' in routekeys and 'id' not in defaults \
           and not self.explicit:
            defaults['id'] = None
        newdefaultkeys = frozenset(key for key in defaults.keys()
                                   if key not in reserved_keys)

        return (defaults, newdefaultkeys)

    def makeregexp(self, clist, include_names=True):
        """Create a regular expression for matching purposes

        Note: This MUST be called before match can function properly.

        clist should be a list of valid controller strings that can be
        matched, for this reason makeregexp should be called by the web
        framework after it knows all available controllers that can be

        include_names indicates whether this should be a match regexp
        assigned to itself using regexp grouping names, or if names
        should be excluded for use in a single larger regexp to
        determine if any routes match

        if self.minimization:
            reg = self.buildnextreg(self.routelist, clist, include_names)[0]
            if not reg:
                reg = '/'
            reg = reg + '/?' + '$'

            if not reg.startswith('/'):
                reg = '/' + reg
            reg = self.buildfullreg(clist, include_names)

        reg = '^' + reg

        if not include_names:
            return reg

        self.regexp = reg
        self.regmatch = re.compile(reg)

    def buildfullreg(self, clist, include_names=True):
        """Build the regexp by iterating through the routelist and
        replacing dicts with the appropriate regexp match"""
        regparts = []
        for part in self.routelist:
            if isinstance(part, dict):
                var = part['name']
                if var == 'controller':
                    partmatch = '|'.join(map(re.escape, clist))
                elif part['type'] == ':':
                    partmatch = self.reqs.get(var) or '[^/]+?'
                elif part['type'] == '.':
                    partmatch = self.reqs.get(var) or '[^/.]+?'
                    partmatch = self.reqs.get(var) or '.+?'
                if include_names:
                    regpart = '(?P<%s>%s)' % (var, partmatch)
                    regpart = '(?:%s)' % partmatch
                if part['type'] == '.':
                    regparts.append(r'(?:\.%s)??' % regpart)
        regexp = ''.join(regparts) + '$'
        return regexp

    def buildnextreg(self, path, clist, include_names=True):
        """Recursively build our regexp given a path, and a controller

        Returns the regular expression string, and two booleans that
        can be ignored as they're only used internally by buildnextreg.

        if path:
            part = path[0]
            part = ''
        reg = ''

        # noreqs will remember whether the remainder has either a string
        # match, or a non-defaulted regexp match on a key, allblank remembers
        # if the rest could possible be completely empty
        (rest, noreqs, allblank) = ('', True, True)
        if len(path[1:]) > 0:
            self.prior = part
            (rest, noreqs, allblank) = self.buildnextreg(path[1:], clist,
        if isinstance(part, dict) and part['type'] in (':', '.'):
            var = part['name']
            typ = part['type']
            partreg = ''

            # First we plug in the proper part matcher
            if var in self.reqs:
                if include_names:
                    partreg = '(?P<%s>%s)' % (var, self.reqs[var])
                    partreg = '(?:%s)' % self.reqs[var]
                if typ == '.':
                    partreg = r'(?:\.%s)??' % partreg
            elif var == 'controller':
                if include_names:
                    partreg = '(?P<%s>%s)' % (var, '|'.join(map(re.escape,
                    partreg = '(?:%s)' % '|'.join(map(re.escape, clist))
            elif self.prior in ['/', '#']:
                if include_names:
                    partreg = '(?P<' + var + '>[^' + self.prior + ']+?)'
                    partreg = '(?:[^' + self.prior + ']+?)'
                if not rest:
                    if typ == '.':
                        exclude_chars = '/.'
                        exclude_chars = '/'
                    if include_names:
                        partreg = '(?P<%s>[^%s]+?)' % (var, exclude_chars)
                        partreg = '(?:[^%s]+?)' % exclude_chars
                    if typ == '.':
                        partreg = r'(?:\.%s)??' % partreg
                    end = ''.join(self.done_chars)
                    rem = rest
                    if rem[0] == '\\' and len(rem) > 1:
                        rem = rem[1]
                    elif rem.startswith('(\\') and len(rem) > 2:
                        rem = rem[2]
                        rem = end
                    rem = frozenset(rem) | frozenset(['/'])
                    if include_names:
                        partreg = '(?P<%s>[^%s]+?)' % (var, ''.join(rem))
                        partreg = '(?:[^%s]+?)' % ''.join(rem)

            if var in self.reqs:
                noreqs = False
            if var not in self.defaults:
                allblank = False
                noreqs = False

            # Now we determine if its optional, or required. This changes
            # depending on what is in the rest of the match. If noreqs is
            # true, then its possible the entire thing is optional as there's
            # no reqs or string matches.
            if noreqs:
                # The rest is optional, but now we have an optional with a
                # regexp. Wrap to ensure that if we match anything, we match
                # our regexp first. It's still possible we could be completely
                # blank as we have a default
                if var in self.reqs and var in self.defaults:
                    reg = '(?:' + partreg + rest + ')?'

                # Or we have a regexp match with no default, so now being
                # completely blank form here on out isn't possible
                elif var in self.reqs:
                    allblank = False
                    reg = partreg + rest

                # If the character before this is a special char, it has to be
                # followed by this
                elif var in self.defaults and self.prior in (',', ';', '.'):
                    reg = partreg + rest

                # Or we have a default with no regexp, don't touch the allblank
                elif var in self.defaults:
                    reg = partreg + '?' + rest

                # Or we have a key with no default, and no reqs. Not possible
                # to be all blank from here
                    allblank = False
                    reg = partreg + rest
            # In this case, we have something dangling that might need to be
            # matched
                # If they can all be blank, and we have a default here, we know
                # its safe to make everything from here optional. Since
                # something else in the chain does have req's though, we have
                # to make the partreg here required to continue matching
                if allblank and var in self.defaults:
                    reg = '(?:' + partreg + rest + ')?'

                # Same as before, but they can't all be blank, so we have to
                # require it all to ensure our matches line up right
                    reg = partreg + rest
        elif isinstance(part, dict) and part['type'] == '*':
            var = part['name']
            if noreqs:
                if include_names:
                    reg = '(?P<%s>.*)' % var + rest
                    reg = '(?:.*)' + rest
                if var not in self.defaults:
                    allblank = False
                    noreqs = False
                if allblank and var in self.defaults:
                    if include_names:
                        reg = '(?P<%s>.*)' % var + rest
                        reg = '(?:.*)' + rest
                elif var in self.defaults:
                    if include_names:
                        reg = '(?P<%s>.*)' % var + rest
                        reg = '(?:.*)' + rest
                    if include_names:
                        reg = '(?P<%s>.*)' % var + rest
                        reg = '(?:.*)' + rest
                    allblank = False
                    noreqs = False
        elif part and part[-1] in self.done_chars:
            if allblank:
                reg = re.escape(part[:-1]) + '(?:' + re.escape(part[-1]) + rest
                reg += ')?'
                allblank = False
                # Starting in Python 3.7, the / is no longer escaped, however quite a bit of
                # route generation code relies on it being escaped. This forces the escape in
                # Python 3.7+ so that the remainder of the code functions as intended.
                if part == '/':
                    reg = r'\/' + rest
                    reg = re.escape(part) + rest

        # We have a normal string here, this is a req, and it prevents us from
        # being all blank
            noreqs = False
            allblank = False
            reg = re.escape(part) + rest

        return (reg, noreqs, allblank)

    def match(self, url, environ=None, sub_domains=False,
              sub_domains_ignore=None, domain_match=''):
        """Match a url to our regexp.

        While the regexp might match, this operation isn't
        guaranteed as there's other factors that can cause a match to
        fail even though the regexp succeeds (Default that was relied
        on wasn't given, requirement regexp doesn't pass, etc.).

        Therefore the calling function shouldn't assume this will
        return a valid dict, the other possible return is False if a
        match doesn't work out.

        # Static routes don't match, they generate only
        if self.static:
            return False

        match = self.regmatch.match(url)

        if not match:
            return False

        sub_domain = None

        if sub_domains and environ and 'HTTP_HOST' in environ:
            host = environ['HTTP_HOST'].split(':')[0]
            sub_match = re.compile(r'^(.+?)\.%s$' % domain_match)
            subdomain = re.sub(sub_match, r'\1', host)
            if subdomain not in sub_domains_ignore and host != subdomain:
                sub_domain = subdomain

        if self.conditions:
            if 'method' in self.conditions and environ and \
                    environ['REQUEST_METHOD'] not in self.conditions['method']:
                return False

            # Check sub-domains?
            use_sd = self.conditions.get('sub_domain')
            if use_sd and not sub_domain:
                return False
            elif not use_sd and 'sub_domain' in self.conditions and sub_domain:
                return False
            if isinstance(use_sd, list) and sub_domain not in use_sd:
                return False

        matchdict = match.groupdict()
        result = {}
        extras = self._default_keys - frozenset(matchdict.keys())
        for key, val in six.iteritems(matchdict):
            if key != 'path_info' and self.encoding:
                # change back into python unicode objects from the URL
                # representation
                    val = as_unicode(val, self.encoding, self.decode_errors)
                except UnicodeDecodeError:
                    return False

            if not val and key in self.defaults and self.defaults[key]:
                result[key] = self.defaults[key]
                result[key] = val
        for key in extras:
            result[key] = self.defaults[key]

        # Add the sub-domain if there is one
        if sub_domains:
            result['sub_domain'] = sub_domain

        # If there's a function, call it with environ and expire if it
        # returns False
        if self.conditions and 'function' in self.conditions and \
                not self.conditions['function'](environ, result):
            return False

        return result

    def generate_non_minimized(self, kargs):
        """Generate a non-minimal version of the URL"""
        # Iterate through the keys that are defaults, and NOT in the route
        # path. If its not in kargs, or doesn't match, or is None, this
        # route won't work
        for k in self.maxkeys - self.minkeys:
            if k not in kargs:
                return False
            elif self.make_unicode(kargs[k]) != \
                return False

        # Ensure that all the args in the route path are present and not None
        for arg in self.minkeys:
            if arg not in kargs or kargs[arg] is None:
                if arg in self.dotkeys:
                    kargs[arg] = ''
                    return False

        # Encode all the argument that the regpath can use
        for k in kargs:
            if k in self.maxkeys:
                if k in self.dotkeys:
                    if kargs[k]:
                        kargs[k] = url_quote('.' + as_unicode(kargs[k],
                                             self.encoding), self.encoding)
                    kargs[k] = url_quote(as_unicode(kargs[k], self.encoding),

        return self.regpath % kargs

    def generate_minimized(self, kargs):
        """Generate a minimized version of the URL"""
        routelist = self.routebackwards
        urllist = []
        gaps = False
        for part in routelist:
            if isinstance(part, dict) and part['type'] in (':', '.'):
                arg = part['name']

                # For efficiency, check these just once
                has_arg = arg in kargs
                has_default = arg in self.defaults

                # Determine if we can leave this part off
                # First check if the default exists and wasn't provided in the
                # call (also no gaps)
                if has_default and not has_arg and not gaps:

                # Now check to see if there's a default and it matches the
                # incoming call arg
                if (has_default and has_arg) and \
                    self.make_unicode(kargs[arg]) == \
                        self.make_unicode(self.defaults[arg]) and not gaps:

                # We need to pull the value to append, if the arg is None and
                # we have a default, use that
                if has_arg and kargs[arg] is None and has_default and not gaps:

                # Otherwise if we do have an arg, use that
                elif has_arg:
                    val = kargs[arg]

                elif has_default and self.defaults[arg] is not None:
                    val = self.defaults[arg]
                # Optional format parameter?
                elif part['type'] == '.':
                # No arg at all? This won't work
                    return False

                val = as_unicode(val, self.encoding)
                urllist.append(url_quote(val, self.encoding))
                if part['type'] == '.':

                if has_arg:
                    del kargs[arg]
                gaps = True
            elif isinstance(part, dict) and part['type'] == '*':
                arg = part['name']
                kar = kargs.get(arg)
                if kar is not None:
                    urllist.append(url_quote(kar, self.encoding))
                    gaps = True
            elif part and part[-1] in self.done_chars:
                if not gaps and part in self.done_chars:
                elif not gaps:
                    gaps = True
                    gaps = True
                gaps = True
        url = ''.join(urllist)
        return url

    def generate(self, _ignore_req_list=False, _append_slash=False, **kargs):
        """Generate a URL from ourself given a set of keyword arguments

        Toss an exception if this
        set of keywords would cause a gap in the url.

        # Verify that our args pass any regexp requirements
        if not _ignore_req_list:
            for key in self.reqs.keys():
                val = kargs.get(key)
                if val and not self.req_regs[key].match(self.make_unicode(val)):
                    return False

        # Verify that if we have a method arg, its in the method accept list.
        # Also, method will be changed to _method for route generation
        meth = as_unicode(kargs.get('method'), self.encoding)
        if meth:
            if self.conditions and 'method' in self.conditions \
                    and meth.upper() not in self.conditions['method']:
                return False

        if self.minimization:
            url = self.generate_minimized(kargs)
            url = self.generate_non_minimized(kargs)

        if url is False:
            return url

        if not url.startswith('/') and not self.static:
            url = '/' + url
        extras = frozenset(kargs.keys()) - self.maxkeys
        if extras:
            if _append_slash and not url.endswith('/'):
                url += '/'
            fragments = []
            # don't assume the 'extras' set preserves order: iterate
            # through the ordered kargs instead
            for key in kargs:
                if key not in extras:
                if key == 'action' or key == 'controller':
                val = kargs[key]
                if isinstance(val, (tuple, list)):
                    for value in val:
                        value = as_unicode(value, self.encoding)
                        fragments.append((key, _str_encode(value,
                    val = as_unicode(val, self.encoding)
                    fragments.append((key, _str_encode(val, self.encoding)))
            if fragments:
                url += '?'
                url += urlparse.urlencode(fragments)
        elif _append_slash and not url.endswith('/'):
            url += '/'
        return url