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