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