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 

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

86 

87 

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

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

90 

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

92 """ 

93 try: 

94 return value.index(target, pos) 

95 except ValueError: 

96 return len(value) 

97 

98 

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

100 if value in _PYTHON_CONSTANTS: 

101 return _PYTHON_CONSTANTS[value] 

102 for convert in int, float: 

103 try: 

104 return convert(value) 

105 except ValueError: 

106 pass 

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

108 value = value[1:-1] 

109 return str(value) 

110 

111 

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

113 argstr += "," 

114 args = [] 

115 kwargs = {} 

116 position = 0 

117 

118 for item in _converter_args_re.finditer(argstr): 

119 if item.start() != position: 

120 raise ValueError( 

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

122 ) 

123 

124 value = item.group("stringval") 

125 if value is None: 

126 value = item.group("value") 

127 value = _pythonize(value) 

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

129 args.append(value) 

130 else: 

131 name = item.group("name") 

132 kwargs[name] = value 

133 position = item.end() 

134 

135 return tuple(args), kwargs 

136 

137 

138class RuleFactory: 

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

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

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

142 """ 

143 

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

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

146 an iterable of rules.""" 

147 raise NotImplementedError() 

148 

149 

150class Subdomain(RuleFactory): 

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

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

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

154 

155 url_map = Map([ 

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

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

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

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

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

161 ]) 

162 ]) 

163 

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

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

166 for the current request. 

167 """ 

168 

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

170 self.subdomain = subdomain 

171 self.rules = rules 

172 

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

174 for rulefactory in self.rules: 

175 for rule in rulefactory.get_rules(map): 

176 rule = rule.empty() 

177 rule.subdomain = self.subdomain 

178 yield rule 

179 

180 

181class Submount(RuleFactory): 

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

183 

184 url_map = Map([ 

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

186 Submount('/blog', [ 

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

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

189 ]) 

190 ]) 

191 

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

193 """ 

194 

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

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

197 self.rules = rules 

198 

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

200 for rulefactory in self.rules: 

201 for rule in rulefactory.get_rules(map): 

202 rule = rule.empty() 

203 rule.rule = self.path + rule.rule 

204 yield rule 

205 

206 

207class EndpointPrefix(RuleFactory): 

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

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

210 

211 url_map = Map([ 

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

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

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

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

216 ])]) 

217 ]) 

218 """ 

219 

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

221 self.prefix = prefix 

222 self.rules = rules 

223 

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

225 for rulefactory in self.rules: 

226 for rule in rulefactory.get_rules(map): 

227 rule = rule.empty() 

228 rule.endpoint = self.prefix + rule.endpoint 

229 yield rule 

230 

231 

232class RuleTemplate: 

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

234 the endpoint, rule, defaults or subdomain sections. 

235 

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

237 

238 from werkzeug.routing import Map, Rule, RuleTemplate 

239 

240 resource = RuleTemplate([ 

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

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

243 ]) 

244 

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

246 

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

248 replace the placeholders in all the string parameters. 

249 """ 

250 

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

252 self.rules = list(rules) 

253 

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

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

256 

257 

258class RuleTemplateFactory(RuleFactory): 

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

260 `RuleTemplate` internally. 

261 

262 :internal: 

263 """ 

264 

265 def __init__( 

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

267 ) -> None: 

268 self.rules = rules 

269 self.context = context 

270 

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

272 for rulefactory in self.rules: 

273 for rule in rulefactory.get_rules(map): 

274 new_defaults = subdomain = None 

275 if rule.defaults: 

276 new_defaults = {} 

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

278 if isinstance(value, str): 

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

280 new_defaults[key] = value 

281 if rule.subdomain is not None: 

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

283 new_endpoint = rule.endpoint 

284 if isinstance(new_endpoint, str): 

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

286 yield Rule( 

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

288 new_defaults, 

289 subdomain, 

290 rule.methods, 

291 rule.build_only, 

292 new_endpoint, 

293 rule.strict_slashes, 

294 ) 

