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

378 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 07:17 +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(f"URL rule '{string}' must start with a slash.") 

458 

459 self.rule = string 

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

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

462 

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

464 self.strict_slashes = strict_slashes 

465 self.merge_slashes = merge_slashes 

466 self.subdomain = subdomain 

467 self.host = host 

468 self.defaults = defaults 

469 self.build_only = build_only 

470 self.alias = alias 

471 self.websocket = websocket 

472 

473 if methods is not None: 

474 if isinstance(methods, str): 

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

476 

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

478 

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

480 methods.add("HEAD") 

481 

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

483 raise ValueError( 

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

485 ) 

486 

487 self.methods = methods 

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

489 self.redirect_to = redirect_to 

490 

491 if defaults: 

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

493 else: 

494 self.arguments = set() 

495 

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

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

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

499 

500 def empty(self) -> Rule: 

501 """ 

502 Return an unbound copy of this rule. 

503 

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

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

506 provided to the new copy. 

507 """ 

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

509 

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

511 """ 

512 Provides kwargs for instantiating empty copy with empty() 

513 

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

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

516 has custom keyword arguments that are needed at instantiation. 

517 

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

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

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

521 """ 

522 defaults = None 

523 if self.defaults: 

524 defaults = dict(self.defaults) 

525 return dict( 

526 defaults=defaults, 

527 subdomain=self.subdomain, 

528 methods=self.methods, 

529 build_only=self.build_only, 

530 endpoint=self.endpoint, 

531 strict_slashes=self.strict_slashes, 

532 redirect_to=self.redirect_to, 

533 alias=self.alias, 

534 host=self.host, 

535 ) 

536 

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

538 yield self 

539 

540 def refresh(self) -> None: 

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

542 rule in place. 

543 

544 :internal: 

545 """ 

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

547 

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

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

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

551 

552 :internal: 

553 """ 

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

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

556 self.map = map 

557 if self.strict_slashes is None: 

558 self.strict_slashes = map.strict_slashes 

559 if self.merge_slashes is None: 

560 self.merge_slashes = map.merge_slashes 

561 if self.subdomain is None: 

562 self.subdomain = map.default_subdomain 

563 self.compile() 

564 

565 def get_converter( 

566 self, 

567 variable_name: str, 

568 converter_name: str, 

569 args: t.Tuple, 

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

571 ) -> BaseConverter: 

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

573 

574 .. versionadded:: 0.9 

