Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/werkzeug/routing/rules.py: 69%

378 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 06:08 +0000

1from __future__ import annotations 

2 

3import ast 

4import re 

5import typing as t 

6from dataclasses import dataclass 

7from string import Template 

8from types import CodeType 

9from urllib.parse import quote 

10 

11from ..datastructures import iter_multi_items 

12from ..urls import _urlencode 

13from .converters import ValidationError 

14 

15if t.TYPE_CHECKING: 

16 from .converters import BaseConverter 

17 from .map import Map 

18 

19 

20class Weighting(t.NamedTuple): 

21 number_static_weights: int 

22 static_weights: list[tuple[int, int]] 

23 number_argument_weights: int 

24 argument_weights: list[int] 

25 

26 

27@dataclass 

28class RulePart: 

29 """A part of a rule. 

30 

31 Rules can be represented by parts as delimited by `/` with 

32 instances of this class representing those parts. The *content* is 

33 either the raw content if *static* or a regex string to match 

34 against. The *weight* can be used to order parts when matching. 

35 

36 """ 

37 

38 content: str 

39 final: bool 

40 static: bool 

41 suffixed: bool 

42 weight: Weighting 

43 

44 

45_part_re = re.compile( 

46 r""" 

47 (?: 

48 (?P<slash>/) # a slash 

49 | 

50 (?P<static>[^</]+) # static rule data 

51 | 

52 (?: 

53 < 

54 (?: 

55 (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name 

56 (?:\((?P<arguments>.*?)\))? # converter arguments 

57 : # variable delimiter 

58 )? 

59 (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name 

60 > 

61 ) 

62 ) 

63 """, 

64 re.VERBOSE, 

65) 

66 

67_simple_rule_re = re.compile(r"<([^>]+)>") 

68_converter_args_re = re.compile( 

69 r""" 

70 ((?P<name>\w+)\s*=\s*)? 

71 (?P<value> 

72 True|False| 

73 \d+.\d+| 

74 \d+.| 

75 \d+| 

76 [\w\d_.]+| 

77 [urUR]?(?P<stringval>"[^"]*?"|'[^']*') 

78 )\s*, 

79 """, 

80 re.VERBOSE, 

81) 

82 

83 

84_PYTHON_CONSTANTS = {"None": None, "True": True, "False": False} 

85 

86 

87def _find(value: str, target: str, pos: int) -> int: 

88 """Find the *target* in *value* after *pos*. 

89 

90 Returns the *value* length if *target* isn't found. 

91 """ 

92 try: 

93 return value.index(target, pos) 

94 except ValueError: 

95 return len(value) 

96 

97 

98def _pythonize(value: str) -> None | bool | int | float | str: 

99 if value in _PYTHON_CONSTANTS: 

100 return _PYTHON_CONSTANTS[value] 

101 for convert in int, float: 

102 try: 

103 return convert(value) # type: ignore 

104 except ValueError: 

105 pass 

106 if value[:1] == value[-1:] and value[0] in "\"'": 

107 value = value[1:-1] 

108 return str(value) 

109 

110 

111def parse_converter_args(argstr: str) -> tuple[t.Tuple, dict[str, t.Any]]: 

112 argstr += "," 

113 args = [] 

114 kwargs = {} 

115 

116 for item in _converter_args_re.finditer(argstr): 

117 value = item.group("stringval") 

118 if value is None: 

119 value = item.group("value") 

120 value = _pythonize(value) 

121 if not item.group("name"): 

122 args.append(value) 

123 else: 

124 name = item.group("name") 

125 kwargs[name] = value 

126 

127 return tuple(args), kwargs 

128 

129 

130class RuleFactory: 

131 """As soon as you have more complex URL setups it's a good idea to use rule 

132 factories to avoid repetitive tasks. Some of them are builtin, others can 

133 be added by subclassing `RuleFactory` and overriding `get_rules`. 

134 """ 

135 

136 def get_rules(self, map: Map) -> t.Iterable[Rule]: 

137 """Subclasses of `RuleFactory` have to override this method and return 

138 an iterable of rules.""" 