295 

296 

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

298 

299 

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

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

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

303 if isinstance(tree, ast.Expr): 

304 tree = tree.value 

305 if not isinstance(tree, expected_type): 

306 raise TypeError( 

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

308 ) 

309 for node in ast.walk(tree): 

310 if isinstance(node, ast.Name): 

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

312 return tree 

313 

314 

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

316_IF_KWARGS_URL_ENCODE_CODE = """\ 

317if kwargs: 

318 params = self._encode_query_vars(kwargs) 

319 q = "?" if params else "" 

320else: 

321 q = params = "" 

322""" 

323_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE, ast.If) 

324_URL_ENCODE_AST_NAMES = ( 

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

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

327) 

328 

329 

330class Rule(RuleFactory): 

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

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

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

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

335 

336 `string` 

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

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

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

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

341 

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

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

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

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

346 

347 The converters are defined on the `Map`. 

348 

349 `endpoint` 

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

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

352 because the endpoint is used for URL generation. 

353 

354 `defaults` 

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

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

357 

358 url_map = Map([ 

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

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

361 ]) 

362 

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

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

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

366 generation. 

367 

368 `subdomain` 

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

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

371 not bound to a subdomain this feature is disabled. 

372 

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

374 and all subdomains are forwarded to your application:: 

375 

376 url_map = Map([ 

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

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

379 ]) 

380 

381 `methods` 

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

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

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

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

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

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

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

389 

390 `strict_slashes` 

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

392 not specified the `Map` setting is used. 

393 

394 `merge_slashes` 

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

396 

397 `build_only` 

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

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

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

401 

402 `redirect_to` 

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

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

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

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

407 rule syntax:: 

408 

409 def foo_with_slug(adapter, id): 

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

411 # course has nothing to do with werkzeug. 

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

413 

414 url_map = Map([ 

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

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

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

418 ]) 

419 

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

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

422 

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

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

425 really mean root of that domain. 

426 

427 `alias` 

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

429 endpoint and arguments. 

430 

431 `host` 

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

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

434 that the subdomain feature is disabled. 

435 

436 `websocket` 

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

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

439 requests. 

440 

441 .. versionchanged:: 2.1 

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

443 servers, are considered when routing instead of terminating the 

444 match early. 

445 

446 .. versionadded:: 1.0 

447 Added ``websocket``. 

448 

449 .. versionadded:: 1.0 

450 Added ``merge_slashes``. 

451 

452 .. versionadded:: 0.7 

453 Added ``alias`` and ``host``. 

454 

455 .. versionchanged:: 0.6.1 

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

457 """ 

458 

459 def __init__( 

460 self, 

461 string: str, 

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

463 subdomain: str | None = None, 

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

465 build_only: bool = False, 

466 endpoint: t.Any | None = None, 

467 strict_slashes: bool | None = None, 

468 merge_slashes: bool | None = None, 

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

470 alias: bool = False, 

471 host: str | None = None, 

472 websocket: bool = False, 

473 ) -> None: 

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

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

476 

477 self.rule = string 

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

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

480 

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

482 self.strict_slashes = strict_slashes 

483 self.merge_slashes = merge_slashes 

484 self.subdomain = subdomain 

485 self.host = host 

486 self.defaults = defaults 

487 self.build_only = build_only 

488 self.alias = alias 

489 self.websocket = websocket 

490 

491 if methods is not None: 

492 if isinstance(methods, str): 

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

494 

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

496 

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

498 methods.add("HEAD") 

499 

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

501 raise ValueError( 

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

503 ) 

504 

505 self.methods = methods 

506 self.endpoint: t.Any = endpoint 

507 self.redirect_to = redirect_to 

508 

509 if defaults: 

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

511 else: 

512 self.arguments = set() 

513 

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

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

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

517 

518 def empty(self) -> Rule: 

519 """ 

