765 lines
29 KiB
Python
765 lines
29 KiB
Python
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
|
|
routine.
|
|
|
|
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
|
|
matching/generation
|
|
|
|
The set of keyword args will be used as defaults.
|
|
|
|
Usage::
|
|
|
|
>>> 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
|
|
self.name = 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:]
|
|
self._setup_route()
|
|
|
|
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:
|
|
self.make_full_route()
|
|
|
|
# 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,
|
|
self.reserved_keys,
|
|
self._kargs.copy())
|
|
# 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
|
|
generation"""
|
|
regpath = ''
|
|
for part in self.routelist:
|
|
if isinstance(part, dict):
|
|
regpath += '%(' + part['name'] + ')s'
|
|
else:
|
|
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
|
|
else:
|
|
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
|
|
else:
|
|
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:
|
|
routelist.append(current)
|
|
current = ''
|
|
elif collecting and just_started:
|
|
just_started = False
|
|
if char == '(':
|
|
done_on = ')'
|
|
else:
|
|
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:]
|
|
else:
|
|
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:
|
|
routelist.append(char)
|
|
done_on = var_type = current = ''
|
|
else:
|
|
current += char
|
|
if collecting:
|
|
routelist.append(dict(type=var_type, name=current))
|
|
elif current:
|
|
routelist.append(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):
|
|
minkeys.append(part['name'])
|
|
return (frozenset(minkeys), backcheck)
|
|
|
|
gaps = False
|
|
backcheck.reverse()
|
|
for part in backcheck:
|
|
if not isinstance(part, dict) and part not in self.done_chars:
|
|
gaps = True
|
|
continue
|
|
elif not isinstance(part, dict):
|
|
continue
|
|
key = part['name']
|
|
if key in self.defaults and not gaps:
|
|
continue
|
|
minkeys.append(key)
|
|
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])
|
|
else:
|
|
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
|
|
utilized.
|
|
|
|
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
|
|
else:
|
|
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 '[^/.]+?'
|
|
else:
|
|
partmatch = self.reqs.get(var) or '.+?'
|
|
if include_names:
|
|
regpart = '(?P<%s>%s)' % (var, partmatch)
|
|
else:
|
|
regpart = '(?:%s)' % partmatch
|
|
if part['type'] == '.':
|
|
regparts.append(r'(?:\.%s)??' % regpart)
|
|
else:
|
|
regparts.append(regpart)
|
|
else:
|
|
regparts.append(re.escape(part))
|
|
regexp = ''.join(regparts) + '$'
|
|
return regexp
|
|
|
|
def buildnextreg(self, path, clist, include_names=True):
|
|
"""Recursively build our regexp given a path, and a controller
|
|
list.
|
|
|
|
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]
|
|
else:
|
|
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,
|
|
include_names)
|
|
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])
|
|
else:
|
|
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,
|
|
clist)))
|
|
else:
|
|
partreg = '(?:%s)' % '|'.join(map(re.escape, clist))
|
|
elif self.prior in ['/', '#']:
|
|
if include_names:
|
|
partreg = '(?P<' + var + '>[^' + self.prior + ']+?)'
|
|
else:
|
|
partreg = '(?:[^' + self.prior + ']+?)'
|
|
else:
|
|
if not rest:
|
|
if typ == '.':
|
|
exclude_chars = '/.'
|
|
else:
|
|
exclude_chars = '/'
|
|
if include_names:
|
|
partreg = '(?P<%s>[^%s]+?)' % (var, exclude_chars)
|
|
else:
|
|
partreg = '(?:[^%s]+?)' % exclude_chars
|
|
if typ == '.':
|
|
partreg = r'(?:\.%s)??' % partreg
|
|
else:
|
|
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]
|
|
else:
|
|
rem = end
|
|
rem = frozenset(rem) | frozenset(['/'])
|
|
if include_names:
|
|
partreg = '(?P<%s>[^%s]+?)' % (var, ''.join(rem))
|
|
else:
|
|
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
|
|
else:
|
|
allblank = False
|
|
reg = partreg + rest
|
|
# In this case, we have something dangling that might need to be
|
|
# matched
|
|
else:
|
|
# 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
|
|
else:
|
|
reg = partreg + rest
|
|
elif isinstance(part, dict) and part['type'] == '*':
|
|
var = part['name']
|
|
if noreqs:
|
|
if include_names:
|
|
reg = '(?P<%s>.*)' % var + rest
|
|
else:
|
|
reg = '(?:.*)' + rest
|
|
if var not in self.defaults:
|
|
allblank = False
|
|
noreqs = False
|
|
else:
|
|
if allblank and var in self.defaults:
|
|
if include_names:
|
|
reg = '(?P<%s>.*)' % var + rest
|
|
else:
|
|
reg = '(?:.*)' + rest
|
|
elif var in self.defaults:
|
|
if include_names:
|
|
reg = '(?P<%s>.*)' % var + rest
|
|
else:
|
|
reg = '(?:.*)' + rest
|
|
else:
|
|
if include_names:
|
|
reg = '(?P<%s>.*)' % var + rest
|
|
else:
|
|
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 += ')?'
|
|
else:
|
|
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
|
|
else:
|
|
reg = re.escape(part) + rest
|
|
|
|
# We have a normal string here, this is a req, and it prevents us from
|
|
# being all blank
|
|
else:
|
|
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
|
|
try:
|
|
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]
|
|
else:
|
|
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]) != \
|
|
self.make_unicode(self.defaults[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] = ''
|
|
else:
|
|
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)
|
|
else:
|
|
kargs[k] = url_quote(as_unicode(kargs[k], self.encoding),
|
|
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:
|
|
continue
|
|
|
|
# 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:
|
|
continue
|
|
|
|
# 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:
|
|
continue
|
|
|
|
# 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'] == '.':
|
|
continue
|
|
# No arg at all? This won't work
|
|
else:
|
|
return False
|
|
|
|
val = as_unicode(val, self.encoding)
|
|
urllist.append(url_quote(val, self.encoding))
|
|
if part['type'] == '.':
|
|
urllist.append('.')
|
|
|
|
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:
|
|
continue
|
|
elif not gaps:
|
|
urllist.append(part[:-1])
|
|
gaps = True
|
|
else:
|
|
gaps = True
|
|
urllist.append(part)
|
|
else:
|
|
gaps = True
|
|
urllist.append(part)
|
|
urllist.reverse()
|
|
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
|
|
kargs.pop('method')
|
|
|
|
if self.minimization:
|
|
url = self.generate_minimized(kargs)
|
|
else:
|
|
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:
|
|
continue
|
|
if key == 'action' or key == 'controller':
|
|
continue
|
|
val = kargs[key]
|
|
if isinstance(val, (tuple, list)):
|
|
for value in val:
|
|
value = as_unicode(value, self.encoding)
|
|
fragments.append((key, _str_encode(value,
|
|
self.encoding)))
|
|
else:
|
|
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
|