139 raise NotImplementedError() 

140 

141 

142class Subdomain(RuleFactory): 

143 """All URLs provided by this factory have the subdomain set to a 

144 specific domain. For example if you want to use the subdomain for 

145 the current language this can be a good setup:: 

146 

147 url_map = Map([ 

148 Rule('/', endpoint='#select_language'), 

149 Subdomain('<string(length=2):lang_code>', [ 

150 Rule('/', endpoint='index'), 

151 Rule('/about', endpoint='about'), 

152 Rule('/help', endpoint='help') 

153 ]) 

154 ]) 

155 

156 All the rules except for the ``'#select_language'`` endpoint will now 

157 listen on a two letter long subdomain that holds the language code 

158 for the current request. 

159 """ 

160 

161 def __init__(self, subdomain: str, rules: t.Iterable[RuleFactory]) -> None: 

162 self.subdomain = subdomain 

163 self.rules = rules 

164 

165 def get_rules(self, map: Map) -> t.Iterator[Rule]: 

166 for rulefactory in self.rules: 

167 for rule in rulefactory.get_rules(map): 

168 rule = rule.empty() 

169 rule.subdomain = self.subdomain 

170 yield rule 

171 

172 

173class Submount(RuleFactory): 

174 """Like `Subdomain` but prefixes the URL rule with a given string:: 

175 

176 url_map = Map([ 

177 Rule('/', endpoint='index'), 

178 Submount('/blog', [ 

179 Rule('/', endpoint='blog/index'), 

180 Rule('/entry/<entry_slug>', endpoint='blog/show') 

181 ]) 

182 ]) 

183 

184 Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``. 

185 """ 

186 

187 def __init__(self, path: str, rules: t.Iterable[RuleFactory]) -> None: 

188 self.path = path.rstrip("/") 

189 self.rules = rules 

190 

191 def get_rules(self, map: Map) -> t.Iterator[Rule]: 

192 for rulefactory in self.rules: 

193 for rule in rulefactory.get_rules(map): 

194 rule = rule.empty() 

195 rule.rule = self.path + rule.rule 

196 yield rule 

197 

198 

199class EndpointPrefix(RuleFactory): 

200 """Prefixes all endpoints (which must be strings for this factory) with 

201 another string. This can be useful for sub applications:: 

202 

203 url_map = Map([ 

204 Rule('/', endpoint='index'), 

205 EndpointPrefix('blog/', [Submount('/blog', [ 

206 Rule('/', endpoint='index'), 

207 Rule('/entry/<entry_slug>', endpoint='show') 

208 ])]) 

209 ]) 

210 """ 

211 

212 def __init__(self, prefix: str, rules: t.Iterable[RuleFactory]) -> None: 

213 self.prefix = prefix 

214 self.rules = rules 

215 

216 def get_rules(self, map: Map) -> t.Iterator[Rule]: 

217 for rulefactory in self.rules: 

218 for rule in rulefactory.get_rules(map): 

219 rule = rule.empty() 

220 rule.endpoint = self.prefix + rule.endpoint 

221 yield rule 

222 

223 

224class RuleTemplate: 

225 """Returns copies of the rules wrapped and expands string templates in 

226 the endpoint, rule, defaults or subdomain sections. 

227 

228 Here a small example for such a rule template:: 

229 

230 from werkzeug.routing import Map, Rule, RuleTemplate 

231 

232 resource = RuleTemplate([ 

233 Rule('/$name/', endpoint='$name.list'), 

234 Rule('/$name/<int:id>', endpoint='$name.show') 

235 ]) 

236 

237 url_map = Map([resource(name='user'), resource(name='page')]) 

238 

239 When a rule template is called the keyword arguments are used to 

240 replace the placeholders in all the string parameters. 

241 """ 

242 

243 def __init__(self, rules: t.Iterable[Rule]) -> None: 

244 self.rules = list(rules) 

245 

246 def __call__(self, *args: t.Any, **kwargs: t.Any) -> RuleTemplateFactory: 

247 return RuleTemplateFactory(self.rules, dict(*args, **kwargs)) 

248 

249 