520 Return an unbound copy of this rule. 

521 

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

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

524 provided to the new copy. 

525 """ 

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

527 

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

529 """ 

530 Provides kwargs for instantiating empty copy with empty() 

531 

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

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

534 has custom keyword arguments that are needed at instantiation. 

535 

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

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

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

539 """ 

540 defaults = None 

541 if self.defaults: 

542 defaults = dict(self.defaults) 

543 return dict( 

544 defaults=defaults, 

545 subdomain=self.subdomain, 

546 methods=self.methods, 

547 build_only=self.build_only, 

548 endpoint=self.endpoint, 

549 strict_slashes=self.strict_slashes, 

550 redirect_to=self.redirect_to, 

551 alias=self.alias, 

552 host=self.host, 

553 ) 

554 

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

556 yield self 

557 

558 def refresh(self) -> None: 

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

560 rule in place. 

561 

562 :internal: 

563 """ 

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

565 

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

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

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

569 

570 :internal: 

571 """ 

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

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

574 self.map = map 

575 if self.strict_slashes is None: 

576 self.strict_slashes = map.strict_slashes 

577 if self.merge_slashes is None: 

578 self.merge_slashes = map.merge_slashes 

579 if self.subdomain is None: 

580 self.subdomain = map.default_subdomain 

581 self.compile() 

582 

583 def get_converter( 

584 self, 

585 variable_name: str, 

586 converter_name: str, 

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

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

589 ) -> BaseConverter: 

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

591 

592 .. versionadded:: 0.9 

593 """ 

594 if converter_name not in self.map.converters: 

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

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

597 

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

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

600 

601 if self.map.sort_parameters: 

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

603 

604 return _urlencode(items) 

605 

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

607 content = "" 

608 static = True 

609 argument_weights = [] 

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

611 final = False 

612 convertor_number = 0 

613 

614 pos = 0 

615 while pos < len(rule): 

616 match = _part_re.match(rule, pos) 

617 if match is None: 

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

619 

620 data = match.groupdict() 

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

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

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

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

625 

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

627 if static: 

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

629 content = re.escape(content) 

630 static = False 

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

632 convobj = self.get_converter( 

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

634 ) 

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

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

637 if not convobj.part_isolating: 

638 final = True 

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

640 convertor_number += 1 

641 argument_weights.append(convobj.weight) 

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

643 

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

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

646 if final: 

647 content += "/" 

648 else: 

649 if not static: 

650 content += r"\Z" 

651 weight = Weighting( 

652 -len(static_weights), 

653 static_weights, 

654 -len(argument_weights), 

655 argument_weights, 

656 ) 

657 yield RulePart( 

658 content=content, 

659 final=final, 

660 static=static, 

661 suffixed=False, 

662 weight=weight, 

663 ) 

664 content = "" 

665 static = True 

666 argument_weights = [] 

667 static_weights = [] 

668 final = False 

669 convertor_number = 0 

670 

671 pos = match.end() 

672 

673 suffixed = False 

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

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

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

677 suffixed = True 

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

679 if not static: 

680 content += r"\Z" 

681 weight = Weighting( 

682 -len(static_weights), 

683 static_weights, 

684 -len(argument_weights), 

685 argument_weights, 

686 ) 

687 yield RulePart( 

688 content=content, 

689 final=final, 

690 static=static, 

691 suffixed=suffixed, 

692 weight=weight, 

693 ) 

694 if suffixed: 

695 yield RulePart( 

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

697 ) 

698 

699 def compile(self) -> None: 

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

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

702 

703 if self.map.host_matching: 

704 domain_rule = self.host or "" 

705 else: 

706 domain_rule = self.subdomain or "" 

707 self._parts = [] 

708 self._trace = [] 

709 self._converters = {} 

710 if domain_rule == "": 

