commit | author | age
|
50cedf
|
1 |
import itertools |
05c023
|
2 |
import re |
50cedf
|
3 |
from bisect import insort |
25cbe1
|
4 |
from urllib import unquote |
74409d
|
5 |
|
b60bdb
|
6 |
from pyramid.interfaces import IRoute |
534cb4
|
7 |
from pyramid.interfaces import IRouteGroup |
MM |
8 |
from pyramid.interfaces import IRoutesMapper |
8392d7
|
9 |
|
b60bdb
|
10 |
from pyramid.encode import url_quote |
CM |
11 |
from pyramid.exceptions import URLDecodeError |
|
12 |
from pyramid.traversal import traversal_path |
|
13 |
from pyramid.traversal import quote_path_segment |
1aa864
|
14 |
from pyramid.util import join_elements |
534cb4
|
15 |
|
MM |
16 |
from zope.interface import implements |
74409d
|
17 |
|
dfc2b6
|
18 |
_marker = object() |
277df8
|
19 |
|
05c023
|
20 |
class Route(object): |
74409d
|
21 |
implements(IRoute) |
70f1cd
|
22 |
def __init__(self, name, pattern, factory=None, predicates=(), |
4018ad
|
23 |
pregenerator=None): |
74409d
|
24 |
self.pattern = pattern |
CM |
25 |
self.path = pattern # indefinite b/w compat, not in interface |
671416
|
26 |
self.match, self.generate, self.args = _compile_route(pattern) |
05c023
|
27 |
self.name = name |
CM |
28 |
self.factory = factory |
65476e
|
29 |
self.predicates = predicates |
70f1cd
|
30 |
self.pregenerator = pregenerator |
05c023
|
31 |
|
1aa864
|
32 |
def gen(self, request, elements, kw): |
MM |
33 |
if self.pregenerator is not None: |
|
34 |
elements, kw = self.pregenerator(request, elements, kw) |
|
35 |
|
|
36 |
path = self.generate(kw) |
|
37 |
|
|
38 |
if elements: |
|
39 |
suffix = join_elements(elements) |
|
40 |
if not path.endswith('/'): |
|
41 |
suffix = '/' + suffix |
|
42 |
else: |
|
43 |
suffix = '' |
|
44 |
|
|
45 |
return path + suffix, kw |
|
46 |
|
50cedf
|
47 |
class RouteGroup(object): |
534cb4
|
48 |
implements(IRouteGroup) |
50cedf
|
49 |
def __init__(self, name): |
MM |
50 |
self.name = name |
|
51 |
self.counter = itertools.count(1) |
|
52 |
self.routes = [] |
|
53 |
self.sorted_routes = [] |
|
54 |
|
7ecc82
|
55 |
def _find_best_match(self, request, elements, kw): |
50cedf
|
56 |
""" Compare the provided keys to the required args of a route. |
MM |
57 |
|
|
58 |
The selected route is the first one with all required keys satisfied. |
|
59 |
""" |
|
60 |
default_keys = frozenset(kw.keys()) |
|
61 |
for entry in self.sorted_routes: |
|
62 |
args, route = entry[2:4] |
|
63 |
if route.pregenerator is not None: |
|
64 |
e, k = route.pregenerator(request, elements[:], dict(kw)) |
|
65 |
keys = frozenset(k.keys()) |
|
66 |
else: |
|
67 |
e, k, keys = elements, kw, default_keys |
|
68 |
if args.issubset(keys): |
|
69 |
return route, e, k |
|
70 |
raise KeyError('Cannot find matching route in group "%s" using ' |
|
71 |
'provided keys "%s"' % (self.name, sorted(keys))) |
|
72 |
|
|
73 |
def gen(self, request, elements, kw): |
7ecc82
|
74 |
route, elements, kw = self._find_best_match(request, elements, kw) |
50cedf
|
75 |
|
MM |
76 |
path = route.generate(kw) |
|
77 |
|
|
78 |
if elements: |
|
79 |
suffix = join_elements(elements) |
|
80 |
if not path.endswith('/'): |
|
81 |
suffix = '/' + suffix |
|
82 |
else: |
|
83 |
suffix = '' |
|
84 |
|
|
85 |
return path + suffix, kw |
|
86 |
|
|
87 |
def add(self, route): |
|
88 |
self.routes.append(route) |
|
89 |
|
|
90 |
args = frozenset(route.args) |
534cb4
|
91 |
# -len(args) sorts routes in descending order by the number of args |
7ecc82
|
92 |
# -next(self.counter) sorts routes in reverse order of adding |
MM |
93 |
entry = (-len(args), -next(self.counter), args, route) |
50cedf
|
94 |
insort(self.sorted_routes, entry) |
MM |
95 |
|
cbfafb
|
96 |
class RoutesMapper(object): |
74409d
|
97 |
implements(IRoutesMapper) |
cbfafb
|
98 |
def __init__(self): |
05c023
|
99 |
self.routelist = [] |
CM |
100 |
self.routes = {} |
534cb4
|
101 |
self.groups = {} |
62267e
|
102 |
|
CM |
103 |
def has_routes(self): |
05c023
|
104 |
return bool(self.routelist) |
62267e
|
105 |
|
750ce4
|
106 |
def get_routes(self): |
CM |
107 |
return self.routelist |
|
108 |
|
534cb4
|
109 |
def get_groups(self): |
MM |
110 |
return self.groups |
|
111 |
|
74409d
|
112 |
def get_route(self, name): |
CM |
113 |
return self.routes.get(name) |
534cb4
|
114 |
|
MM |
115 |
def get_group(self, name): |
|
116 |
return self.groups.get(name) |
74409d
|
117 |
|
70f1cd
|
118 |
def connect(self, name, pattern, factory=None, predicates=(), |
e725cf
|
119 |
pregenerator=None, static=False): |
4018ad
|
120 |
route = Route(name, pattern, factory, predicates, pregenerator) |
534cb4
|
121 |
group = self.get_group(name) |
MM |
122 |
if group is not None: |
44c2bb
|
123 |
group.add(route) |
MM |
124 |
else: |
534cb4
|
125 |
oldroute = self.get_route(name) |
MM |
126 |
if oldroute in self.routelist: |
|
127 |
self.routelist.remove(oldroute) |
44c2bb
|
128 |
self.routes[name] = route |
534cb4
|
129 |
if not static: |
MM |
130 |
self.routelist.append(route) |
05c023
|
131 |
return route |
CM |
132 |
|
534cb4
|
133 |
def add_group(self, name): |
MM |
134 |
oldgroup = self.get_group(name) |
|
135 |
oldroute = self.get_route(name) |
|
136 |
if oldgroup is not None: |
|
137 |
for route in oldgroup.routes: |
|
138 |
if route in self.routelist: |
|
139 |
self.routelist.remove(route) |
|
140 |
elif oldroute is not None: |
|
141 |
if oldroute in self.routelist: |
|
142 |
self.routelist.remove(oldroute) |
|
143 |
group = RouteGroup(name) |
|
144 |
self.groups[name] = group |
|
145 |
self.routes[name] = group |
|
146 |
return group |
|
147 |
|
05c023
|
148 |
def generate(self, name, kw): |
CM |
149 |
return self.routes[name].generate(kw) |
8392d7
|
150 |
|
11644e
|
151 |
def __call__(self, request): |
cbfafb
|
152 |
environ = request.environ |
11644e
|
153 |
try: |
cbfafb
|
154 |
# empty if mounted under a path in mod_wsgi, for example |
CM |
155 |
path = environ['PATH_INFO'] or '/' |
f646fe
|
156 |
except KeyError: |
22e72d
|
157 |
path = '/' |
CM |
158 |
|
05c023
|
159 |
for route in self.routelist: |
CM |
160 |
match = route.match(path) |
|
161 |
if match is not None: |
65476e
|
162 |
preds = route.predicates |
23ab84
|
163 |
info = {'match':match, 'route':route} |
44bc11
|
164 |
if preds and not all((p(info, request) for p in preds)): |
65476e
|
165 |
continue |
44bc11
|
166 |
return info |
8392d7
|
167 |
|
cbfafb
|
168 |
return {'route':None, 'match':None} |
62267e
|
169 |
|
05c023
|
170 |
# stolen from bobo and modified |
4018ad
|
171 |
old_route_re = re.compile(r'(\:[a-zA-Z]\w*)') |
da171d
|
172 |
star_at_end = re.compile(r'\*\w*$') |
b8fc93
|
173 |
|
e39ddf
|
174 |
# The torturous nature of the regex named ``route_re`` below is due to the |
JG |
175 |
# fact that we need to support at least one level of "inner" squigglies |
|
176 |
# inside the expr of a {name:expr} pattern. This regex used to be just |
|
177 |
# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}. |
|
178 |
route_re = re.compile(r'(\{[a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})') |
959523
|
179 |
|
4018ad
|
180 |
def update_pattern(matchobj): |
BB |
181 |
name = matchobj.group(0) |
|
182 |
return '{%s}' % name[1:] |
|
183 |
|
|
184 |
def _compile_route(route): |
|
185 |
if old_route_re.search(route) and not route_re.search(route): |
|
186 |
route = old_route_re.sub(update_pattern, route) |
|
187 |
|
05c023
|
188 |
if not route.startswith('/'): |
CM |
189 |
route = '/' + route |
560e6e
|
190 |
|
05c023
|
191 |
star = None |
da171d
|
192 |
if star_at_end.search(route): |
05c023
|
193 |
route, star = route.rsplit('*', 1) |
da171d
|
194 |
|
05c023
|
195 |
pat = route_re.split(route) |
CM |
196 |
pat.reverse() |
|
197 |
rpat = [] |
|
198 |
gen = [] |
560e6e
|
199 |
prefix = pat.pop() # invar: always at least one element (route='/'+route) |
CM |
200 |
rpat.append(re.escape(prefix)) |
|
201 |
gen.append(prefix) |
671416
|
202 |
args = [] # list of placeholder names in the pattern |
560e6e
|
203 |
|
05c023
|
204 |
while pat: |
CM |
205 |
name = pat.pop() |
4018ad
|
206 |
name = name[1:-1] |
BB |
207 |
if ':' in name: |
|
208 |
name, reg = name.split(':') |
|
209 |
else: |
|
210 |
reg = '[^/]+' |
671416
|
211 |
args.append(name) |
e84116
|
212 |
gen.append('%%(%s)s' % name) |
4018ad
|
213 |
name = '(?P<%s>%s)' % (name, reg) |
05c023
|
214 |
rpat.append(name) |
CM |
215 |
s = pat.pop() |
|
216 |
if s: |
|
217 |
rpat.append(re.escape(s)) |
|
218 |
gen.append(s) |
|
219 |
|
|
220 |
if star: |
671416
|
221 |
args.append(star) |
05c023
|
222 |
rpat.append('(?P<%s>.*?)' % star) |
CM |
223 |
gen.append('%%(%s)s' % star) |
|
224 |
|
|
225 |
pattern = ''.join(rpat) + '$' |
|
226 |
|
|
227 |
match = re.compile(pattern).match |
|
228 |
def matcher(path): |
|
229 |
m = match(path) |
|
230 |
if m is None: |
|
231 |
return m |
25cbe1
|
232 |
d = {} |
ac4689
|
233 |
for k, v in m.groupdict().iteritems(): |
CM |
234 |
if k == star: |
|
235 |
d[k] = traversal_path(v) |
|
236 |
else: |
|
237 |
encoded = unquote(v) |
|
238 |
try: |
|
239 |
d[k] = encoded.decode('utf-8') |
|
240 |
except UnicodeDecodeError, e: |
|
241 |
raise URLDecodeError( |
|
242 |
e.encoding, e.object, e.start, e.end, e.reason |
|
243 |
) |
0482bd
|
244 |
|
CM |
245 |
|
25cbe1
|
246 |
return d |
CM |
247 |
|
05c023
|
248 |
|
CM |
249 |
gen = ''.join(gen) |
|
250 |
def generator(dict): |
|
251 |
newdict = {} |
|
252 |
for k, v in dict.items(): |
|
253 |
if isinstance(v, unicode): |
|
254 |
v = v.encode('utf-8') |
25cbe1
|
255 |
if k == star and hasattr(v, '__iter__'): |
CM |
256 |
v = '/'.join([quote_path_segment(x) for x in v]) |
|
257 |
elif k != star: |
17358d
|
258 |
try: |
eb9fbf
|
259 |
v = url_quote(v) |
17358d
|
260 |
except TypeError: |
CM |
261 |
pass |
05c023
|
262 |
newdict[k] = v |
CM |
263 |
return gen % newdict |
|
264 |
|
671416
|
265 |
return matcher, generator, args |
187941
|
266 |
|
MM |
267 |
def DefaultsPregenerator(defaults, wrapped=None): |
|
268 |
if wrapped is None: |
|
269 |
wrapped = lambda r, e, k: (e, k) |
|
270 |
def generator(request, elements, kwargs): |
|
271 |
newkw = dict(defaults) |
|
272 |
newkw.update(kwargs) |
|
273 |
return wrapped(request, elements, newkw) |
|
274 |
return generator |