comparison env/lib/python3.7/site-packages/routes/route.py @ 5:9b1c78e6ba9c draft default tip

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