Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/urldispatch.py : 62%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2from zope.interface import implementer
4from pyramid.interfaces import IRoutesMapper, IRoute
6from pyramid.compat import (
7 PY2,
8 native_,
9 text_,
10 text_type,
11 string_types,
12 binary_type,
13 is_nonstr_iter,
14 decode_path_info,
15)
17from pyramid.exceptions import URLDecodeError
19from pyramid.traversal import quote_path_segment, split_path_info, PATH_SAFE
21_marker = object()
24@implementer(IRoute)
25class Route(object):
26 def __init__(
27 self, name, pattern, factory=None, predicates=(), pregenerator=None
28 ):
29 self.pattern = pattern
30 self.path = pattern # indefinite b/w compat, not in interface
31 self.match, self.generate = _compile_route(pattern)
32 self.name = name
33 self.factory = factory
34 self.predicates = predicates
35 self.pregenerator = pregenerator
38@implementer(IRoutesMapper)
39class RoutesMapper(object):
40 def __init__(self):
41 self.routelist = []
42 self.static_routes = []
44 self.routes = {}
46 def has_routes(self):
47 return bool(self.routelist)
49 def get_routes(self, include_static=False):
50 if include_static is True:
51 return self.routelist + self.static_routes
53 return self.routelist
55 def get_route(self, name):
56 return self.routes.get(name)
58 def connect(
59 self,
60 name,
61 pattern,
62 factory=None,
63 predicates=(),
64 pregenerator=None,
65 static=False,
66 ):
67 if name in self.routes:
68 oldroute = self.routes[name]
69 if oldroute in self.routelist:
70 self.routelist.remove(oldroute)
72 route = Route(name, pattern, factory, predicates, pregenerator)
73 if not static:
74 self.routelist.append(route)
75 else:
76 self.static_routes.append(route)
78 self.routes[name] = route
79 return route
81 def generate(self, name, kw):
82 return self.routes[name].generate(kw)
84 def __call__(self, request):
85 environ = request.environ
86 try:
87 # empty if mounted under a path in mod_wsgi, for example
88 path = decode_path_info(environ['PATH_INFO'] or '/')
89 except KeyError:
90 path = '/'
91 except UnicodeDecodeError as e:
92 raise URLDecodeError(
93 e.encoding, e.object, e.start, e.end, e.reason
94 )
96 for route in self.routelist:
97 match = route.match(path)
98 if match is not None:
99 preds = route.predicates
100 info = {'match': match, 'route': route}
101 if preds and not all((p(info, request) for p in preds)):
102 continue
103 return info
105 return {'route': None, 'match': None}
108# stolen from bobo and modified
109old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)')
110star_at_end = re.compile(r'\*(\w*)$')
112# The tortuous nature of the regex named ``route_re`` below is due to the
113# fact that we need to support at least one level of "inner" squigglies
114# inside the expr of a {name:expr} pattern. This regex used to be just
115# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}.
116route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})')
119def update_pattern(matchobj):
120 name = matchobj.group(0)
121 return '{%s}' % name[1:]
124def _compile_route(route):
125 # This function really wants to consume Unicode patterns natively, but if
126 # someone passes us a bytestring, we allow it by converting it to Unicode
127 # using the ASCII decoding. We decode it using ASCII because we don't
128 # want to accept bytestrings with high-order characters in them here as
129 # we have no idea what the encoding represents.
130 if route.__class__ is not text_type:
131 try:
132 route = text_(route, 'ascii')
133 except UnicodeDecodeError:
134 raise ValueError(
135 'The pattern value passed to add_route must be '
136 'either a Unicode string or a plain string without '
137 'any non-ASCII characters (you provided %r).' % route
138 )
140 if old_route_re.search(route) and not route_re.search(route):
141 route = old_route_re.sub(update_pattern, route)
143 if not route.startswith('/'):
144 route = '/' + route
146 remainder = None
147 if star_at_end.search(route):
148 route, remainder = route.rsplit('*', 1)
150 pat = route_re.split(route)
152 # every element in "pat" will be Unicode (regardless of whether the
153 # route_re regex pattern is itself Unicode or str)
154 pat.reverse()
155 rpat = []
156 gen = []
157 prefix = pat.pop() # invar: always at least one element (route='/'+route)
159 # We want to generate URL-encoded URLs, so we url-quote the prefix, being
160 # careful not to quote any embedded slashes. We have to replace '%' with
161 # '%%' afterwards, as the strings that go into "gen" are used as string
162 # replacement targets.
163 gen.append(
164 quote_path_segment(prefix, safe='/').replace('%', '%%')
165 ) # native
166 rpat.append(re.escape(prefix)) # unicode
168 while pat:
169 name = pat.pop() # unicode
170 name = name[1:-1]
171 if ':' in name:
172 # reg may contain colons as well,
173 # so we must strictly split name into two parts
174 name, reg = name.split(':', 1)
175 else:
176 reg = '[^/]+'
177 gen.append('%%(%s)s' % native_(name)) # native
178 name = '(?P<%s>%s)' % (name, reg) # unicode
179 rpat.append(name)
180 s = pat.pop() # unicode
181 if s:
182 rpat.append(re.escape(s)) # unicode
183 # We want to generate URL-encoded URLs, so we url-quote this
184 # literal in the pattern, being careful not to quote the embedded
185 # slashes. We have to replace '%' with '%%' afterwards, as the
186 # strings that go into "gen" are used as string replacement
187 # targets. What is appended to gen is a native string.
188 gen.append(quote_path_segment(s, safe='/').replace('%', '%%'))
190 if remainder:
191 rpat.append('(?P<%s>.*?)' % remainder) # unicode
192 gen.append('%%(%s)s' % native_(remainder)) # native
194 pattern = ''.join(rpat) + '$' # unicode
196 match = re.compile(pattern).match
198 def matcher(path):
199 # This function really wants to consume Unicode patterns natively,
200 # but if someone passes us a bytestring, we allow it by converting it
201 # to Unicode using the ASCII decoding. We decode it using ASCII
202 # because we don't want to accept bytestrings with high-order
203 # characters in them here as we have no idea what the encoding
204 # represents.
205 if path.__class__ is not text_type:
206 path = text_(path, 'ascii')
207 m = match(path)
208 if m is None:
209 return None
210 d = {}
211 for k, v in m.groupdict().items():
212 # k and v will be Unicode 2.6.4 and lower doesnt accept unicode
213 # kwargs as **kw, so we explicitly cast the keys to native
214 # strings in case someone wants to pass the result as **kw
215 nk = native_(k, 'ascii')
216 if k == remainder:
217 d[nk] = split_path_info(v)
218 else:
219 d[nk] = v
220 return d
222 gen = ''.join(gen)
224 def q(v):
225 return quote_path_segment(v, safe=PATH_SAFE)
227 def generator(dict):
228 newdict = {}
229 for k, v in dict.items():
230 if PY2:
231 if v.__class__ is text_type:
232 # url_quote below needs bytes, not unicode on Py2
233 v = v.encode('utf-8')
234 else:
235 if v.__class__ is binary_type:
236 # url_quote below needs a native string, not bytes on Py3
237 v = v.decode('utf-8')
239 if k == remainder:
240 # a stararg argument
241 if is_nonstr_iter(v):
242 v = '/'.join([q(x) for x in v]) # native
243 else:
244 if v.__class__ not in string_types:
245 v = str(v)
246 v = q(v)
247 else:
248 if v.__class__ not in string_types:
249 v = str(v)
250 # v may be bytes (py2) or native string (py3)
251 v = q(v)
253 # at this point, the value will be a native string
254 newdict[k] = v
256 result = gen % newdict # native string result
257 return result
259 return matcher, generator