import re
|
from zope.interface import implementer
|
|
from pyramid.interfaces import IRoutesMapper, IRoute
|
|
from pyramid.compat import (
|
PY2,
|
native_,
|
text_,
|
text_type,
|
string_types,
|
binary_type,
|
is_nonstr_iter,
|
decode_path_info,
|
)
|
|
from pyramid.exceptions import URLDecodeError
|
|
from pyramid.traversal import quote_path_segment, split_path_info, PATH_SAFE
|
|
_marker = object()
|
|
|
@implementer(IRoute)
|
class Route(object):
|
def __init__(
|
self, name, pattern, factory=None, predicates=(), pregenerator=None
|
):
|
self.pattern = pattern
|
self.path = pattern # indefinite b/w compat, not in interface
|
self.match, self.generate = _compile_route(pattern)
|
self.name = name
|
self.factory = factory
|
self.predicates = predicates
|
self.pregenerator = pregenerator
|
|
|
@implementer(IRoutesMapper)
|
class RoutesMapper(object):
|
def __init__(self):
|
self.routelist = []
|
self.static_routes = []
|
|
self.routes = {}
|
|
def has_routes(self):
|
return bool(self.routelist)
|
|
def get_routes(self, include_static=False):
|
if include_static is True:
|
return self.routelist + self.static_routes
|
|
return self.routelist
|
|
def get_route(self, name):
|
return self.routes.get(name)
|
|
def connect(
|
self,
|
name,
|
pattern,
|
factory=None,
|
predicates=(),
|
pregenerator=None,
|
static=False,
|
):
|
if name in self.routes:
|
oldroute = self.routes[name]
|
if oldroute in self.routelist:
|
self.routelist.remove(oldroute)
|
|
route = Route(name, pattern, factory, predicates, pregenerator)
|
if not static:
|
self.routelist.append(route)
|
else:
|
self.static_routes.append(route)
|
|
self.routes[name] = route
|
return route
|
|
def generate(self, name, kw):
|
return self.routes[name].generate(kw)
|
|
def __call__(self, request):
|
environ = request.environ
|
try:
|
# empty if mounted under a path in mod_wsgi, for example
|
path = decode_path_info(environ['PATH_INFO'] or '/')
|
except KeyError:
|
path = '/'
|
except UnicodeDecodeError as e:
|
raise URLDecodeError(
|
e.encoding, e.object, e.start, e.end, e.reason
|
)
|
|
for route in self.routelist:
|
match = route.match(path)
|
if match is not None:
|
preds = route.predicates
|
info = {'match': match, 'route': route}
|
if preds and not all((p(info, request) for p in preds)):
|
continue
|
return info
|
|
return {'route': None, 'match': None}
|
|
|
# stolen from bobo and modified
|
old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)')
|
star_at_end = re.compile(r'\*(\w*)$')
|
|
# The tortuous nature of the regex named ``route_re`` below is due to the
|
# fact that we need to support at least one level of "inner" squigglies
|
# inside the expr of a {name:expr} pattern. This regex used to be just
|
# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}.
|
route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
|
|
|
def update_pattern(matchobj):
|
name = matchobj.group(0)
|
return '{%s}' % name[1:]
|
|
|
def _compile_route(route):
|
# This function really wants to consume Unicode patterns natively, but if
|
# someone passes us a bytestring, we allow it by converting it to Unicode
|
# using the ASCII decoding. We decode it using ASCII because we don't
|
# want to accept bytestrings with high-order characters in them here as
|
# we have no idea what the encoding represents.
|
if route.__class__ is not text_type:
|
try:
|
route = text_(route, 'ascii')
|
except UnicodeDecodeError:
|
raise ValueError(
|
'The pattern value passed to add_route must be '
|
'either a Unicode string or a plain string without '
|
'any non-ASCII characters (you provided %r).' % route
|
)
|
|
if old_route_re.search(route) and not route_re.search(route):
|
route = old_route_re.sub(update_pattern, route)
|
|
if not route.startswith('/'):
|
route = '/' + route
|
|
remainder = None
|
if star_at_end.search(route):
|
route, remainder = route.rsplit('*', 1)
|
|
pat = route_re.split(route)
|
|
# every element in "pat" will be Unicode (regardless of whether the
|
# route_re regex pattern is itself Unicode or str)
|
pat.reverse()
|
rpat = []
|
gen = []
|
prefix = pat.pop() # invar: always at least one element (route='/'+route)
|
|
# We want to generate URL-encoded URLs, so we url-quote the prefix, being
|
# careful not to quote any embedded slashes. We have to replace '%' with
|
# '%%' afterwards, as the strings that go into "gen" are used as string
|
# replacement targets.
|
gen.append(
|
quote_path_segment(prefix, safe='/').replace('%', '%%')
|
) # native
|
rpat.append(re.escape(prefix)) # unicode
|
|
while pat:
|
name = pat.pop() # unicode
|
name = name[1:-1]
|
if ':' in name:
|
# reg may contain colons as well,
|
# so we must strictly split name into two parts
|
name, reg = name.split(':', 1)
|
else:
|
reg = '[^/]+'
|
gen.append('%%(%s)s' % native_(name)) # native
|
name = '(?P<%s>%s)' % (name, reg) # unicode
|
rpat.append(name)
|
s = pat.pop() # unicode
|
if s:
|
rpat.append(re.escape(s)) # unicode
|
# We want to generate URL-encoded URLs, so we url-quote this
|
# literal in the pattern, being careful not to quote the embedded
|
# slashes. We have to replace '%' with '%%' afterwards, as the
|
# strings that go into "gen" are used as string replacement
|
# targets. What is appended to gen is a native string.
|
gen.append(quote_path_segment(s, safe='/').replace('%', '%%'))
|
|
if remainder:
|
rpat.append('(?P<%s>.*?)' % remainder) # unicode
|
gen.append('%%(%s)s' % native_(remainder)) # native
|
|
pattern = ''.join(rpat) + '$' # unicode
|
|
match = re.compile(pattern).match
|
|
def matcher(path):
|
# This function really wants to consume Unicode patterns natively,
|
# but if someone passes us a bytestring, we allow it by converting it
|
# to Unicode using the ASCII decoding. We decode it using ASCII
|
# because we don't want to accept bytestrings with high-order
|
# characters in them here as we have no idea what the encoding
|
# represents.
|
if path.__class__ is not text_type:
|
path = text_(path, 'ascii')
|
m = match(path)
|
if m is None:
|
return None
|
d = {}
|
for k, v in m.groupdict().items():
|
# k and v will be Unicode 2.6.4 and lower doesnt accept unicode
|
# kwargs as **kw, so we explicitly cast the keys to native
|
# strings in case someone wants to pass the result as **kw
|
nk = native_(k, 'ascii')
|
if k == remainder:
|
d[nk] = split_path_info(v)
|
else:
|
d[nk] = v
|
return d
|
|
gen = ''.join(gen)
|
|
def q(v):
|
return quote_path_segment(v, safe=PATH_SAFE)
|
|
def generator(dict):
|
newdict = {}
|
for k, v in dict.items():
|
if PY2:
|
if v.__class__ is text_type:
|
# url_quote below needs bytes, not unicode on Py2
|
v = v.encode('utf-8')
|
else:
|
if v.__class__ is binary_type:
|
# url_quote below needs a native string, not bytes on Py3
|
v = v.decode('utf-8')
|
|
if k == remainder:
|
# a stararg argument
|
if is_nonstr_iter(v):
|
v = '/'.join([q(x) for x in v]) # native
|
else:
|
if v.__class__ not in string_types:
|
v = str(v)
|
v = q(v)
|
else:
|
if v.__class__ not in string_types:
|
v = str(v)
|
# v may be bytes (py2) or native string (py3)
|
v = q(v)
|
|
# at this point, the value will be a native string
|
newdict[k] = v
|
|
result = gen % newdict # native string result
|
return result
|
|
return matcher, generator
|