250class RuleTemplateFactory(RuleFactory): 

251 """A factory that fills in template variables into rules. Used by 

252 `RuleTemplate` internally. 

253 

254 :internal: 

255 """ 

256 

257 def __init__( 

258 self, rules: t.Iterable[RuleFactory], context: dict[str, t.Any] 

259 ) -> None: 

260 self.rules = rules 

261 self.context = context 

262 

263 def get_rules(self, map: Map) -> t.Iterator[Rule]: 

264 for rulefactory in self.rules: 

265 for rule in rulefactory.get_rules(map): 

266 new_defaults = subdomain = None 

267 if rule.defaults: 

268 new_defaults = {} 

269 for key, value in rule.defaults.items(): 

270 if isinstance(value, str): 

271 value = Template(value).substitute(self.context) 

272 new_defaults[key] = value 

273 if rule.subdomain is not None: 

274 subdomain = Template(rule.subdomain).substitute(self.context) 

275 new_endpoint = rule.endpoint 

276 if isinstance(new_endpoint, str): 

277 new_endpoint = Template(new_endpoint).substitute(self.context) 

278 yield Rule( 

279 Template(rule.rule).substitute(self.context), 

280 new_defaults, 

281 subdomain, 

282 rule.methods, 

283 rule.build_only, 

284 new_endpoint, 

285 rule.strict_slashes, 

286 ) 

287 

288 

289def _prefix_names(src: str) -> ast.stmt: 

290 """ast parse and prefix names with `.` to avoid collision with user vars""" 

291 tree = ast.parse(src).body[0] 

292 if isinstance(tree, ast.Expr): 

293 tree = tree.value # type: ignore 

294 for node in ast.walk(tree): 

295 if isinstance(node, ast.Name): 

296 node.id = f".{node.id}" 

297 return tree 

298 

299 

300_CALL_CONVERTER_CODE_FMT = "self._converters[{elem!r}].to_url()" 

301_IF_KWARGS_URL_ENCODE_CODE = """\ 

302if kwargs: 

303 params = self._encode_query_vars(kwargs) 

304 q = "?" if params else "" 

305else: 

306 q = params = "" 

307""" 

308_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE) 

309_URL_ENCODE_AST_NAMES = (_prefix_names("q"), _prefix_names("params")) 

310 

311 

312class Rule(RuleFactory): 

