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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

385 statements  

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 \s* 

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

72 (?P<value> 

73 True|False| 

74 \d+.\d+| 

75 \d+.| 

76 \d+| 

77 [\w\d_.]+| 

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

79 )\s*, 

80 """, 

81 re.VERBOSE, 

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) 

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[tuple[t.Any, ...], dict[str, t.Any]]: 

112 argstr += "," 

113 args = [] 

114 kwargs = {} 

115 position = 0 

116 

117 for item in _converter_args_re.finditer(argstr): 

118 if item.start() != position: 

119 raise ValueError( 

120 f"Cannot parse converter argument '{argstr[position : item.start()]}'" 

121 ) 

122 

123 value = item.group("stringval") 

124 if value is None: 

125 value = item.group("value") 

126 value = _pythonize(value) 

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

128 args.append(value) 

129 else: 

130 name = item.group("name") 

131 kwargs[name] = value 

132 position = item.end() 

133 

134 return tuple(args), kwargs 

135 

136 

137class RuleFactory: 

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

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

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

141 """ 

142 

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

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

145 an iterable of rules.""" 

146 raise NotImplementedError() 

147 

148 

149class Subdomain(RuleFactory): 

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

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

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

153 

154 url_map = Map([ 

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

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

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

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

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

160 ]) 

161 ]) 

162 

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

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

165 for the current request. 

166 """ 

167 

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

169 self.subdomain = subdomain 

170 self.rules = rules 

171 

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

173 for rulefactory in self.rules: 

174 for rule in rulefactory.get_rules(map): 

175 rule = rule.empty() 

176 rule.subdomain = self.subdomain 

177 yield rule 

178 

179 

180class Submount(RuleFactory): 

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

182 

183 url_map = Map([ 

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

185 Submount('/blog', [ 

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

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

188 ]) 

189 ]) 

190 

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

192 """ 

193 

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

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

196 self.rules = rules 

197 

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

199 for rulefactory in self.rules: 

200 for rule in rulefactory.get_rules(map): 

201 rule = rule.empty() 

202 rule.rule = self.path + rule.rule 

203 yield rule 

204 

205 

206class EndpointPrefix(RuleFactory): 

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

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

209 

210 url_map = Map([ 

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

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

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

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

215 ])]) 

216 ]) 

217 """ 

218 

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

220 self.prefix = prefix 

221 self.rules = rules 

222 

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

224 for rulefactory in self.rules: 

225 for rule in rulefactory.get_rules(map): 

226 rule = rule.empty() 

227 rule.endpoint = self.prefix + rule.endpoint 

228 yield rule 

229 

230 

231class RuleTemplate: 

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

233 the endpoint, rule, defaults or subdomain sections. 

234 

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

236 

237 from werkzeug.routing import Map, Rule, RuleTemplate 

238 

239 resource = RuleTemplate([ 

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

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

242 ]) 

243 

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

245 

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

247 replace the placeholders in all the string parameters. 

248 """ 

249 

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

251 self.rules = list(rules) 

252 

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

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

255 

256 

257class RuleTemplateFactory(RuleFactory): 

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

259 `RuleTemplate` internally. 

260 

261 :internal: 

262 """ 

263 

264 def __init__( 

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

266 ) -> None: 

267 self.rules = rules 

268 self.context = context 

269 

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

271 for rulefactory in self.rules: 

272 for rule in rulefactory.get_rules(map): 

273 new_defaults = subdomain = None 

274 if rule.defaults: 

275 new_defaults = {} 

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

277 if isinstance(value, str): 

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

279 new_defaults[key] = value 

280 if rule.subdomain is not None: 

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

282 new_endpoint = rule.endpoint 

283 if isinstance(new_endpoint, str): 

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

285 yield Rule( 

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

287 new_defaults, 

288 subdomain, 

289 rule.methods, 

290 rule.build_only, 

291 new_endpoint, 

292 rule.strict_slashes, 

293 ) 

294 

295 

296_ASTT = t.TypeVar("_ASTT", bound=ast.AST) 

297 

298 

299def _prefix_names(src: str, expected_type: type[_ASTT]) -> _ASTT: 

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

301 tree: ast.AST = ast.parse(src).body[0] 

