routing.py (24620B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\carbon\common\lib\werkzeug\routing.py 2 import re 3 from pprint import pformat 4 from urlparse import urljoin 5 from itertools import izip 6 from werkzeug.urls import url_encode, url_quote 7 from werkzeug.utils import redirect, format_string 8 from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed 9 from werkzeug._internal import _get_environ 10 _rule_re = re.compile('\n (?P<static>[^<]*) # static rule data\n <\n (?:\n (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name\n (?:\\((?P<args>.*?)\\))? # converter arguments\n \\: # variable delimiter\n )?\n (?P<variable>[a-zA-Z][a-zA-Z0-9_]*) # variable name\n >\n', re.VERBOSE) 11 _simple_rule_re = re.compile('<([^>]+)>') 12 13 def parse_rule(rule): 14 pos = 0 15 end = len(rule) 16 do_match = _rule_re.match 17 used_names = set() 18 while pos < end: 19 m = do_match(rule, pos) 20 if m is None: 21 break 22 data = m.groupdict() 23 if data['static']: 24 yield (None, None, data['static']) 25 variable = data['variable'] 26 converter = data['converter'] or 'default' 27 if variable in used_names: 28 raise ValueError('variable name %r used twice.' % variable) 29 used_names.add(variable) 30 yield (converter, data['args'] or None, variable) 31 pos = m.end() 32 33 if pos < end: 34 remaining = rule[pos:] 35 if '>' in remaining or '<' in remaining: 36 raise ValueError('malformed url rule: %r' % rule) 37 yield (None, None, remaining) 38 39 40 def get_converter(map, name, args): 41 if name not in map.converters: 42 raise LookupError('the converter %r does not exist' % name) 43 if args: 44 storage = type('_Storage', (), {'__getitem__': lambda s, x: x})() 45 args, kwargs = eval(u'(lambda *a, **kw: (a, kw))(%s)' % args, {}, storage) 46 else: 47 args = () 48 kwargs = {} 49 return map.converters[name](map, *args, **kwargs) 50 51 52 class RoutingException(Exception): 53 pass 54 55 56 class RequestRedirect(HTTPException, RoutingException): 57 code = 301 58 59 def __init__(self, new_url): 60 RoutingException.__init__(self, new_url) 61 self.new_url = new_url 62 63 def get_response(self, environ): 64 return redirect(self.new_url, 301) 65 66 67 class RequestSlash(RoutingException): 68 pass 69 70 71 class BuildError(RoutingException, LookupError): 72 73 def __init__(self, endpoint, values, method): 74 LookupError.__init__(self, endpoint, values, method) 75 self.endpoint = endpoint 76 self.values = values 77 self.method = method 78 79 80 class ValidationError(ValueError): 81 pass 82 83 84 class RuleFactory(object): 85 86 def get_rules(self, map): 87 raise NotImplementedError() 88 89 90 class Subdomain(RuleFactory): 91 92 def __init__(self, subdomain, rules): 93 self.subdomain = subdomain 94 self.rules = rules 95 96 def get_rules(self, map): 97 for rulefactory in self.rules: 98 for rule in rulefactory.get_rules(map): 99 rule = rule.empty() 100 rule.subdomain = self.subdomain 101 yield rule 102 103 104 class Submount(RuleFactory): 105 106 def __init__(self, path, rules): 107 self.path = path.rstrip('/') 108 self.rules = rules 109 110 def get_rules(self, map): 111 for rulefactory in self.rules: 112 for rule in rulefactory.get_rules(map): 113 rule = rule.empty() 114 rule.rule = self.path + rule.rule 115 yield rule 116 117 118 class EndpointPrefix(RuleFactory): 119 120 def __init__(self, prefix, rules): 121 self.prefix = prefix 122 self.rules = rules 123 124 def get_rules(self, map): 125 for rulefactory in self.rules: 126 for rule in rulefactory.get_rules(map): 127 rule = rule.empty() 128 rule.endpoint = self.prefix + rule.endpoint 129 yield rule 130 131 132 class RuleTemplate(object): 133 134 def __init__(self, rules): 135 self.rules = list(rules) 136 137 def __call__(self, *args, **kwargs): 138 return RuleTemplateFactory(self.rules, dict(*args, **kwargs)) 139 140 141 class RuleTemplateFactory(RuleFactory): 142 143 def __init__(self, rules, context): 144 self.rules = rules 145 self.context = context 146 147 def get_rules(self, map): 148 for rulefactory in self.rules: 149 for rule in rulefactory.get_rules(map): 150 new_defaults = subdomain = None 151 if rule.defaults is not None: 152 new_defaults = {} 153 for key, value in rule.defaults.iteritems(): 154 if isinstance(value, basestring): 155 value = format_string(value, self.context) 156 new_defaults[key] = value 157 158 if rule.subdomain is not None: 159 subdomain = format_string(rule.subdomain, self.context) 160 new_endpoint = rule.endpoint 161 if isinstance(new_endpoint, basestring): 162 new_endpoint = format_string(new_endpoint, self.context) 163 yield Rule(format_string(rule.rule, self.context), new_defaults, subdomain, rule.methods, rule.build_only, new_endpoint, rule.strict_slashes) 164 165 166 class Rule(RuleFactory): 167 168 def __init__(self, string, defaults = None, subdomain = None, methods = None, build_only = False, endpoint = None, strict_slashes = None, redirect_to = None): 169 if not string.startswith('/'): 170 raise ValueError('urls must start with a leading slash') 171 self.rule = string 172 self.is_leaf = not string.endswith('/') 173 self.map = None 174 self.strict_slashes = strict_slashes 175 self.subdomain = subdomain 176 self.defaults = defaults 177 self.build_only = build_only 178 if methods is None: 179 self.methods = None 180 else: 181 self.methods = set([ x.upper() for x in methods ]) 182 if 'HEAD' not in self.methods and 'GET' in self.methods: 183 self.methods.add('HEAD') 184 self.endpoint = endpoint 185 self.greediness = 0 186 self.redirect_to = redirect_to 187 if defaults is not None: 188 self.arguments = set(map(str, defaults)) 189 else: 190 self.arguments = set() 191 self._trace = self._converters = self._regex = self._weights = None 192 193 def empty(self): 194 defaults = None 195 if self.defaults is not None: 196 defaults = dict(self.defaults) 197 return Rule(self.rule, defaults, self.subdomain, self.methods, self.build_only, self.endpoint, self.strict_slashes, self.redirect_to) 198 199 def get_rules(self, map): 200 yield self 201 202 def refresh(self): 203 self.bind(self.map, rebind=True) 204 205 def bind(self, map, rebind = False): 206 if self.map is not None and not rebind: 207 raise RuntimeError('url rule %r already bound to map %r' % (self, self.map)) 208 self.map = map 209 if self.strict_slashes is None: 210 self.strict_slashes = map.strict_slashes 211 if self.subdomain is None: 212 self.subdomain = map.default_subdomain 213 rule = self.subdomain + '|' + (self.is_leaf and self.rule or self.rule.rstrip('/')) 214 self._trace = [] 215 self._converters = {} 216 self._weights = [] 217 regex_parts = [] 218 for converter, arguments, variable in parse_rule(rule): 219 if converter is None: 220 regex_parts.append(re.escape(variable)) 221 self._trace.append((False, variable)) 222 self._weights.append(len(variable)) 223 else: 224 convobj = get_converter(map, converter, arguments) 225 regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex)) 226 self._converters[variable] = convobj 227 self._trace.append((True, variable)) 228 self._weights.append(convobj.weight) 229 self.arguments.add(str(variable)) 230 if convobj.is_greedy: 231 self.greediness += 1 232 233 if not self.is_leaf: 234 self._trace.append((False, '/')) 235 if not self.build_only: 236 regex = '^%s%s$' % (u''.join(regex_parts), (not self.is_leaf or not self.strict_slashes) and '(?<!/)(?P<__suffix__>/?)' or '') 237 self._regex = re.compile(regex, re.UNICODE) 238 239 def match(self, path): 240 if not self.build_only: 241 m = self._regex.search(path) 242 if m is not None: 243 groups = m.groupdict() 244 if self.strict_slashes and not self.is_leaf and not groups.pop('__suffix__'): 245 raise RequestSlash() 246 elif not self.strict_slashes: 247 del groups['__suffix__'] 248 result = {} 249 for name, value in groups.iteritems(): 250 try: 251 value = self._converters[name].to_python(value) 252 except ValidationError: 253 return 254 255 result[str(name)] = value 256 257 if self.defaults is not None: 258 result.update(self.defaults) 259 return result 260 261 def build(self, values, append_unknown = True): 262 tmp = [] 263 add = tmp.append 264 processed = set(self.arguments) 265 for is_dynamic, data in self._trace: 266 if is_dynamic: 267 try: 268 add(self._converters[data].to_url(values[data])) 269 except ValidationError: 270 return 271 272 processed.add(data) 273 else: 274 add(data) 275 276 subdomain, url = u''.join(tmp).split('|', 1) 277 if append_unknown: 278 query_vars = MultiDict(values) 279 for key in processed: 280 if key in query_vars: 281 del query_vars[key] 282 283 if query_vars: 284 url += '?' + url_encode(query_vars, self.map.charset, sort=self.map.sort_parameters, key=self.map.sort_key) 285 return (subdomain, url) 286 287 def provides_defaults_for(self, rule): 288 return not self.build_only and self.defaults is not None and self.endpoint == rule.endpoint and self != rule and self.arguments == rule.arguments 289 290 def suitable_for(self, values, method = None): 291 if method is not None: 292 if self.methods is not None and method not in self.methods: 293 return False 294 valueset = set(values) 295 for key in self.arguments - set(self.defaults or ()): 296 if key not in values: 297 return False 298 299 if self.arguments.issubset(valueset): 300 if self.defaults is None: 301 return True 302 for key, value in self.defaults.iteritems(): 303 if value != values[key]: 304 return False 305 306 return True 307 308 def match_compare(self, other): 309 for sw, ow in izip(self._weights, other._weights): 310 if sw > ow: 311 return -1 312 if sw < ow: 313 return 1 314 315 if len(self._weights) > len(other._weights): 316 return -1 317 if len(self._weights) < len(other._weights): 318 return 1 319 if not other.arguments and self.arguments: 320 return 1 321 if other.arguments and not self.arguments: 322 return -1 323 if other.defaults is None and self.defaults is not None: 324 return 1 325 if other.defaults is not None and self.defaults is None: 326 return -1 327 if self.greediness > other.greediness: 328 return -1 329 if self.greediness < other.greediness: 330 return 1 331 if len(self.arguments) > len(other.arguments): 332 return 1 333 if len(self.arguments) < len(other.arguments): 334 return -1 335 return 1 336 337 def build_compare(self, other): 338 if not other.arguments and self.arguments: 339 return -1 340 if other.arguments and not self.arguments: 341 return 1 342 if other.defaults is None and self.defaults is not None: 343 return -1 344 if other.defaults is not None and self.defaults is None: 345 return 1 346 if self.provides_defaults_for(other): 347 return -1 348 if other.provides_defaults_for(self): 349 return 1 350 if self.greediness > other.greediness: 351 return -1 352 if self.greediness < other.greediness: 353 return 1 354 if len(self.arguments) > len(other.arguments): 355 return -1 356 if len(self.arguments) < len(other.arguments): 357 return 1 358 return -1 359 360 def __eq__(self, other): 361 return self.__class__ is other.__class__ and self._trace == other._trace 362 363 def __ne__(self, other): 364 return not self.__eq__(other) 365 366 def __unicode__(self): 367 return self.rule 368 369 def __str__(self): 370 charset = self.map is not None and self.map.charset or 'utf-8' 371 return unicode(self).encode(charset) 372 373 def __repr__(self): 374 if self.map is None: 375 return '<%s (unbound)>' % self.__class__.__name__ 376 charset = self.map is not None and self.map.charset or 'utf-8' 377 tmp = [] 378 for is_dynamic, data in self._trace: 379 if is_dynamic: 380 tmp.append('<%s>' % data) 381 else: 382 tmp.append(data) 383 384 return '<%s %r%s -> %s>' % (self.__class__.__name__, 385 u''.join(tmp).encode(charset).lstrip('|'), 386 self.methods is not None and ' (%s)' % ', '.join(self.methods) or '', 387 self.endpoint) 388 389 390 class BaseConverter(object): 391 regex = '[^/]+' 392 is_greedy = False 393 weight = 100 394 395 def __init__(self, map): 396 self.map = map 397 398 def to_python(self, value): 399 return value 400 401 def to_url(self, value): 402 return url_quote(value, self.map.charset) 403 404 405 class UnicodeConverter(BaseConverter): 406 407 def __init__(self, map, minlength = 1, maxlength = None, length = None): 408 BaseConverter.__init__(self, map) 409 if length is not None: 410 length = '{%d}' % int(length) 411 else: 412 if maxlength is None: 413 maxlength = '' 414 else: 415 maxlength = int(maxlength) 416 length = '{%s,%s}' % (int(minlength), maxlength) 417 self.regex = '[^/]' + length 418 419 420 class AnyConverter(BaseConverter): 421 422 def __init__(self, map, *items): 423 BaseConverter.__init__(self, map) 424 self.regex = '(?:%s)' % '|'.join([ re.escape(x) for x in items ]) 425 426 427 class PathConverter(BaseConverter): 428 regex = '[^/].*?' 429 is_greedy = True 430 weight = 50 431 432 433 class NumberConverter(BaseConverter): 434 435 def __init__(self, map, fixed_digits = 0, min = None, max = None): 436 BaseConverter.__init__(self, map) 437 self.fixed_digits = fixed_digits 438 self.min = min 439 self.max = max 440 441 def to_python(self, value): 442 if self.fixed_digits and len(value) != self.fixed_digits: 443 raise ValidationError() 444 value = self.num_convert(value) 445 if self.min is not None and value < self.min or self.max is not None and value > self.max: 446 raise ValidationError() 447 return value 448 449 def to_url(self, value): 450 value = self.num_convert(value) 451 if self.fixed_digits: 452 value = '%%0%sd' % self.fixed_digits % value 453 return str(value) 454 455 456 class IntegerConverter(NumberConverter): 457 regex = '\\d+' 458 num_convert = int 459 460 461 class FloatConverter(NumberConverter): 462 regex = '\\d+\\.\\d+' 463 num_convert = float 464 465 def __init__(self, map, min = None, max = None): 466 NumberConverter.__init__(self, map, 0, min, max) 467 468 469 class Map(object): 470 default_converters = None 471 472 def __init__(self, rules = None, default_subdomain = '', charset = 'utf-8', strict_slashes = True, redirect_defaults = True, converters = None, sort_parameters = False, sort_key = None): 473 self._rules = [] 474 self._rules_by_endpoint = {} 475 self._remap = True 476 self.default_subdomain = default_subdomain 477 self.charset = charset 478 self.strict_slashes = strict_slashes 479 self.redirect_defaults = redirect_defaults 480 self.converters = self.default_converters.copy() 481 if converters: 482 self.converters.update(converters) 483 self.sort_parameters = sort_parameters 484 self.sort_key = sort_key 485 for rulefactory in rules or (): 486 self.add(rulefactory) 487 488 def is_endpoint_expecting(self, endpoint, *arguments): 489 self.update() 490 arguments = set(arguments) 491 for rule in self._rules_by_endpoint[endpoint]: 492 if arguments.issubset(rule.arguments): 493 return True 494 495 return False 496 497 def iter_rules(self, endpoint = None): 498 if endpoint is not None: 499 return iter(self._rules_by_endpoint[endpoint]) 500 return iter(self._rules) 501 502 def add(self, rulefactory): 503 for rule in rulefactory.get_rules(self): 504 rule.bind(self) 505 self._rules.append(rule) 506 self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule) 507 508 self._remap = True 509 510 def bind(self, server_name, script_name = None, subdomain = None, url_scheme = 'http', default_method = 'GET', path_info = None): 511 if subdomain is None: 512 subdomain = self.default_subdomain 513 if script_name is None: 514 script_name = '/' 515 return MapAdapter(self, server_name, script_name, subdomain, url_scheme, path_info, default_method) 516 517 def bind_to_environ(self, environ, server_name = None, subdomain = None): 518 environ = _get_environ(environ) 519 if server_name is None: 520 if 'HTTP_HOST' in environ: 521 server_name = environ['HTTP_HOST'] 522 else: 523 server_name = environ['SERVER_NAME'] 524 if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not in (('https', '443'), ('http', '80')): 525 server_name += ':' + environ['SERVER_PORT'] 526 elif subdomain is None: 527 wsgi_server_name = environ.get('HTTP_HOST', environ['SERVER_NAME']) 528 cur_server_name = wsgi_server_name.split(':', 1)[0].split('.') 529 real_server_name = server_name.split(':', 1)[0].split('.') 530 offset = -len(real_server_name) 531 if cur_server_name[offset:] != real_server_name: 532 raise ValueError('the server name provided (%r) does not match the server name from the WSGI environment (%r)' % (server_name, wsgi_server_name)) 533 subdomain = '.'.join(filter(None, cur_server_name[:offset])) 534 return Map.bind(self, server_name, environ.get('SCRIPT_NAME'), subdomain, environ['wsgi.url_scheme'], environ['REQUEST_METHOD'], environ.get('PATH_INFO')) 535 536 def update(self): 537 if self._remap: 538 self._rules.sort(lambda a, b: a.match_compare(b)) 539 for rules in self._rules_by_endpoint.itervalues(): 540 rules.sort(lambda a, b: a.build_compare(b)) 541 542 self._remap = False 543 544 def __repr__(self): 545 rules = self.iter_rules() 546 return '%s([%s])' % (self.__class__.__name__, pformat(list(rules))) 547 548 549 class MapAdapter(object): 550 551 def __init__(self, map, server_name, script_name, subdomain, url_scheme, path_info, default_method): 552 self.map = map 553 self.server_name = server_name 554 if not script_name.endswith('/'): 555 script_name += '/' 556 self.script_name = script_name 557 self.subdomain = subdomain 558 self.url_scheme = url_scheme 559 self.path_info = path_info or u'' 560 self.default_method = default_method 561 562 def dispatch(self, view_func, path_info = None, method = None, catch_http_exceptions = False): 563 try: 564 try: 565 endpoint, args = self.match(path_info, method) 566 except RequestRedirect as e: 567 return e 568 569 return view_func(endpoint, args) 570 except HTTPException as e: 571 if catch_http_exceptions: 572 return e 573 raise 574 575 def match(self, path_info = None, method = None, return_rule = False): 576 self.map.update() 577 if path_info is None: 578 path_info = self.path_info 579 if not isinstance(path_info, unicode): 580 path_info = path_info.decode(self.map.charset, 'ignore') 581 method = (method or self.default_method).upper() 582 path = u'%s|/%s' % (self.subdomain, path_info.lstrip('/')) 583 have_match_for = set() 584 for rule in self.map._rules: 585 try: 586 rv = rule.match(path) 587 except RequestSlash: 588 raise RequestRedirect(str('%s://%s%s%s/%s/' % (self.url_scheme, 589 self.subdomain and self.subdomain + '.' or '', 590 self.server_name, 591 self.script_name[:-1], 592 url_quote(path_info.lstrip('/'), self.map.charset)))) 593 594 if rv is None: 595 continue 596 if rule.methods is not None and method not in rule.methods: 597 have_match_for.update(rule.methods) 598 continue 599 if self.map.redirect_defaults: 600 for r in self.map._rules_by_endpoint[rule.endpoint]: 601 if r.provides_defaults_for(rule) and r.suitable_for(rv, method): 602 rv.update(r.defaults) 603 subdomain, path = r.build(rv) 604 raise RequestRedirect(str('%s://%s%s%s/%s' % (self.url_scheme, 605 subdomain and subdomain + '.' or '', 606 self.server_name, 607 self.script_name[:-1], 608 url_quote(path.lstrip('/'), self.map.charset)))) 609 610 if rule.redirect_to is not None: 611 if isinstance(rule.redirect_to, basestring): 612 613 def _handle_match(match): 614 value = rv[match.group(1)] 615 return rule._converters[match.group(1)].to_url(value) 616 617 redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to) 618 else: 619 redirect_url = rule.redirect_to(self, **rv) 620 raise RequestRedirect(str(urljoin('%s://%s%s%s' % (self.url_scheme, 621 self.subdomain and self.subdomain + '.' or '', 622 self.server_name, 623 self.script_name), redirect_url))) 624 if return_rule: 625 return (rule, rv) 626 return (rule.endpoint, rv) 627 628 if have_match_for: 629 raise MethodNotAllowed(valid_methods=list(have_match_for)) 630 raise NotFound() 631 632 def test(self, path_info = None, method = None): 633 try: 634 self.match(path_info, method) 635 except RequestRedirect: 636 pass 637 except NotFound: 638 return False 639 640 return True 641 642 def _partial_build(self, endpoint, values, method, append_unknown): 643 if method is None: 644 rv = self._partial_build(endpoint, values, self.default_method, append_unknown) 645 if rv is not None: 646 return rv 647 for rule in self.map._rules_by_endpoint.get(endpoint, ()): 648 if rule.suitable_for(values, method): 649 rv = rule.build(values, append_unknown) 650 if rv is not None: 651 return rv 652 653 def build(self, endpoint, values = None, method = None, force_external = False, append_unknown = True): 654 self.map.update() 655 if values: 656 if isinstance(values, MultiDict): 657 values = dict(((k, v) for k, v in values.iteritems(multi=True) if v is not None)) 658 else: 659 values = dict(((k, v) for k, v in values.iteritems() if v is not None)) 660 else: 661 values = {} 662 rv = self._partial_build(endpoint, values, method, append_unknown) 663 if rv is None: 664 raise BuildError(endpoint, values, method) 665 subdomain, path = rv 666 if not force_external and subdomain == self.subdomain: 667 return str(urljoin(self.script_name, path.lstrip('/'))) 668 return str('%s://%s%s%s/%s' % (self.url_scheme, 669 subdomain and subdomain + '.' or '', 670 self.server_name, 671 self.script_name[:-1], 672 path.lstrip('/'))) 673 674 675 DEFAULT_CONVERTERS = {'default': UnicodeConverter, 676 'string': UnicodeConverter, 677 'any': AnyConverter, 678 'path': PathConverter, 679 'int': IntegerConverter, 680 'float': FloatConverter} 681 from werkzeug.datastructures import ImmutableDict, MultiDict 682 Map.default_converters = ImmutableDict(DEFAULT_CONVERTERS)