313 """A Rule represents one URL pattern. There are some options for `Rule` 

314 that change the way it behaves and are passed to the `Rule` constructor. 

315 Note that besides the rule-string all arguments *must* be keyword arguments 

316 in order to not break the application on Werkzeug upgrades. 

317 

318 `string` 

319 Rule strings basically are just normal URL paths with placeholders in 

320 the format ``<converter(arguments):name>`` where the converter and the 

321 arguments are optional. If no converter is defined the `default` 

322 converter is used which means `string` in the normal configuration. 

323 

324 URL rules that end with a slash are branch URLs, others are leaves. 

325 If you have `strict_slashes` enabled (which is the default), all 

326 branch URLs that are matched without a trailing slash will trigger a 

327 redirect to the same URL with the missing slash appended. 

328 

329 The converters are defined on the `Map`. 

330 

331 `endpoint` 

332 The endpoint for this rule. This can be anything. A reference to a 

333 function, a string, a number etc. The preferred way is using a string 

334 because the endpoint is used for URL generation. 

335 

336 `defaults` 

337 An optional dict with defaults for other rules with the same endpoint. 

338 This is a bit tricky but useful if you want to have unique URLs:: 

339 

340 url_map = Map([ 

341 Rule('/all/', defaults={'page': 1}, endpoint='all_entries'), 

342 Rule('/all/page/<int:page>', endpoint='all_entries') 

343 ]) 

344 

345 If a user now visits ``http://example.com/all/page/1`` they will be 

346 redirected to ``http://example.com/all/``. If `redirect_defaults` is 

347 disabled on the `Map` instance this will only affect the URL 

348 generation. 

349 

350 `subdomain` 

351 The subdomain rule string for this rule. If not specified the rule 

352 only matches for the `default_subdomain` of the map. If the map is 

353 not bound to a subdomain this feature is disabled. 

354 

355 Can be useful if you want to have user profiles on different subdomains 

356 and all subdomains are forwarded to your application:: 

357 

358 url_map = Map([ 

359 Rule('/', subdomain='<username>', endpoint='user/homepage'), 

360 Rule('/stats', subdomain='<username>', endpoint='user/stats') 

361 ]) 

362 

363 `methods` 

364 A sequence of http methods this rule applies to. If not specified, all 

365 methods are allowed. For example this can be useful if you want different 

366 endpoints for `POST` and `GET`. If methods are defined and the path 

367 matches but the method matched against is not in this list or in the 

368 list of another rule for that path the error raised is of the type 

369 `MethodNotAllowed` rather than `NotFound`. If `GET` is present in the 

370 list of methods and `HEAD` is not, `HEAD` is added automatically. 

371 

372 `strict_slashes` 

373 Override the `Map` setting for `strict_slashes` only for this rule. If 

374 not specified the `Map` setting is used. 

375 

376 `merge_slashes` 

377 Override :attr:`Map.merge_slashes` for this rule. 

378 

379 `build_only` 

380 Set this to True and the rule will never match but will create a URL 

381 that can be build. This is useful if you have resources on a subdomain 

382 or folder that are not handled by the WSGI application (like static data) 

383 

384 `redirect_to` 

385 If given this must be either a string or callable. In case of a 

386 callable it's called with the url adapter that triggered the match and 

387 the values of the URL as keyword arguments and has to return the target 

388 for the redirect, otherwise it has to be a string with placeholders in 

389 rule syntax:: 

390 

391 def foo_with_slug(adapter, id): 

392 # ask the database for the slug for the old id. this of 

393 # course has nothing to do with werkzeug. 

394 return f'foo/{Foo.get_slug_for_id(id)}' 

395 

396 url_map = Map([ 

397 Rule('/foo/<slug>', endpoint='foo'), 

398 Rule('/some/old/url/<slug>', redirect_to='foo/<slug>'), 

399 Rule('/other/old/url/<int:id>', redirect_to=foo_with_slug) 

400 ]) 

401 

402 When the rule is matched the routing system will raise a 

403 `RequestRedirect` exception with the target for the redirect. 

404 

405 Keep in mind that the URL will be joined against the URL root of the 

406 script so don't use a leading slash on the target URL unless you 

407 really mean root of that domain. 

408 

409 `alias` 

410 If enabled this rule serves as an alias for another rule with the same 

411 endpoint and arguments. 

412 

413 `host` 

414 If provided and the URL map has host matching enabled this can be 

415 used to provide a match rule for the whole host. This also means 

416 that the subdomain feature is disabled. 

417 

418 `websocket` 

419 If ``True``, this rule is only matches for WebSocket (``ws://``, 

420 ``wss://``) requests. By default, rules will only match for HTTP 

421 requests. 

422 

423 .. versionchanged:: 2.1 

424 Percent-encoded newlines (``%0a``), which are decoded by WSGI 

425 servers, are considered when routing instead of terminating the 

426 match early. 

427 

428 .. versionadded:: 1.0 

429 Added ``websocket``. 

430 

431 .. versionadded:: 1.0 

432 Added ``merge_slashes``. 

433 

434 .. versionadded:: 0.7 

435 Added ``alias`` and ``host``. 

436 

437 .. versionchanged:: 0.6.1 

438 ``HEAD`` is added to ``methods`` if ``GET`` is present. 

439 """ 

440 

441 def __init__( 

442 self, 

443 string: str, 

444 defaults: t.Mapping[str, t.Any] | None = None, 

445 subdomain: str | None = None, 

446 methods: t.Iterable[str] | None = None, 

447 build_only: bool = False, 

448 endpoint: str | None = None, 

449 strict_slashes: bool | None = None, 

450 merge_slashes: bool | None = None, 

451 redirect_to: str | t.Callable[..., str] | None = None, 

452 alias: bool = False, 

453 host: str | None = None, 

454 websocket: bool = False, 

455 ) -> None: 