302 if isinstance(tree, ast.Expr): 

303 tree = tree.value 

304 if not isinstance(tree, expected_type): 

305 raise TypeError( 

306 f"AST node is of type {type(tree).__name__}, not {expected_type.__name__}" 

307 ) 

308 for node in ast.walk(tree): 

309 if isinstance(node, ast.Name): 

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

311 return tree 

312 

313 

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

315_IF_KWARGS_URL_ENCODE_CODE = """\ 

316if kwargs: 

317 params = self._encode_query_vars(kwargs) 

318 q = "?" if params else "" 

319else: 

320 q = params = "" 

321""" 

322_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE, ast.If) 

323_URL_ENCODE_AST_NAMES = ( 

324 _prefix_names("q", ast.Name), 

325 _prefix_names("params", ast.Name), 

326) 

327 

328 

329class Rule(RuleFactory): 

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

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

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

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

334 

335 `string` 

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

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

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

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

340 

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

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

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

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

345 

346 The converters are defined on the `Map`. 

347 

348 `endpoint` 

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

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

351 because the endpoint is used for URL generation. 

352 

353 `defaults` 

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

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

356 

357 url_map = Map([ 

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

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

360 ]) 

361 

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

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

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

365 generation. 

366 

367 `subdomain` 

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

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

370 not bound to a subdomain this feature is disabled. 

371 

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

373 and all subdomains are forwarded to your application:: 

374 

375 url_map = Map([ 

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

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

378 ]) 

379 

380 `methods` 

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

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

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

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

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

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

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

388 

389 `strict_slashes` 

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

391 not specified the `Map` setting is used. 

392 

393 `merge_slashes` 

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

395 

396 `build_only` 

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

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

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

400 

401 `redirect_to` 

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

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

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

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

406 rule syntax:: 

407 

408 def foo_with_slug(adapter, id): 

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

410 # course has nothing to do with werkzeug. 

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

412 

413 url_map = Map([ 

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

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

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

417 ]) 

418 

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

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

421 

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

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

424 really mean root of that domain. 

425 

426 `alias` 

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

428 endpoint and arguments. 

429 

430 `host` 

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

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

433 that the subdomain feature is disabled. 

434 

435 `websocket` 

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

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

438 requests. 

439 

440 .. versionchanged:: 2.1 

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

442 servers, are considered when routing instead of terminating the 

443 match early. 

444 

445 .. versionadded:: 1.0 

446 Added ``websocket``. 

447 

448 .. versionadded:: 1.0 

449 Added ``merge_slashes``. 

450 

451 .. versionadded:: 0.7 

452 Added ``alias`` and ``host``. 

453 

454 .. versionchanged:: 0.6.1 

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

456 """ 

457 

458 def __init__( 

459 self, 

460 string: str, 

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

462 subdomain: str | None = None, 

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

464 build_only: bool = False, 

465 endpoint: t.Any | None = None, 

466 strict_slashes: bool | None = None, 

467 merge_slashes: bool | None = None, 

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

469 alias: bool = False, 

470 host: str | None = None, 

471 websocket: bool = False, 

472 ) -> None: 

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

474 raise ValueError(f"URL rule '{string}' must start with a slash.") 

475 

476 self.rule = string 

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

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

479 

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

481 self.strict_slashes = strict_slashes 

482 self.merge_slashes = merge_slashes 

483 self.subdomain = subdomain 

484 self.host = host 

485 self.defaults = defaults 

486 self.build_only = build_only 

487 self.alias = alias 

488 self.websocket = websocket 

489 

490 if methods is not None: 

491 if isinstance(methods, str): 

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

493 

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

495 

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

497 methods.add("HEAD") 

498 

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

500 raise ValueError( 

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

502 ) 

503 

504 self.methods = methods 

505 self.endpoint: t.Any = endpoint 

506 self.redirect_to = redirect_to 

507 

508 if defaults: 

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

510 else: 

511 self.arguments = set() 

512 

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

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

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

516 

517 def empty(self) -> Rule: 

518 """ 

519 Return an unbound copy of this rule. 

520 

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

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

523 provided to the new copy. 

524 """ 

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

526 

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

528 """ 

529 Provides kwargs for instantiating empty copy with empty() 

