1 | import re |
---|
2 | import sys |
---|
3 | import urllib |
---|
4 | |
---|
5 | if sys.version < '2.4': |
---|
6 | from sets import ImmutableSet as frozenset |
---|
7 | |
---|
8 | from routes.util import _url_quote as url_quote, _str_encode |
---|
9 | |
---|
10 | |
---|
11 | class Route(object): |
---|
12 | """The Route object holds a route recognition and generation |
---|
13 | routine. |
---|
14 | |
---|
15 | See Route.__init__ docs for usage. |
---|
16 | |
---|
17 | """ |
---|
18 | # reserved keys that don't count |
---|
19 | reserved_keys = ['requirements'] |
---|
20 | |
---|
21 | # special chars to indicate a natural split in the URL |
---|
22 | done_chars = ('/', ',', ';', '.', '#') |
---|
23 | |
---|
24 | def __init__(self, name, routepath, **kargs): |
---|
25 | """Initialize a route, with a given routepath for |
---|
26 | matching/generation |
---|
27 | |
---|
28 | The set of keyword args will be used as defaults. |
---|
29 | |
---|
30 | Usage:: |
---|
31 | |
---|
32 | >>> from routes.base import Route |
---|
33 | >>> newroute = Route(None, ':controller/:action/:id') |
---|
34 | >>> sorted(newroute.defaults.items()) |
---|
35 | [('action', 'index'), ('id', None)] |
---|
36 | >>> newroute = Route(None, 'date/:year/:month/:day', |
---|
37 | ... controller="blog", action="view") |
---|
38 | >>> newroute = Route(None, 'archives/:page', controller="blog", |
---|
39 | ... action="by_page", requirements = { 'page':'\d{1,2}' }) |
---|
40 | >>> newroute.reqs |
---|
41 | {'page': '\\\d{1,2}'} |
---|
42 | |
---|
43 | .. Note:: |
---|
44 | Route is generally not called directly, a Mapper instance |
---|
45 | connect method should be used to add routes. |
---|
46 | |
---|
47 | """ |
---|
48 | self.routepath = routepath |
---|
49 | self.sub_domains = False |
---|
50 | self.prior = None |
---|
51 | self.redirect = False |
---|
52 | self.name = name |
---|
53 | self._kargs = kargs |
---|
54 | self.minimization = kargs.pop('_minimize', False) |
---|
55 | self.encoding = kargs.pop('_encoding', 'utf-8') |
---|
56 | self.reqs = kargs.get('requirements', {}) |
---|
57 | self.decode_errors = 'replace' |
---|
58 | |
---|
59 | # Don't bother forming stuff we don't need if its a static route |
---|
60 | self.static = kargs.pop('_static', False) |
---|
61 | self.filter = kargs.pop('_filter', None) |
---|
62 | self.absolute = kargs.pop('_absolute', False) |
---|
63 | |
---|
64 | # Pull out the member/collection name if present, this applies only to |
---|
65 | # map.resource |
---|
66 | self.member_name = kargs.pop('_member_name', None) |
---|
67 | self.collection_name = kargs.pop('_collection_name', None) |
---|
68 | self.parent_resource = kargs.pop('_parent_resource', None) |
---|
69 | |
---|
70 | # Pull out route conditions |
---|
71 | self.conditions = kargs.pop('conditions', None) |
---|
72 | |
---|
73 | # Determine if explicit behavior should be used |
---|
74 | self.explicit = kargs.pop('_explicit', False) |
---|
75 | |
---|
76 | # Since static need to be generated exactly, treat them as |
---|
77 | # non-minimized |
---|
78 | if self.static: |
---|
79 | self.external = '://' in self.routepath |
---|
80 | self.minimization = False |
---|
81 | |
---|
82 | # Strip preceding '/' if present, and not minimizing |
---|
83 | if routepath.startswith('/') and self.minimization: |
---|
84 | self.routepath = routepath[1:] |
---|
85 | self._setup_route() |
---|
86 | |
---|
87 | def _setup_route(self): |
---|
88 | # Build our routelist, and the keys used in the route |
---|
89 | self.routelist = routelist = self._pathkeys(self.routepath) |
---|
90 | routekeys = frozenset([key['name'] for key in routelist |
---|
91 | if isinstance(key, dict)]) |
---|
92 | self.dotkeys = frozenset([key['name'] for key in routelist |
---|
93 | if isinstance(key, dict) and |
---|
94 | key['type'] == '.']) |
---|
95 | |
---|
96 | if not self.minimization: |
---|
97 | self.make_full_route() |
---|
98 | |
---|
99 | # Build a req list with all the regexp requirements for our args |
---|
100 | self.req_regs = {} |
---|
101 | for key, val in self.reqs.iteritems(): |
---|
102 | self.req_regs[key] = re.compile('^' + val + '$') |
---|
103 | # Update our defaults and set new default keys if needed. defaults |
---|
104 | # needs to be saved |
---|
105 | (self.defaults, defaultkeys) = self._defaults(routekeys, |
---|
106 | self.reserved_keys, |
---|
107 | self._kargs.copy()) |
---|
108 | # Save the maximum keys we could utilize |
---|
109 | self.maxkeys = defaultkeys | routekeys |
---|
110 | |
---|
111 | # Populate our minimum keys, and save a copy of our backward keys for |
---|
112 | # quicker generation later |
---|
113 | (self.minkeys, self.routebackwards) = self._minkeys(routelist[:]) |
---|
114 | |
---|
115 | # Populate our hardcoded keys, these are ones that are set and don't |
---|
116 | # exist in the route |
---|
117 | self.hardcoded = frozenset([key for key in self.maxkeys \ |
---|
118 | if key not in routekeys and self.defaults[key] is not None]) |
---|
119 | |
---|
120 | # Cache our default keys |
---|
121 | self._default_keys = frozenset(self.defaults.keys()) |
---|
122 | |
---|
123 | def make_full_route(self): |
---|
124 | """Make a full routelist string for use with non-minimized |
---|
125 | generation""" |
---|
126 | regpath = '' |
---|
127 | for part in self.routelist: |
---|
128 | if isinstance(part, dict): |
---|
129 | regpath += '%(' + part['name'] + ')s' |
---|
130 | else: |
---|
131 | regpath += part |
---|
132 | self.regpath = regpath |
---|
133 | |
---|
134 | def make_unicode(self, s): |
---|
135 | """Transform the given argument into a unicode string.""" |
---|
136 | if isinstance(s, unicode): |
---|
137 | return s |
---|
138 | elif isinstance(s, str): |
---|
139 | return s.decode(self.encoding) |
---|
140 | elif callable(s): |
---|
141 | return s |
---|
142 | else: |
---|
143 | return unicode(s) |
---|
144 | |
---|
145 | def _pathkeys(self, routepath): |
---|
146 | """Utility function to walk the route, and pull out the valid |
---|
147 | dynamic/wildcard keys.""" |
---|
148 | collecting = False |
---|
149 | current = '' |
---|
150 | done_on = '' |
---|
151 | var_type = '' |
---|
152 | just_started = False |
---|
153 | routelist = [] |
---|
154 | for char in routepath: |
---|
155 | if char in [':', '*', '{'] and not collecting and not self.static \ |
---|
156 | or char in ['{'] and not collecting: |
---|
157 | just_started = True |
---|
158 | collecting = True |
---|
159 | var_type = char |
---|
160 | if char == '{': |
---|
161 | done_on = '}' |
---|
162 | just_started = False |
---|
163 | if len(current) > 0: |
---|
164 | routelist.append(current) |
---|
165 | current = '' |
---|
166 | elif collecting and just_started: |
---|
167 | just_started = False |
---|
168 | if char == '(': |
---|
169 | done_on = ')' |
---|
170 | else: |
---|
171 | current = char |
---|
172 | done_on = self.done_chars + ('-',) |
---|
173 | elif collecting and char not in done_on: |
---|
174 | current += char |
---|
175 | elif collecting: |
---|
176 | collecting = False |
---|
177 | if var_type == '{': |
---|
178 | if current[0] == '.': |
---|
179 | var_type = '.' |
---|
180 | current = current[1:] |
---|
181 | else: |
---|
182 | var_type = ':' |
---|
183 | opts = current.split(':') |
---|
184 | if len(opts) > 1: |
---|
185 | current = opts[0] |
---|
186 | self.reqs[current] = opts[1] |
---|
187 | routelist.append(dict(type=var_type, name=current)) |
---|
188 | if char in self.done_chars: |
---|
189 | routelist.append(char) |
---|
190 | done_on = var_type = current = '' |
---|
191 | else: |
---|
192 | current += char |
---|
193 | if collecting: |
---|
194 | routelist.append(dict(type=var_type, name=current)) |
---|
195 | elif current: |
---|
196 | routelist.append(current) |
---|
197 | return routelist |
---|
198 | |
---|
199 | def _minkeys(self, routelist): |
---|
200 | """Utility function to walk the route backwards |
---|
201 | |
---|
202 | Will also determine the minimum keys we can handle to generate |
---|
203 | a working route. |
---|
204 | |
---|
205 | routelist is a list of the '/' split route path |
---|
206 | defaults is a dict of all the defaults provided for the route |
---|
207 | |
---|
208 | """ |
---|
209 | minkeys = [] |
---|
210 | backcheck = routelist[:] |
---|
211 | |
---|
212 | # If we don't honor minimization, we need all the keys in the |
---|
213 | # route path |
---|
214 | if not self.minimization: |
---|
215 | for part in backcheck: |
---|
216 | if isinstance(part, dict): |
---|
217 | minkeys.append(part['name']) |
---|
218 | return (frozenset(minkeys), backcheck) |
---|
219 | |
---|
220 | gaps = False |
---|
221 | backcheck.reverse() |
---|
222 | for part in backcheck: |
---|
223 | if not isinstance(part, dict) and part not in self.done_chars: |
---|
224 | gaps = True |
---|
225 | continue |
---|
226 | elif not isinstance(part, dict): |
---|
227 | continue |
---|
228 | key = part['name'] |
---|
229 | if self.defaults.has_key(key) and not gaps: |
---|
230 | continue |
---|
231 | minkeys.append(key) |
---|
232 | gaps = True |
---|
233 | return (frozenset(minkeys), backcheck) |
---|
234 | |
---|
235 | def _defaults(self, routekeys, reserved_keys, kargs): |
---|
236 | """Creates default set with values stringified |
---|
237 | |
---|
238 | Put together our list of defaults, stringify non-None values |
---|
239 | and add in our action/id default if they use it and didn't |
---|
240 | specify it. |
---|
241 | |
---|
242 | defaultkeys is a list of the currently assumed default keys |
---|
243 | routekeys is a list of the keys found in the route path |
---|
244 | reserved_keys is a list of keys that are not |
---|
245 | |
---|
246 | """ |
---|
247 | defaults = {} |
---|
248 | # Add in a controller/action default if they don't exist |
---|
249 | if 'controller' not in routekeys and 'controller' not in kargs \ |
---|
250 | and not self.explicit: |
---|
251 | kargs['controller'] = 'content' |
---|
252 | if 'action' not in routekeys and 'action' not in kargs \ |
---|
253 | and not self.explicit: |
---|
254 | kargs['action'] = 'index' |
---|
255 | defaultkeys = frozenset([key for key in kargs.keys() \ |
---|
256 | if key not in reserved_keys]) |
---|
257 | for key in defaultkeys: |
---|
258 | if kargs[key] is not None: |
---|
259 | defaults[key] = self.make_unicode(kargs[key]) |
---|
260 | else: |
---|
261 | defaults[key] = None |
---|
262 | if 'action' in routekeys and not defaults.has_key('action') \ |
---|
263 | and not self.explicit: |
---|
264 | defaults['action'] = 'index' |
---|
265 | if 'id' in routekeys and not defaults.has_key('id') \ |
---|
266 | and not self.explicit: |
---|
267 | defaults['id'] = None |
---|
268 | newdefaultkeys = frozenset([key for key in defaults.keys() \ |
---|
269 | if key not in reserved_keys]) |
---|
270 | |
---|
271 | return (defaults, newdefaultkeys) |
---|
272 | |
---|
273 | def makeregexp(self, clist, include_names=True): |
---|
274 | """Create a regular expression for matching purposes |
---|
275 | |
---|
276 | Note: This MUST be called before match can function properly. |
---|
277 | |
---|
278 | clist should be a list of valid controller strings that can be |
---|
279 | matched, for this reason makeregexp should be called by the web |
---|
280 | framework after it knows all available controllers that can be |
---|
281 | utilized. |
---|
282 | |
---|
283 | include_names indicates whether this should be a match regexp |
---|
284 | assigned to itself using regexp grouping names, or if names |
---|
285 | should be excluded for use in a single larger regexp to |
---|
286 | determine if any routes match |
---|
287 | |
---|
288 | """ |
---|
289 | if self.minimization: |
---|
290 | reg = self.buildnextreg(self.routelist, clist, include_names)[0] |
---|
291 | if not reg: |
---|
292 | reg = '/' |
---|
293 | reg = reg + '/?' + '$' |
---|
294 | |
---|
295 | if not reg.startswith('/'): |
---|
296 | reg = '/' + reg |
---|
297 | else: |
---|
298 | reg = self.buildfullreg(clist, include_names) |
---|
299 | |
---|
300 | reg = '^' + reg |
---|
301 | |
---|
302 | if not include_names: |
---|
303 | return reg |
---|
304 | |
---|
305 | self.regexp = reg |
---|
306 | self.regmatch = re.compile(reg) |
---|
307 | |
---|
308 | def buildfullreg(self, clist, include_names=True): |
---|
309 | """Build the regexp by iterating through the routelist and |
---|
310 | replacing dicts with the appropriate regexp match""" |
---|
311 | regparts = [] |
---|
312 | for part in self.routelist: |
---|
313 | if isinstance(part, dict): |
---|
314 | var = part['name'] |
---|
315 | if var == 'controller': |
---|
316 | partmatch = '|'.join(map(re.escape, clist)) |
---|
317 | elif part['type'] == ':': |
---|
318 | partmatch = self.reqs.get(var) or '[^/]+?' |
---|
319 | elif part['type'] == '.': |
---|
320 | partmatch = self.reqs.get(var) or '[^/.]+?' |
---|
321 | else: |
---|
322 | partmatch = self.reqs.get(var) or '.+?' |
---|
323 | if include_names: |
---|
324 | regpart = '(?P<%s>%s)' % (var, partmatch) |
---|
325 | else: |
---|
326 | regpart = '(?:%s)' % partmatch |
---|
327 | if part['type'] == '.': |
---|
328 | regparts.append('(?:\.%s)??' % regpart) |
---|
329 | else: |
---|
330 | regparts.append(regpart) |
---|
331 | else: |
---|
332 | regparts.append(re.escape(part)) |
---|
333 | regexp = ''.join(regparts) + '$' |
---|
334 | return regexp |
---|
335 | |
---|
336 | def buildnextreg(self, path, clist, include_names=True): |
---|
337 | """Recursively build our regexp given a path, and a controller |
---|
338 | list. |
---|
339 | |
---|
340 | Returns the regular expression string, and two booleans that |
---|
341 | can be ignored as they're only used internally by buildnextreg. |
---|
342 | |
---|
343 | """ |
---|
344 | if path: |
---|
345 | part = path[0] |
---|
346 | else: |
---|
347 | part = '' |
---|
348 | reg = '' |
---|
349 | |
---|
350 | # noreqs will remember whether the remainder has either a string |
---|
351 | # match, or a non-defaulted regexp match on a key, allblank remembers |
---|
352 | # if the rest could possible be completely empty |
---|
353 | (rest, noreqs, allblank) = ('', True, True) |
---|
354 | if len(path[1:]) > 0: |
---|
355 | self.prior = part |
---|
356 | (rest, noreqs, allblank) = self.buildnextreg(path[1:], clist, include_names) |
---|
357 | |
---|
358 | if isinstance(part, dict) and part['type'] in (':', '.'): |
---|
359 | var = part['name'] |
---|
360 | typ = part['type'] |
---|
361 | partreg = '' |
---|
362 | |
---|
363 | # First we plug in the proper part matcher |
---|
364 | if self.reqs.has_key(var): |
---|
365 | if include_names: |
---|
366 | partreg = '(?P<%s>%s)' % (var, self.reqs[var]) |
---|
367 | else: |
---|
368 | partreg = '(?:%s)' % self.reqs[var] |
---|
369 | if typ == '.': |
---|
370 | partreg = '(?:\.%s)??' % partreg |
---|
371 | elif var == 'controller': |
---|
372 | if include_names: |
---|
373 | partreg = '(?P<%s>%s)' % (var, '|'.join(map(re.escape, clist))) |
---|
374 | else: |
---|
375 | partreg = '(?:%s)' % '|'.join(map(re.escape, clist)) |
---|
376 | elif self.prior in ['/', '#']: |
---|
377 | if include_names: |
---|
378 | partreg = '(?P<' + var + '>[^' + self.prior + ']+?)' |
---|
379 | else: |
---|
380 | partreg = '(?:[^' + self.prior + ']+?)' |
---|
381 | else: |
---|
382 | if not rest: |
---|
383 | if typ == '.': |
---|
384 | exclude_chars = '/.' |
---|
385 | else: |
---|
386 | exclude_chars = '/' |
---|
387 | if include_names: |
---|
388 | partreg = '(?P<%s>[^%s]+?)' % (var, exclude_chars) |
---|
389 | else: |
---|
390 | partreg = '(?:[^%s]+?)' % exclude_chars |
---|
391 | if typ == '.': |
---|
392 | partreg = '(?:\.%s)??' % partreg |
---|
393 | else: |
---|
394 | end = ''.join(self.done_chars) |
---|
395 | rem = rest |
---|
396 | if rem[0] == '\\' and len(rem) > 1: |
---|
397 | rem = rem[1] |
---|
398 | elif rem.startswith('(\\') and len(rem) > 2: |
---|
399 | rem = rem[2] |
---|
400 | else: |
---|
401 | rem = end |
---|
402 | rem = frozenset(rem) | frozenset(['/']) |
---|
403 | if include_names: |
---|
404 | partreg = '(?P<%s>[^%s]+?)' % (var, ''.join(rem)) |
---|
405 | else: |
---|
406 | partreg = '(?:[^%s]+?)' % ''.join(rem) |
---|
407 | |
---|
408 | if self.reqs.has_key(var): |
---|
409 | noreqs = False |
---|
410 | if not self.defaults.has_key(var): |
---|
411 | allblank = False |
---|
412 | noreqs = False |
---|
413 | |
---|
414 | # Now we determine if its optional, or required. This changes |
---|
415 | # depending on what is in the rest of the match. If noreqs is |
---|
416 | # true, then its possible the entire thing is optional as there's |
---|
417 | # no reqs or string matches. |
---|
418 | if noreqs: |
---|
419 | # The rest is optional, but now we have an optional with a |
---|
420 | # regexp. Wrap to ensure that if we match anything, we match |
---|
421 | # our regexp first. It's still possible we could be completely |
---|
422 | # blank as we have a default |
---|
423 | if self.reqs.has_key(var) and self.defaults.has_key(var): |
---|
424 | reg = '(' + partreg + rest + ')?' |
---|
425 | |
---|
426 | # Or we have a regexp match with no default, so now being |
---|
427 | # completely blank form here on out isn't possible |
---|
428 | elif self.reqs.has_key(var): |
---|
429 | allblank = False |
---|
430 | reg = partreg + rest |
---|
431 | |
---|
432 | # If the character before this is a special char, it has to be |
---|
433 | # followed by this |
---|
434 | elif self.defaults.has_key(var) and \ |
---|
435 | self.prior in (',', ';', '.'): |
---|
436 | reg = partreg + rest |
---|
437 | |
---|
438 | # Or we have a default with no regexp, don't touch the allblank |
---|
439 | elif self.defaults.has_key(var): |
---|
440 | reg = partreg + '?' + rest |
---|
441 | |
---|
442 | # Or we have a key with no default, and no reqs. Not possible |
---|
443 | # to be all blank from here |
---|
444 | else: |
---|
445 | allblank = False |
---|
446 | reg = partreg + rest |
---|
447 | # In this case, we have something dangling that might need to be |
---|
448 | # matched |
---|
449 | else: |
---|
450 | # If they can all be blank, and we have a default here, we know |
---|
451 | # its safe to make everything from here optional. Since |
---|
452 | # something else in the chain does have req's though, we have |
---|
453 | # to make the partreg here required to continue matching |
---|
454 | if allblank and self.defaults.has_key(var): |
---|
455 | reg = '(' + partreg + rest + ')?' |
---|
456 | |
---|
457 | # Same as before, but they can't all be blank, so we have to |
---|
458 | # require it all to ensure our matches line up right |
---|
459 | else: |
---|
460 | reg = partreg + rest |
---|
461 | elif isinstance(part, dict) and part['type'] == '*': |
---|
462 | var = part['name'] |
---|
463 | if noreqs: |
---|
464 | if include_names: |
---|
465 | reg = '(?P<%s>.*)' % var + rest |
---|
466 | else: |
---|
467 | reg = '(?:.*)' + rest |
---|
468 | if not self.defaults.has_key(var): |
---|
469 | allblank = False |
---|
470 | noreqs = False |
---|
471 | else: |
---|
472 | if allblank and self.defaults.has_key(var): |
---|
473 | if include_names: |
---|
474 | reg = '(?P<%s>.*)' % var + rest |
---|
475 | else: |
---|
476 | reg = '(?:.*)' + rest |
---|
477 | elif self.defaults.has_key(var): |
---|
478 | if include_names: |
---|
479 | reg = '(?P<%s>.*)' % var + rest |
---|
480 | else: |
---|
481 | reg = '(?:.*)' + rest |
---|
482 | else: |
---|
483 | if include_names: |
---|
484 | reg = '(?P<%s>.*)' % var + rest |
---|
485 | else: |
---|
486 | reg = '(?:.*)' + rest |
---|
487 | allblank = False |
---|
488 | noreqs = False |
---|
489 | elif part and part[-1] in self.done_chars: |
---|
490 | if allblank: |
---|
491 | reg = re.escape(part[:-1]) + '(' + re.escape(part[-1]) + rest |
---|
492 | reg += ')?' |
---|
493 | else: |
---|
494 | allblank = False |
---|
495 | reg = re.escape(part) + rest |
---|
496 | |
---|
497 | # We have a normal string here, this is a req, and it prevents us from |
---|
498 | # being all blank |
---|
499 | else: |
---|
500 | noreqs = False |
---|
501 | allblank = False |
---|
502 | reg = re.escape(part) + rest |
---|
503 | |
---|
504 | return (reg, noreqs, allblank) |
---|
505 | |
---|
506 | def match(self, url, environ=None, sub_domains=False, |
---|
507 | sub_domains_ignore=None, domain_match=''): |
---|
508 | """Match a url to our regexp. |
---|
509 | |
---|
510 | While the regexp might match, this operation isn't |
---|
511 | guaranteed as there's other factors that can cause a match to |
---|
512 | fail even though the regexp succeeds (Default that was relied |
---|
513 | on wasn't given, requirement regexp doesn't pass, etc.). |
---|
514 | |
---|
515 | Therefore the calling function shouldn't assume this will |
---|
516 | return a valid dict, the other possible return is False if a |
---|
517 | match doesn't work out. |
---|
518 | |
---|
519 | """ |
---|
520 | # Static routes don't match, they generate only |
---|
521 | if self.static: |
---|
522 | return False |
---|
523 | |
---|
524 | match = self.regmatch.match(url) |
---|
525 | |
---|
526 | if not match: |
---|
527 | return False |
---|
528 | |
---|
529 | sub_domain = None |
---|
530 | |
---|
531 | if sub_domains and environ and 'HTTP_HOST' in environ: |
---|
532 | host = environ['HTTP_HOST'].split(':')[0] |
---|
533 | sub_match = re.compile('^(.+?)\.%s$' % domain_match) |
---|
534 | subdomain = re.sub(sub_match, r'\1', host) |
---|
535 | if subdomain not in sub_domains_ignore and host != subdomain: |
---|
536 | sub_domain = subdomain |
---|
537 | |
---|
538 | if self.conditions: |
---|
539 | if 'method' in self.conditions and environ and \ |
---|
540 | environ['REQUEST_METHOD'] not in self.conditions['method']: |
---|
541 | return False |
---|
542 | |
---|
543 | # Check sub-domains? |
---|
544 | use_sd = self.conditions.get('sub_domain') |
---|
545 | if use_sd and not sub_domain: |
---|
546 | return False |
---|
547 | elif not use_sd and 'sub_domain' in self.conditions and sub_domain: |
---|
548 | return False |
---|
549 | if isinstance(use_sd, list) and sub_domain not in use_sd: |
---|
550 | return False |
---|
551 | |
---|
552 | matchdict = match.groupdict() |
---|
553 | result = {} |
---|
554 | extras = self._default_keys - frozenset(matchdict.keys()) |
---|
555 | for key, val in matchdict.iteritems(): |
---|
556 | if key != 'path_info' and self.encoding: |
---|
557 | # change back into python unicode objects from the URL |
---|
558 | # representation |
---|
559 | try: |
---|
560 | val = val and val.decode(self.encoding, self.decode_errors) |
---|
561 | except UnicodeDecodeError: |
---|
562 | return False |
---|
563 | |
---|
564 | if not val and key in self.defaults and self.defaults[key]: |
---|
565 | result[key] = self.defaults[key] |
---|
566 | else: |
---|
567 | result[key] = val |
---|
568 | for key in extras: |
---|
569 | result[key] = self.defaults[key] |
---|
570 | |
---|
571 | # Add the sub-domain if there is one |
---|
572 | if sub_domains: |
---|
573 | result['sub_domain'] = sub_domain |
---|
574 | |
---|
575 | # If there's a function, call it with environ and expire if it |
---|
576 | # returns False |
---|
577 | if self.conditions and 'function' in self.conditions and \ |
---|
578 | not self.conditions['function'](environ, result): |
---|
579 | return False |
---|
580 | |
---|
581 | return result |
---|
582 | |
---|
583 | def generate_non_minimized(self, kargs): |
---|
584 | """Generate a non-minimal version of the URL""" |
---|
585 | # Iterate through the keys that are defaults, and NOT in the route |
---|
586 | # path. If its not in kargs, or doesn't match, or is None, this |
---|
587 | # route won't work |
---|
588 | for k in self.maxkeys - self.minkeys: |
---|
589 | if k not in kargs: |
---|
590 | return False |
---|
591 | elif self.make_unicode(kargs[k]) != \ |
---|
592 | self.make_unicode(self.defaults[k]): |
---|
593 | return False |
---|
594 | |
---|
595 | # Ensure that all the args in the route path are present and not None |
---|
596 | for arg in self.minkeys: |
---|
597 | if arg not in kargs or kargs[arg] is None: |
---|
598 | if arg in self.dotkeys: |
---|
599 | kargs[arg] = '' |
---|
600 | else: |
---|
601 | return False |
---|
602 | |
---|
603 | # Encode all the argument that the regpath can use |
---|
604 | for k in kargs: |
---|
605 | if k in self.maxkeys: |
---|
606 | if k in self.dotkeys: |
---|
607 | if kargs[k]: |
---|
608 | kargs[k] = url_quote('.' + kargs[k], self.encoding) |
---|
609 | else: |
---|
610 | kargs[k] = url_quote(kargs[k], self.encoding) |
---|
611 | |
---|
612 | return self.regpath % kargs |
---|
613 | |
---|
614 | def generate_minimized(self, kargs): |
---|
615 | """Generate a minimized version of the URL""" |
---|
616 | routelist = self.routebackwards |
---|
617 | urllist = [] |
---|
618 | gaps = False |
---|
619 | for part in routelist: |
---|
620 | if isinstance(part, dict) and part['type'] in (':', '.'): |
---|
621 | arg = part['name'] |
---|
622 | |
---|
623 | # For efficiency, check these just once |
---|
624 | has_arg = kargs.has_key(arg) |
---|
625 | has_default = self.defaults.has_key(arg) |
---|
626 | |
---|
627 | # Determine if we can leave this part off |
---|
628 | # First check if the default exists and wasn't provided in the |
---|
629 | # call (also no gaps) |
---|
630 | if has_default and not has_arg and not gaps: |
---|
631 | continue |
---|
632 | |
---|
633 | # Now check to see if there's a default and it matches the |
---|
634 | # incoming call arg |
---|
635 | if (has_default and has_arg) and self.make_unicode(kargs[arg]) == \ |
---|
636 | self.make_unicode(self.defaults[arg]) and not gaps: |
---|
637 | continue |
---|
638 | |
---|
639 | # We need to pull the value to append, if the arg is None and |
---|
640 | # we have a default, use that |
---|
641 | if has_arg and kargs[arg] is None and has_default and not gaps: |
---|
642 | continue |
---|
643 | |
---|
644 | # Otherwise if we do have an arg, use that |
---|
645 | elif has_arg: |
---|
646 | val = kargs[arg] |
---|
647 | |
---|
648 | elif has_default and self.defaults[arg] is not None: |
---|
649 | val = self.defaults[arg] |
---|
650 | # Optional format parameter? |
---|
651 | elif part['type'] == '.': |
---|
652 | continue |
---|
653 | # No arg at all? This won't work |
---|
654 | else: |
---|
655 | return False |
---|
656 | |
---|
657 | urllist.append(url_quote(val, self.encoding)) |
---|
658 | if part['type'] == '.': |
---|
659 | urllist.append('.') |
---|
660 | |
---|
661 | if has_arg: |
---|
662 | del kargs[arg] |
---|
663 | gaps = True |
---|
664 | elif isinstance(part, dict) and part['type'] == '*': |
---|
665 | arg = part['name'] |
---|
666 | kar = kargs.get(arg) |
---|
667 | if kar is not None: |
---|
668 | urllist.append(url_quote(kar, self.encoding)) |
---|
669 | gaps = True |
---|
670 | elif part and part[-1] in self.done_chars: |
---|
671 | if not gaps and part in self.done_chars: |
---|
672 | continue |
---|
673 | elif not gaps: |
---|
674 | urllist.append(part[:-1]) |
---|
675 | gaps = True |
---|
676 | else: |
---|
677 | gaps = True |
---|
678 | urllist.append(part) |
---|
679 | else: |
---|
680 | gaps = True |
---|
681 | urllist.append(part) |
---|
682 | urllist.reverse() |
---|
683 | url = ''.join(urllist) |
---|
684 | return url |
---|
685 | |
---|
686 | def generate(self, _ignore_req_list=False, _append_slash=False, **kargs): |
---|
687 | """Generate a URL from ourself given a set of keyword arguments |
---|
688 | |
---|
689 | Toss an exception if this |
---|
690 | set of keywords would cause a gap in the url. |
---|
691 | |
---|
692 | """ |
---|
693 | # Verify that our args pass any regexp requirements |
---|
694 | if not _ignore_req_list: |
---|
695 | for key in self.reqs.keys(): |
---|
696 | val = kargs.get(key) |
---|
697 | if val and not self.req_regs[key].match(self.make_unicode(val)): |
---|
698 | return False |
---|
699 | |
---|
700 | # Verify that if we have a method arg, its in the method accept list. |
---|
701 | # Also, method will be changed to _method for route generation |
---|
702 | meth = kargs.get('method') |
---|
703 | if meth: |
---|
704 | if self.conditions and 'method' in self.conditions \ |
---|
705 | and meth.upper() not in self.conditions['method']: |
---|
706 | return False |
---|
707 | kargs.pop('method') |
---|
708 | |
---|
709 | if self.minimization: |
---|
710 | url = self.generate_minimized(kargs) |
---|
711 | else: |
---|
712 | url = self.generate_non_minimized(kargs) |
---|
713 | |
---|
714 | if url is False: |
---|
715 | return url |
---|
716 | |
---|
717 | if not url.startswith('/') and not self.static: |
---|
718 | url = '/' + url |
---|
719 | extras = frozenset(kargs.keys()) - self.maxkeys |
---|
720 | if extras: |
---|
721 | if _append_slash and not url.endswith('/'): |
---|
722 | url += '/' |
---|
723 | fragments = [] |
---|
724 | # don't assume the 'extras' set preserves order: iterate |
---|
725 | # through the ordered kargs instead |
---|
726 | for key in kargs: |
---|
727 | if key not in extras: |
---|
728 | continue |
---|
729 | if key == 'action' or key == 'controller': |
---|
730 | continue |
---|
731 | val = kargs[key] |
---|
732 | if isinstance(val, (tuple, list)): |
---|
733 | for value in val: |
---|
734 | fragments.append((key, _str_encode(value, self.encoding))) |
---|
735 | else: |
---|
736 | fragments.append((key, _str_encode(val, self.encoding))) |
---|
737 | if fragments: |
---|
738 | url += '?' |
---|
739 | url += urllib.urlencode(fragments) |
---|
740 | elif _append_slash and not url.endswith('/'): |
---|
741 | url += '/' |
---|
742 | return url |
---|