575 """ 

576 if converter_name not in self.map.converters: 

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

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

579 

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

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

582 

583 if self.map.sort_parameters: 

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

585 

586 return _urlencode(items) 

587 

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

589 content = "" 

590 static = True 

591 argument_weights = [] 

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

593 final = False 

594 convertor_number = 0 

595 

596 pos = 0 

597 while pos < len(rule): 

598 match = _part_re.match(rule, pos) 

599 if match is None: 

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

601 

602 data = match.groupdict() 

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

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

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

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

607 

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

609 if static: 

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

611 content = re.escape(content) 

612 static = False 

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

614 convobj = self.get_converter( 

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

616 ) 

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

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

619 if not convobj.part_isolating: 

620 final = True 

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

622 convertor_number += 1 

623 argument_weights.append(convobj.weight) 

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

625 

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

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

628 if final: 

629 content += "/" 

630 else: 

631 if not static: 

632 content += r"\Z" 

633 weight = Weighting( 

634 -len(static_weights), 

635 static_weights, 

636 -len(argument_weights), 

637 argument_weights, 

638 ) 

639 yield RulePart( 

640 content=content, 

641 final=final, 

642 static=static, 

643 suffixed=False, 

644 weight=weight, 

645 ) 

646 content = "" 

647 static = True 

648 argument_weights = [] 

649 static_weights = [] 

650 final = False 

651 convertor_number = 0 

652 

653 pos = match.end() 

654 

655 suffixed = False 

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

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

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

659 suffixed = True 

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

661 if not static: 

662 content += r"\Z" 

663 weight = Weighting( 

664 -len(static_weights), 

665 static_weights, 

666 -len(argument_weights), 

667 argument_weights, 

668 ) 

669 yield RulePart( 

670 content=content, 

671 final=final, 

672 static=static, 

673 suffixed=suffixed, 

674 weight=weight, 

675 ) 

676 if suffixed: 

677 yield RulePart( 

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

679 ) 

680 

681 def compile(self) -> None: 

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

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

684 

685 if self.map.host_matching: 

686 domain_rule = self.host or "" 

687 else: 

688 domain_rule = self.subdomain or "" 

689 self._parts = [] 

690 self._trace = [] 

691 self._converters = {} 

692 if domain_rule == "": 

693 self._parts = [ 

694 RulePart( 

695 content="", 

696 final=False, 

697 static=True, 

698 suffixed=False, 

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

700 ) 

701 ] 

702 else: 

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

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

705 rule = self.rule 

706 if self.merge_slashes: 

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

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

709 

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

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

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

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

714 

715 @staticmethod 

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

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

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

719 exec(code, globs, locs) 

720 return locs[name] # type: ignore 

721 

722 def _compile_builder( 

723 self, append_unknown: bool = True 

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

725 defaults = self.defaults or {} 

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

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

728 

729 opl = dom_ops 

730 for is_dynamic, data in self._trace: 

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

732 opl = url_ops 

733 continue 

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

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

736 # resolve it to a constant ahead of time 

737 if is_dynamic and data in defaults: 

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

739 opl.append((False, data)) 

740 elif not is_dynamic: 

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

742 opl.append((False, quote(data, safe="!$&'()*+,/:;=@"))) 

743 else: 

744 opl.append((True, data)) 

745 

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

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

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

749 return ret 

750 

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

752 parts = [ 

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

754 for is_dynamic, elem in ops 

755 ] 

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

757 # constant fold 

758 ret = [parts[0]] 

759 for p in parts[1:]: 

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

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

762 else: 

763 ret.append(p) 

764 return ret 

765 

766 dom_parts = _parts(dom_ops) 

767 url_parts = _parts(url_ops) 

768 if not append_unknown: 

769 body = [] 

770 else: 

771 body = [_IF_KWARGS_URL_ENCODE_AST] 

772 url_parts.extend(_URL_ENCODE_AST_NAMES) 

773 

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

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

776 return parts[0] 

777 return ast.JoinedStr(parts) 

778 

779 body.append( 

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

781 ) 

782 

783 pargs = [ 

784 elem 

785 for is_dynamic, elem in dom_ops + url_ops 

786 if is_dynamic and elem not in defaults 

787 ] 

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

789 

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

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

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

793 for arg in pargs + kargs: 

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

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

796 for _ in kargs: 

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

798 func_ast.body = body 

799 

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

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

802 module = ast.parse("") 

803 module.body = [func_ast] 

804 

805 # mark everything as on line 1, offset 0 

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

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

808 for node in ast.walk(module): 

809 if "lineno" in node._attributes: 

810 node.lineno = 1 

811 if "end_lineno" in node._attributes: 

812 node.end_lineno = node.lineno 

813 if "col_offset" in node._attributes: 

814 node.col_offset = 0 

815 if "end_col_offset" in node._attributes: 

816 node.end_col_offset = node.col_offset 

817 

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

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

820 

821 def build( 

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

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

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

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

826 

827 :internal: 

828 """ 

829 try: 

830 if append_unknown: 

831 return self._build_unknown(**values) 

832 else: 

833 return self._build(**values) 

834 except ValidationError: 

835 return None 

836 

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

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

839 

840 :internal: 

841 """ 

842 return bool( 

843 not self.build_only 

844 and self.defaults 

845 and self.endpoint == rule.endpoint 

846 and self != rule 

847 and self.arguments == rule.arguments 

848 ) 

849 

850 def suitable_for( 

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

852 ) -> bool: 

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

854 

855 :internal: 

856 """ 

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

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

859 if ( 

860 method is not None 

861 and self.methods is not None 

862 and method not in self.methods 

863 ): 

864 return False 

865 

866 defaults = self.defaults or () 

867 

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

869 # the value dictionary otherwise it's not suitable 

870 for key in self.arguments: 

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

872 return False 

873 

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

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

876 if defaults: 

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

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

879 return False 

880 

881 return True 

882 

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

884 """The build compare key for sorting. 

885 

886 :internal: 

887 """ 

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

889 

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

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

892 

893 __hash__ = None # type: ignore 

894 

895 def __str__(self) -> str: 

896 return self.rule 

897 

898 def __repr__(self) -> str: 

899 if self.map is None: 

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

901 parts = [] 

902 for is_dynamic, data in self._trace: 

903 if is_dynamic: 

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

905 else: 

906 parts.append(data) 

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

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

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