456 if not string.startswith("/"): 

457 raise ValueError("urls must start with a leading slash") 

458 self.rule = string 

459 self.is_leaf = not string.endswith("/") 

460 self.is_branch = string.endswith("/") 

461 

462 self.map: Map = None # type: ignore 

463 self.strict_slashes = strict_slashes 

464 self.merge_slashes = merge_slashes 

465 self.subdomain = subdomain 

466 self.host = host 

467 self.defaults = defaults 

468 self.build_only = build_only 

469 self.alias = alias 

470 self.websocket = websocket 

471 

472 if methods is not None: 

473 if isinstance(methods, str): 

474 raise TypeError("'methods' should be a list of strings.") 

475 

476 methods = {x.upper() for x in methods} 

477 

478 if "HEAD" not in methods and "GET" in methods: 

479 methods.add("HEAD") 

480 

481 if websocket and methods - {"GET", "HEAD", "OPTIONS"}: 

482 raise ValueError( 

483 "WebSocket rules can only use 'GET', 'HEAD', and 'OPTIONS' methods." 

484 ) 

485 

486 self.methods = methods 

487 self.endpoint: str = endpoint # type: ignore 

488 self.redirect_to = redirect_to 

489 

490 if defaults: 

491 self.arguments = set(map(str, defaults)) 

492 else: 

493 self.arguments = set() 

494 

495 self._converters: dict[str, BaseConverter] = {} 

496 self._trace: list[tuple[bool, str]] = [] 

497 self._parts: list[RulePart] = [] 

498 

499 def empty(self) -> Rule: 

500 """ 

501 Return an unbound copy of this rule. 

502 

503 This can be useful if want to reuse an already bound URL for another 

504 map. See ``get_empty_kwargs`` to override what keyword arguments are 

505 provided to the new copy. 

506 """ 

507 return type(self)(self.rule, **self.get_empty_kwargs()) 

508 

509 def get_empty_kwargs(self) -> t.Mapping[str, t.Any]: 

510 """ 

511 Provides kwargs for instantiating empty copy with empty() 

512 

513 Use this method to provide custom keyword arguments to the subclass of 

514 ``Rule`` when calling ``some_rule.empty()``. Helpful when the subclass 

515 has custom keyword arguments that are needed at instantiation. 

516 

517 Must return a ``dict`` that will be provided as kwargs to the new 

518 instance of ``Rule``, following the initial ``self.rule`` value which 

519 is always provided as the first, required positional argument. 

520 """ 

521 defaults = None 

522 if self.defaults: 

523 defaults = dict(self.defaults) 

524 return dict( 

525 defaults=defaults, 

526 subdomain=self.subdomain, 

527 methods=self.methods, 

528 build_only=self.build_only, 

529 endpoint=self.endpoint, 

530 strict_slashes=self.strict_slashes, 

531 redirect_to=self.redirect_to, 

532 alias=self.alias, 

533 host=self.host, 

534 ) 

535 

536 def get_rules(self, map: Map) -> t.Iterator[Rule]: 

537 yield self 

538 

539 def refresh(self) -> None: 

540 """Rebinds and refreshes the URL. Call this if you modified the 

541 rule in place. 

542 

543 :internal: 

544 """ 

545 self.bind(self.map, rebind=True) 

546 

547 def bind(self, map: Map, rebind: bool = False) -> None: 

548 """Bind the url to a map and create a regular expression based on 

549 the information from the rule itself and the defaults from the map. 

550 

551 :internal: 

552 """ 

553 if self.map is not None and not rebind: 

554 raise RuntimeError(f"url rule {self!r} already bound to map {self.map!r}") 

555 self.map = map 

556 if self.strict_slashes is None: 

557 self.strict_slashes = map.strict_slashes 

558 if self.merge_slashes is None: 

559 self.merge_slashes = map.merge_slashes 

560 if self.subdomain is None: 

561 self.subdomain = map.default_subdomain 

562 self.compile() 

563 

564 def get_converter( 

565 self, 

566 variable_name: str, 

567 converter_name: str, 

568 args: t.Tuple, 

569 kwargs: t.Mapping[str, t.Any], 

570 ) -> BaseConverter: 