530 

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

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

533 has custom keyword arguments that are needed at instantiation. 

534 

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

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

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

538 """ 

539 defaults = None 

540 if self.defaults: 

541 defaults = dict(self.defaults) 

542 return dict( 

543 defaults=defaults, 

544 subdomain=self.subdomain, 

545 methods=self.methods, 

546 build_only=self.build_only, 

547 endpoint=self.endpoint, 

548 strict_slashes=self.strict_slashes, 

549 redirect_to=self.redirect_to, 

550 alias=self.alias, 

551 host=self.host, 

552 ) 

553 

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

555 yield self 

556 

557 def refresh(self) -> None: 

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

559 rule in place. 

560 

561 :internal: 

562 """ 

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

564 

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

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

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

568 

569 :internal: 

570 """ 

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

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

573 self.map = map 

574 if self.strict_slashes is None: 

575 self.strict_slashes = map.strict_slashes 

576 if self.merge_slashes is None: 

577 self.merge_slashes = map.merge_slashes 

578 if self.subdomain is None: 

579 self.subdomain = map.default_subdomain 

580 self.compile() 

581 

582 def get_converter( 

583 self, 

584 variable_name: str, 

585 converter_name: str, 

586 args: tuple[t.Any, ...], 

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

588 ) -> BaseConverter: 

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

590 

591 .. versionadded:: 0.9 

592 """ 

593 if converter_name not in self.map.converters: 

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

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

596 

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

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

599 

600 if self.map.sort_parameters: 

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

602 

603 return _urlencode(items) 

604 

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

606 content = "" 

607 static = True 

608 argument_weights = [] 

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

610 final = False 

611 convertor_number = 0 

612 

613 pos = 0 

614 while pos < len(rule): 

615 match = _part_re.match(rule, pos) 

616 if match is None: 

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

618 

619 data = match.groupdict() 

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

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

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

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

624 

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

626 if static: 

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

628 content = re.escape(content) 

629 static = False 

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

631 convobj = self.get_converter( 

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

633 ) 

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

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

636 if not convobj.part_isolating: 

637 final = True 

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

639 convertor_number += 1 

640 argument_weights.append(convobj.weight) 

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

642 

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

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

645 if final: 

646 content += "/" 

647 else: 

648 if not static: 

649 content += r"\Z" 

650 weight = Weighting( 

651 -len(static_weights), 

652 static_weights, 

653 -len(argument_weights), 

654 argument_weights, 

655 ) 

656 yield RulePart( 

657 content=content, 

658 final=final, 

659 static=static, 

660 suffixed=False, 

661 weight=weight, 

662 ) 

663 content = "" 

664 static = True 

665 argument_weights = [] 

666 static_weights = [] 

667 final = False 

668 convertor_number = 0 

669 

670 pos = match.end() 

671 

672 suffixed = False 

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

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

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

676 suffixed = True 

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

678 if not static: 

679 content += r"\Z" 

680 weight = Weighting( 

681 -len(static_weights), 

682 static_weights, 

683 -len(argument_weights), 

684 argument_weights, 

685 ) 

686 yield RulePart( 

687 content=content, 

688 final=final, 

689 static=static, 

690 suffixed=suffixed, 

691 weight=weight, 

692 ) 

693 if suffixed: 

694 yield RulePart( 

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

696 ) 

697 

698 def compile(self) -> None: 

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

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

701 

702 if self.map.host_matching: 

703 domain_rule = self.host or "" 

704 else: 

705 domain_rule = self.subdomain or "" 

706 self._parts = [] 

707 self._trace = [] 

708 self._converters = {} 

709 if domain_rule == "": 

710 self._parts = [ 

711 RulePart( 

712 content="", 

713 final=False, 

714 static=True, 

715 suffixed=False, 

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

717 ) 

718 ] 

719 else: 

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

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

722 rule = self.rule 

723 if self.merge_slashes: 

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

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

726 

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

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

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

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

731 

732 @staticmethod 

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

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

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

736 exec(code, globs, locs) 

737 return locs[name] # type: ignore 

738 