711 self._parts = [ 

712 RulePart( 

713 content="", 

714 final=False, 

715 static=True, 

716 suffixed=False, 

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

718 ) 

719 ] 

720 else: 

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

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

723 rule = self.rule 

724 if self.merge_slashes: 

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

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

727 

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

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

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

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

732 

733 @staticmethod 

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

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

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

737 exec(code, globs, locs) 

738 return locs[name] # type: ignore 

739 

740 def _compile_builder( 

741 self, append_unknown: bool = True 

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

743 defaults = self.defaults or {} 

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

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

746 

747 opl = dom_ops 

748 for is_dynamic, data in self._trace: 

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

750 opl = url_ops 

751 continue 

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

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

754 # resolve it to a constant ahead of time 

755 if is_dynamic and data in defaults: 

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

757 opl.append((False, data)) 

758 elif not is_dynamic: 

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

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

761 else: 

762 opl.append((True, data)) 

763 

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

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

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

767 return ret 

768 

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

770 parts: list[ast.expr] = [ 

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

772 for is_dynamic, elem in ops 

773 ] 

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

775 # constant fold 

776 ret = [parts[0]] 

777 for p in parts[1:]: 

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

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

780 else: 

781 ret.append(p) 

782 return ret 

783 

784 dom_parts = _parts(dom_ops) 

785 url_parts = _parts(url_ops) 

786 body: list[ast.stmt] 

787 if not append_unknown: 

788 body = [] 

789 else: 

790 body = [_IF_KWARGS_URL_ENCODE_AST] 

791 url_parts.extend(_URL_ENCODE_AST_NAMES) 

792 

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

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

795 return parts[0] 

796 return ast.JoinedStr(parts) 

797 

798 body.append( 

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

800 ) 

801 

802 pargs = [ 

803 elem 

804 for is_dynamic, elem in dom_ops + url_ops 

805 if is_dynamic and elem not in defaults 

806 ] 

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

808 

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

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

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

812 for arg in pargs + kargs: 

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

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

815 for _ in kargs: 

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

817 func_ast.body = body 

818 

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

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

821 module = ast.parse("") 

822 module.body = [func_ast] 

823 

824 # mark everything as on line 1, offset 0 

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

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

827 for node in ast.walk(module): 

828 if "lineno" in node._attributes: 

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

830 if "end_lineno" in node._attributes: 

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

832 if "col_offset" in node._attributes: 

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

834 if "end_col_offset" in node._attributes: 

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

836 

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

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

839 

840 def build( 

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

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

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

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

845 

846 :internal: 

847 """ 

848 try: 

849 if append_unknown: 

850 return self._build_unknown(**values) 

851 else: 

852 return self._build(**values) 

853 except ValidationError: 

854 return None 

855 

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

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

858 

859 :internal: 

860 """ 

861 return bool( 

862 not self.build_only 

863 and self.defaults 

864 and self.endpoint == rule.endpoint 

865 and self != rule 

866 and self.arguments == rule.arguments 

867 ) 

868 

869 def suitable_for( 

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

871 ) -> bool: 

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

873 

874 :internal: 

875 """ 

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

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

878 if ( 

879 method is not None 

880 and self.methods is not None 

881 and method not in self.methods 

882 ): 

883 return False 

884 

885 defaults = self.defaults or () 

886 

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

888 # the value dictionary otherwise it's not suitable 

889 for key in self.arguments: 

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

891 return False 

892 

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

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

895 if defaults: 

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

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

898 return False 

899 

900 return True 

901 

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

903 """The build compare key for sorting. 

904 

905 :internal: 

906 """ 

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

908 

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

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

911 

912 __hash__ = None # type: ignore 

913 

914 def __str__(self) -> str: 

915 return self.rule 

916 

917 def __repr__(self) -> str: 

918 if self.map is None: 

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

920 parts = [] 

921 for is_dynamic, data in self._trace: 

922 if is_dynamic: 

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

924 else: 

925 parts.append(data) 

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

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

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