commit | author | age
|
05c023
|
1 |
import re |
3b7334
|
2 |
from zope.interface import implementer |
74409d
|
3 |
|
0c29cf
|
4 |
from pyramid.interfaces import IRoutesMapper, IRoute |
8392d7
|
5 |
|
0c1c39
|
6 |
from pyramid.compat import ( |
bc37a5
|
7 |
PY2, |
0c1c39
|
8 |
native_, |
a511b1
|
9 |
text_, |
0c1c39
|
10 |
text_type, |
CM |
11 |
string_types, |
a511b1
|
12 |
binary_type, |
0c1c39
|
13 |
is_nonstr_iter, |
c08f76
|
14 |
decode_path_info, |
0c29cf
|
15 |
) |
0c1c39
|
16 |
|
b60bdb
|
17 |
from pyramid.exceptions import URLDecodeError |
0c1c39
|
18 |
|
0c29cf
|
19 |
from pyramid.traversal import quote_path_segment, split_path_info, PATH_SAFE |
17358d
|
20 |
|
dfc2b6
|
21 |
_marker = object() |
277df8
|
22 |
|
0c29cf
|
23 |
|
3b7334
|
24 |
@implementer(IRoute) |
05c023
|
25 |
class Route(object): |
0c29cf
|
26 |
def __init__( |
MM |
27 |
self, name, pattern, factory=None, predicates=(), pregenerator=None |
|
28 |
): |
74409d
|
29 |
self.pattern = pattern |
0c29cf
|
30 |
self.path = pattern # indefinite b/w compat, not in interface |
4018ad
|
31 |
self.match, self.generate = _compile_route(pattern) |
05c023
|
32 |
self.name = name |
CM |
33 |
self.factory = factory |
65476e
|
34 |
self.predicates = predicates |
70f1cd
|
35 |
self.pregenerator = pregenerator |
0c29cf
|
36 |
|
05c023
|
37 |
|
3b7334
|
38 |
@implementer(IRoutesMapper) |
cbfafb
|
39 |
class RoutesMapper(object): |
CM |
40 |
def __init__(self): |
05c023
|
41 |
self.routelist = [] |
582c2e
|
42 |
self.static_routes = [] |
JA |
43 |
|
05c023
|
44 |
self.routes = {} |
62267e
|
45 |
|
CM |
46 |
def has_routes(self): |
05c023
|
47 |
return bool(self.routelist) |
62267e
|
48 |
|
582c2e
|
49 |
def get_routes(self, include_static=False): |
JA |
50 |
if include_static is True: |
|
51 |
return self.routelist + self.static_routes |
|
52 |
|
750ce4
|
53 |
return self.routelist |
CM |
54 |
|
74409d
|
55 |
def get_route(self, name): |
CM |
56 |
return self.routes.get(name) |
|
57 |
|
0c29cf
|
58 |
def connect( |
MM |
59 |
self, |
|
60 |
name, |
|
61 |
pattern, |
|
62 |
factory=None, |
|
63 |
predicates=(), |
|
64 |
pregenerator=None, |
|
65 |
static=False, |
|
66 |
): |
0bf286
|
67 |
if name in self.routes: |
CM |
68 |
oldroute = self.routes[name] |
e725cf
|
69 |
if oldroute in self.routelist: |
CM |
70 |
self.routelist.remove(oldroute) |
582c2e
|
71 |
|
4018ad
|
72 |
route = Route(name, pattern, factory, predicates, pregenerator) |
e725cf
|
73 |
if not static: |
CM |
74 |
self.routelist.append(route) |
582c2e
|
75 |
else: |
JA |
76 |
self.static_routes.append(route) |
|
77 |
|
05c023
|
78 |
self.routes[name] = route |
CM |
79 |
return route |
|
80 |
|
|
81 |
def generate(self, name, kw): |
|
82 |
return self.routes[name].generate(kw) |
8392d7
|
83 |
|
11644e
|
84 |
def __call__(self, request): |
cbfafb
|
85 |
environ = request.environ |
11644e
|
86 |
try: |
cbfafb
|
87 |
# empty if mounted under a path in mod_wsgi, for example |
6212db
|
88 |
path = decode_path_info(environ['PATH_INFO'] or '/') |
f646fe
|
89 |
except KeyError: |
22e72d
|
90 |
path = '/' |
6212db
|
91 |
except UnicodeDecodeError as e: |
0c29cf
|
92 |
raise URLDecodeError( |
MM |
93 |
e.encoding, e.object, e.start, e.end, e.reason |
|
94 |
) |
22e72d
|
95 |
|
05c023
|
96 |
for route in self.routelist: |
CM |
97 |
match = route.match(path) |
|
98 |
if match is not None: |
65476e
|
99 |
preds = route.predicates |
0c29cf
|
100 |
info = {'match': match, 'route': route} |
44bc11
|
101 |
if preds and not all((p(info, request) for p in preds)): |
65476e
|
102 |
continue |
44bc11
|
103 |
return info |
8392d7
|
104 |
|
0c29cf
|
105 |
return {'route': None, 'match': None} |
MM |
106 |
|
62267e
|
107 |
|
05c023
|
108 |
# stolen from bobo and modified |
dceff5
|
109 |
old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)') |
06f57f
|
110 |
star_at_end = re.compile(r'\*(\w*)$') |
b8fc93
|
111 |
|
6516a8
|
112 |
# The tortuous nature of the regex named ``route_re`` below is due to the |
e39ddf
|
113 |
# fact that we need to support at least one level of "inner" squigglies |
JG |
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}}. |
dceff5
|
116 |
route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})') |
959523
|
117 |
|
0c29cf
|
118 |
|
4018ad
|
119 |
def update_pattern(matchobj): |
BB |
120 |
name = matchobj.group(0) |
|
121 |
return '{%s}' % name[1:] |
0c29cf
|
122 |
|
4018ad
|
123 |
|
BB |
124 |
def _compile_route(route): |
a511b1
|
125 |
# This function really wants to consume Unicode patterns natively, but if |
CM |
126 |
# someone passes us a bytestring, we allow it by converting it to Unicode |
6516a8
|
127 |
# using the ASCII decoding. We decode it using ASCII because we don't |
a511b1
|
128 |
# want to accept bytestrings with high-order characters in them here as |
CM |
129 |
# we have no idea what the encoding represents. |
|
130 |
if route.__class__ is not text_type: |
785358
|
131 |
try: |
CM |
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 ' |
0c29cf
|
137 |
'any non-ASCII characters (you provided %r).' % route |
MM |
138 |
) |
a511b1
|
139 |
|
4018ad
|
140 |
if old_route_re.search(route) and not route_re.search(route): |
BB |
141 |
route = old_route_re.sub(update_pattern, route) |
|
142 |
|
05c023
|
143 |
if not route.startswith('/'): |
CM |
144 |
route = '/' + route |
560e6e
|
145 |
|
a511b1
|
146 |
remainder = None |
da171d
|
147 |
if star_at_end.search(route): |
a511b1
|
148 |
route, remainder = route.rsplit('*', 1) |
da171d
|
149 |
|
05c023
|
150 |
pat = route_re.split(route) |
a511b1
|
151 |
|
CM |
152 |
# every element in "pat" will be Unicode (regardless of whether the |
|
153 |
# route_re regex pattern is itself Unicode or str) |
05c023
|
154 |
pat.reverse() |
CM |
155 |
rpat = [] |
|
156 |
gen = [] |
0c29cf
|
157 |
prefix = pat.pop() # invar: always at least one element (route='/'+route) |
a511b1
|
158 |
|
CM |
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. |
0c29cf
|
163 |
gen.append( |
MM |
164 |
quote_path_segment(prefix, safe='/').replace('%', '%%') |
|
165 |
) # native |
|
166 |
rpat.append(re.escape(prefix)) # unicode |
560e6e
|
167 |
|
05c023
|
168 |
while pat: |
0c29cf
|
169 |
name = pat.pop() # unicode |
4018ad
|
170 |
name = name[1:-1] |
BB |
171 |
if ':' in name: |
88bbd4
|
172 |
# reg may contain colons as well, |
MA |
173 |
# so we must strictly split name into two parts |
|
174 |
name, reg = name.split(':', 1) |
4018ad
|
175 |
else: |
BB |
176 |
reg = '[^/]+' |
0c29cf
|
177 |
gen.append('%%(%s)s' % native_(name)) # native |
MM |
178 |
name = '(?P<%s>%s)' % (name, reg) # unicode |
05c023
|
179 |
rpat.append(name) |
0c29cf
|
180 |
s = pat.pop() # unicode |
05c023
|
181 |
if s: |
0c29cf
|
182 |
rpat.append(re.escape(s)) # unicode |
a511b1
|
183 |
# We want to generate URL-encoded URLs, so we url-quote this |
CM |
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('%', '%%')) |
05c023
|
189 |
|
a511b1
|
190 |
if remainder: |
0c29cf
|
191 |
rpat.append('(?P<%s>.*?)' % remainder) # unicode |
MM |
192 |
gen.append('%%(%s)s' % native_(remainder)) # native |
05c023
|
193 |
|
0c29cf
|
194 |
pattern = ''.join(rpat) + '$' # unicode |
05c023
|
195 |
|
CM |
196 |
match = re.compile(pattern).match |
0c29cf
|
197 |
|
05c023
|
198 |
def matcher(path): |
a511b1
|
199 |
# This function really wants to consume Unicode patterns natively, |
CM |
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 |
6516a8
|
202 |
# because we don't want to accept bytestrings with high-order |
a511b1
|
203 |
# characters in them here as we have no idea what the encoding |
CM |
204 |
# represents. |
|
205 |
if path.__class__ is not text_type: |
|
206 |
path = text_(path, 'ascii') |
05c023
|
207 |
m = match(path) |
CM |
208 |
if m is None: |
a511b1
|
209 |
return None |
25cbe1
|
210 |
d = {} |
e6c2d2
|
211 |
for k, v in m.groupdict().items(): |
a511b1
|
212 |
# k and v will be Unicode 2.6.4 and lower doesnt accept unicode |
CM |
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) |
ac4689
|
218 |
else: |
a511b1
|
219 |
d[nk] = v |
25cbe1
|
220 |
return d |
05c023
|
221 |
|
CM |
222 |
gen = ''.join(gen) |
ece7e5
|
223 |
|
MK |
224 |
def q(v): |
f52759
|
225 |
return quote_path_segment(v, safe=PATH_SAFE) |
ece7e5
|
226 |
|
05c023
|
227 |
def generator(dict): |
CM |
228 |
newdict = {} |
|
229 |
for k, v in dict.items(): |
bc37a5
|
230 |
if PY2: |
a511b1
|
231 |
if v.__class__ is text_type: |
CM |
232 |
# url_quote below needs bytes, not unicode on Py2 |
|
233 |
v = v.encode('utf-8') |
bc37a5
|
234 |
else: |
MM |
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') |
785358
|
238 |
|
CM |
239 |
if k == remainder: |
|
240 |
# a stararg argument |
|
241 |
if is_nonstr_iter(v): |
0c29cf
|
242 |
v = '/'.join([q(x) for x in v]) # native |
785358
|
243 |
else: |
CM |
244 |
if v.__class__ not in string_types: |
|
245 |
v = str(v) |
ece7e5
|
246 |
v = q(v) |
785358
|
247 |
else: |
9d7eea
|
248 |
if v.__class__ not in string_types: |
CM |
249 |
v = str(v) |
a511b1
|
250 |
# v may be bytes (py2) or native string (py3) |
ece7e5
|
251 |
v = q(v) |
a511b1
|
252 |
|
CM |
253 |
# at this point, the value will be a native string |
05c023
|
254 |
newdict[k] = v |
a511b1
|
255 |
|
0c29cf
|
256 |
result = gen % newdict # native string result |
a511b1
|
257 |
return result |
05c023
|
258 |
|
CM |
259 |
return matcher, generator |