571 """Looks up the converter for the given parameter. 

572 

573 .. versionadded:: 0.9 

574 """ 

575 if converter_name not in self.map.converters: 

576 raise LookupError(f"the converter {converter_name!r} does not exist") 

577 return self.map.converters[converter_name](self.map, *args, **kwargs) 

578 

579 def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str: 

580 items: t.Iterable[tuple[str, str]] = iter_multi_items(query_vars) 

581 

582 if self.map.sort_parameters: 

583 items = sorted(items, key=self.map.sort_key) 

584 

585 return _urlencode(items, encoding=self.map.charset) 

586 

587 def _parse_rule(self, rule: str) -> t.Iterable[RulePart]: 

588 content = "" 

589 static = True 

590 argument_weights = [] 

591 static_weights: list[tuple[int, int]] = [] 

592 final = False 

593 convertor_number = 0 

594 

595 pos = 0 

596 while pos < len(rule): 

597 match = _part_re.match(rule, pos) 

598 if match is None: 

599 raise ValueError(f"malformed url rule: {rule!r}") 

600 

601 data = match.groupdict() 

602 if data["static"] is not None: 

603 static_weights.append((len(static_weights), -len(data["static"]))) 

604 self._trace.append((False, data["static"])) 

605 content += data["static"] if static else re.escape(data["static"]) 

606 

607 if data["variable"] is not None: 

608 if static: 

609 # Switching content to represent regex, hence the need to escape 

610 content = re.escape(content) 

611 static = False 

612 c_args, c_kwargs = parse_converter_args(data["arguments"] or "") 

613 convobj = self.get_converter( 

614 data["variable"], data["converter"] or "default", c_args, c_kwargs 

615 ) 

616 self._converters[data["variable"]] = convobj 

617 self.arguments.add(data["variable"]) 

618 if not convobj.part_isolating: 

619 final = True 

620 content += f"(?P<__werkzeug_{convertor_number}>{convobj.regex})" 

621 convertor_number += 1 

622 argument_weights.append(convobj.weight) 

623 self._trace.append((True, data["variable"])) 

624 

625 if data["slash"] is not None: 

626 self._trace.append((False, "/")) 

627 if final: 

628 content += "/" 

629 else: 

630 if not static: 

631 content += r"\Z" 

632 weight = Weighting( 

633 -len(static_weights), 

634 static_weights, 

635 -len(argument_weights), 

636 argument_weights, 

637 ) 

638 yield RulePart( 

639 content=content, 

640 final=final, 

641 static=static, 

642 suffixed=False, 

643 weight=weight, 

644 ) 

645 content = "" 

646 static = True 

647 argument_weights = [] 

648 static_weights = [] 

649 final = False 

650 convertor_number = 0 

651 

652 pos = match.end() 

653 

654 suffixed = False 

655 if final and content[-1] == "/": 

656 # If a converter is part_isolating=False (matches slashes) and ends with a 

657 # slash, augment the regex to support slash redirects. 

658 suffixed = True 

659 content = content[:-1] + "(?<!/)(/?)" 

660 if not static: 

661 content += r"\Z" 

662 weight = Weighting( 

663 -len(static_weights), 

664 static_weights, 

665 -len(argument_weights), 

666 argument_weights, 

667 ) 

668 yield RulePart( 

669 content=content, 

670 final=final, 

671 static=static, 

672 suffixed=suffixed, 

673 weight=weight, 

674 ) 

675 if suffixed: 

676 yield RulePart( 

677 content="", final=False, static=True, suffixed=False, weight=weight 

678 ) 

679 

680 def compile(self) -> None: 

681 """Compiles the regular expression and stores it.""" 

682 assert self.map is not None, "rule not bound" 

683 

684 if self.map.host_matching: 

685 domain_rule = self.host or "" 

686 else: 

687 domain_rule = self.subdomain or "" 

688 self._parts = [] 

689 self._trace = [] 

690 self._converters = {} 

691 if domain_rule == "": 