739 def _compile_builder( 

740 self, append_unknown: bool = True 

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

742 defaults = self.defaults or {} 

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

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

745 

746 opl = dom_ops 

747 for is_dynamic, data in self._trace: 

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

749 opl = url_ops 

750 continue 

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

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

753 # resolve it to a constant ahead of time 

754 if is_dynamic and data in defaults: 

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

756 opl.append((False, data)) 

757 elif not is_dynamic: 

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

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

760 else: 

761 opl.append((True, data)) 

762 

763 def _convert(elem: str) -> ast.Call: 

764 ret = _prefix_names(_CALL_CONVERTER_CODE_FMT.format(elem=elem), ast.Call) 

765 ret.args = [ast.Name(elem, ast.Load())] 

766 return ret 

767 

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

769 parts: list[ast.expr] = [ 

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

771 for is_dynamic, elem in ops 

772 ] 

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

774 # constant fold 

775 ret = [parts[0]] 

776 for p in parts[1:]: 

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

778 ret[-1] = ast.Constant(ret[-1].value + p.value) # type: ignore[operator] 

779 else: 

780 ret.append(p) 

781 return ret 

782 

783 dom_parts = _parts(dom_ops) 

784 url_parts = _parts(url_ops) 

785 body: list[ast.stmt] 

786 if not append_unknown: 

787 body = [] 

788 else: 

789 body = [_IF_KWARGS_URL_ENCODE_AST] 

790 url_parts.extend(_URL_ENCODE_AST_NAMES) 

791 

792 def _join(parts: list[ast.expr]) -> ast.expr: 

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

794 return parts[0] 

795 return ast.JoinedStr(parts) 

796 

797 body.append( 

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

799 ) 

800 

801 pargs = [ 

802 elem 

803 for is_dynamic, elem in dom_ops + url_ops 

804 if is_dynamic and elem not in defaults 

805 ] 

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

807 

808 func_ast = _prefix_names("def _(): pass", ast.FunctionDef) 

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

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

811 for arg in pargs + kargs: 

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

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

814 for _ in kargs: 

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

816 func_ast.body = body 

817 

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

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

820 module = ast.parse("") 

821 module.body = [func_ast] 

822 

823 # mark everything as on line 1, offset 0 

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

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

826 for node in ast.walk(module): 

827 if "lineno" in node._attributes: 

828 node.lineno = 1 # type: ignore[attr-defined] 

829 if "end_lineno" in node._attributes: 

830 node.end_lineno = node.lineno # type: ignore[attr-defined] 

831 if "col_offset" in node._attributes: 

832 node.col_offset = 0 # type: ignore[attr-defined] 

833 if "end_col_offset" in node._attributes: 

834 node.end_col_offset = node.col_offset # type: ignore[attr-defined] 

835 

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

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

838 

839 def build( 

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

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

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

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

844 

845 :internal: 

846 """ 

847 try: 

848 if append_unknown: 

849 return self._build_unknown(**values) 

850 else: 

851 return self._build(**values) 

852 except ValidationError: 

853 return None 

854 

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

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

857 

858 :internal: 

859 """ 

860 return bool( 

861 not self.build_only 

862 and self.defaults 

863 and self.endpoint == rule.endpoint 

864 and self != rule 

865 and self.arguments == rule.arguments 

866 ) 

867 

868 def suitable_for( 

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

870 ) -> bool: 

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

872 

873 :internal: 

874 """ 

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

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

877 if ( 

878 method is not None 

879 and self.methods is not None 

880 and method not in self.methods 

881 ): 

882 return False 

883 

884 defaults = self.defaults or () 

885 

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

887 # the value dictionary otherwise it's not suitable 

888 for key in self.arguments: 

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

890 return False 

891 

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

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

894 if defaults: 

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

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

897 return False 

898 

899 return True 

900 

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

902 """The build compare key for sorting. 

903 

904 :internal: 

905 """ 

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

907 

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

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

910 

911 __hash__ = None # type: ignore 

912 

913 def __str__(self) -> str: 

914 return self.rule 

915 

916 def __repr__(self) -> str: 

917 if self.map is None: 

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

919 parts = [] 

920 for is_dynamic, data in self._trace: 

921 if is_dynamic: 

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

923 else: 

924 parts.append(data) 

925 parts_str = "".join(parts).lstrip("|") 

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

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