692 self._parts = [ 

693 RulePart( 

694 content="", 

695 final=False, 

696 static=True, 

697 suffixed=False, 

698 weight=Weighting(0, [], 0, []), 

699 ) 

700 ] 

701 else: 

702 self._parts.extend(self._parse_rule(domain_rule)) 

703 self._trace.append((False, "|")) 

704 rule = self.rule 

705 if self.merge_slashes: 

706 rule = re.sub("/{2,}?", "/", self.rule) 

707 self._parts.extend(self._parse_rule(rule)) 

708 

709 self._build: t.Callable[..., tuple[str, str]] 

710 self._build = self._compile_builder(False).__get__(self, None) 

711 self._build_unknown: t.Callable[..., tuple[str, str]] 

712 self._build_unknown = self._compile_builder(True).__get__(self, None) 

713 

714 @staticmethod 

715 def _get_func_code(code: CodeType, name: str) -> t.Callable[..., tuple[str, str]]: 

716 globs: dict[str, t.Any] = {} 

717 locs: dict[str, t.Any] = {} 

718 exec(code, globs, locs) 

719 return locs[name] # type: ignore 

720 

721 def _compile_builder( 

722 self, append_unknown: bool = True 

723 ) -> t.Callable[..., tuple[str, str]]: 

724 defaults = self.defaults or {} 

725 dom_ops: list[tuple[bool, str]] = [] 

726 url_ops: list[tuple[bool, str]] = [] 

727 

728 opl = dom_ops 

729 for is_dynamic, data in self._trace: 

730 if data == "|" and opl is dom_ops: 

731 opl = url_ops 

732 continue 

733 # this seems like a silly case to ever come up but: 

734 # if a default is given for a value that appears in the rule, 

735 # resolve it to a constant ahead of time 

736 if is_dynamic and data in defaults: 

737 data = self._converters[data].to_url(defaults[data]) 

738 opl.append((False, data)) 

739 elif not is_dynamic: 

740 # safe = https://url.spec.whatwg.org/#url-path-segment-string 

741 opl.append( 

742 ( 

743 False, 

744 quote(data, safe="!$&'()*+,/:;=@", encoding=self.map.charset), 

745 ) 

746 ) 

747 else: 

748 opl.append((True, data)) 

749 

750 def _convert(elem: str) -> ast.stmt: 

751 ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem)) 

752 ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2 

753 return ret 

754 

755 def _parts(ops: list[tuple[bool, str]]) -> list[ast.AST]: 

756 parts = [ 

757 _convert(elem) if is_dynamic else ast.Constant(elem) 

758 for is_dynamic, elem in ops 

759 ] 

760 parts = parts or [ast.Constant("")] 

761 # constant fold 

762 ret = [parts[0]] 

763 for p in parts[1:]: 

764 if isinstance(p, ast.Constant) and isinstance(ret[-1], ast.Constant): 

765 ret[-1] = ast.Constant(ret[-1].value + p.value) 

766 else: 

767 ret.append(p) 

768 return ret 

769 

770 dom_parts = _parts(dom_ops) 

771 url_parts = _parts(url_ops) 

772 if not append_unknown: 

773 body = [] 

774 else: 

775 body = [_IF_KWARGS_URL_ENCODE_AST] 

776 url_parts.extend(_URL_ENCODE_AST_NAMES) 

777 

778 def _join(parts: list[ast.AST]) -> ast.AST: 

779 if len(parts) == 1: # shortcut 

780 return parts[0] 

781 return ast.JoinedStr(parts) 

782 

783 body.append( 

784 ast.Return(ast.Tuple([_join(dom_parts), _join(url_parts)], ast.Load())) 

785 ) 

786 

787 pargs = [ 

788 elem 

789 for is_dynamic, elem in dom_ops + url_ops 

790 if is_dynamic and elem not in defaults 

791 ] 

792 kargs = [str(k) for k in defaults] 

793 

794 func_ast: ast.FunctionDef = _prefix_names("def _(): pass") # type: ignore 

795 func_ast.name = f"<builder:{self.rule!r}>" 

796 func_ast.args.args.append(ast.arg(".self", None)) 

797 for arg in pargs + kargs: 

798 func_ast.args.args.append(ast.arg(arg, None)) 

799 func_ast.args.kwarg = ast.arg(".kwargs", None) 

800 for _ in kargs: 

801 func_ast.args.defaults.append(ast.Constant("")) 

802 func_ast.body = body 

803 

804 # Use `ast.parse` instead of `ast.Module` for better portability, since the 

805 # signature of `ast.Module` can change. 

806 module = ast.parse("") 

807 module.body = [func_ast] 

808 

809 # mark everything as on line 1, offset 0 

810 # less error-prone than `ast.fix_missing_locations` 

811 # bad line numbers cause an assert to fail in debug builds 

812 for node in ast.walk(module): 

813 if "lineno" in node._attributes: 

814 node.lineno = 1 

815 if "end_lineno" in node._attributes: 

816 node.end_lineno = node.lineno 

817 if "col_offset" in node._attributes: 

818 node.col_offset = 0 

819 if "end_col_offset" in node._attributes: 

820 node.end_col_offset = node.col_offset 

821 

822 code = compile(module, "<werkzeug routing>", "exec") 

823 return self._get_func_code(code, func_ast.name) 

824 

825 def build( 

826 self, values: t.Mapping[str, t.Any], append_unknown: bool = True 

827 ) -> tuple[str, str] | None: 

828 """Assembles the relative url for that rule and the subdomain. 

829 If building doesn't work for some reasons `None` is returned. 

830 

831 :internal: 

832 """ 

833 try: 

834 if append_unknown: 

835 return self._build_unknown(**values) 

836 else: 

837 return self._build(**values) 

838 except ValidationError: 

839 return None 

840 

841 def provides_defaults_for(self, rule: Rule) -> bool: 

842 """Check if this rule has defaults for a given rule. 

843 

844 :internal: 

845 """ 

846 return bool( 

847 not self.build_only 

848 and self.defaults 

849 and self.endpoint == rule.endpoint 

850 and self != rule 

851 and self.arguments == rule.arguments 

852 ) 

853 

854 def suitable_for( 

855 self, values: t.Mapping[str, t.Any], method: str | None = None 

856 ) -> bool: 

857 """Check if the dict of values has enough data for url generation. 

858 

859 :internal: 

860 """ 

861 # if a method was given explicitly and that method is not supported 

862 # by this rule, this rule is not suitable. 

863 if ( 

864 method is not None 

865 and self.methods is not None 

866 and method not in self.methods 

867 ): 

868 return False 

869 

870 defaults = self.defaults or () 

871 

872 # all arguments required must be either in the defaults dict or 

873 # the value dictionary otherwise it's not suitable 

874 for key in self.arguments: 

875 if key not in defaults and key not in values: 

876 return False 

877 

878 # in case defaults are given we ensure that either the value was 

879 # skipped or the value is the same as the default value. 

880 if defaults: 

881 for key, value in defaults.items(): 

882 if key in values and value != values[key]: 

883 return False 

884 

885 return True 

886 

887 def build_compare_key(self) -> tuple[int, int, int]: 

888 """The build compare key for sorting. 

889 

890 :internal: 

891 """ 

892 return (1 if self.alias else 0, -len(self.arguments), -len(self.defaults or ())) 

893 

894 def __eq__(self, other: object) -> bool: 

895 return isinstance(other, type(self)) and self._trace == other._trace 

896 

897 __hash__ = None # type: ignore 

898 

899 def __str__(self) -> str: 

900 return self.rule 

901 

902 def __repr__(self) -> str: 

903 if self.map is None: 

904 return f"<{type(self).__name__} (unbound)>" 

905 parts = [] 

906 for is_dynamic, data in self._trace: 

907 if is_dynamic: 

908 parts.append(f"<{data}>") 

909 else: 

910 parts.append(data) 

911 parts = "".join(parts).lstrip("|") 

912 methods = f" ({', '.join(self.methods)})" if self.methods is not None else "" 

913 return f"<{type(self).__name__} {parts!r}{methods} -> {self.endpoint}>"