Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

382 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) # type: ignore 

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 

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

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

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

300 if isinstance(tree, ast.Expr): 

301 tree = tree.value # type: ignore 

302 for node in ast.walk(tree): 

303 if isinstance(node, ast.Name): 

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

305 return tree 

306 

307 

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

309_IF_KWARGS_URL_ENCODE_CODE = """\ 

310if kwargs: 

311 params = self._encode_query_vars(kwargs) 

312 q = "?" if params else "" 

313else: 

314 q = params = "" 

315""" 

316_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE) 

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

318 

319 

320class Rule(RuleFactory): 

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

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

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

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

325 

326 `string` 

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

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

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

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

331 

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

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

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

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

336 

337 The converters are defined on the `Map`. 

338 

339 `endpoint` 

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

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

342 because the endpoint is used for URL generation. 

343 

344 `defaults` 

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

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

347 

348 url_map = Map([ 

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

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

351 ]) 

352 

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

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

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

356 generation. 

357 

358 `subdomain` 

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

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

361 not bound to a subdomain this feature is disabled. 

362 

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

364 and all subdomains are forwarded to your application:: 

365 

366 url_map = Map([ 

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

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

369 ]) 

370 

371 `methods` 

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

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

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

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

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

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

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

379 

380 `strict_slashes` 

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

382 not specified the `Map` setting is used. 

383 

384 `merge_slashes` 

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

386 

387 `build_only` 

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

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

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

391 

392 `redirect_to` 

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

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

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

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

397 rule syntax:: 

398 

399 def foo_with_slug(adapter, id): 

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

401 # course has nothing to do with werkzeug. 

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

403 

404 url_map = Map([ 

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

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

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

408 ]) 

409 

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

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

412 

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

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

415 really mean root of that domain. 

416 

417 `alias` 

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

419 endpoint and arguments. 

420 

421 `host` 

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

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

424 that the subdomain feature is disabled. 

425 

426 `websocket` 

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

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

429 requests. 

430 

431 .. versionchanged:: 2.1 

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

433 servers, are considered when routing instead of terminating the 

434 match early. 

435 

436 .. versionadded:: 1.0 

437 Added ``websocket``. 

438 

439 .. versionadded:: 1.0 

440 Added ``merge_slashes``. 

441 

442 .. versionadded:: 0.7 

443 Added ``alias`` and ``host``. 

444 

445 .. versionchanged:: 0.6.1 

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

447 """ 

448 

449 def __init__( 

450 self, 

451 string: str, 

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

453 subdomain: str | None = None, 

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

455 build_only: bool = False, 

456 endpoint: t.Any | None = None, 

457 strict_slashes: bool | None = None, 

458 merge_slashes: bool | None = None, 

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

460 alias: bool = False, 

461 host: str | None = None, 

462 websocket: bool = False, 

463 ) -> None: 

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

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

466 

467 self.rule = string 

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

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

470 

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

472 self.strict_slashes = strict_slashes 

473 self.merge_slashes = merge_slashes 

474 self.subdomain = subdomain 

475 self.host = host 

476 self.defaults = defaults 

477 self.build_only = build_only 

478 self.alias = alias 

479 self.websocket = websocket 

480 

481 if methods is not None: 

482 if isinstance(methods, str): 

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

484 

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

486 

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

488 methods.add("HEAD") 

489 

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

491 raise ValueError( 

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

493 ) 

494 

495 self.methods = methods 

496 self.endpoint: t.Any = endpoint 

497 self.redirect_to = redirect_to 

498 

499 if defaults: 

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

501 else: 

502 self.arguments = set() 

503 

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

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

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

507 

508 def empty(self) -> Rule: 

509 """ 

510 Return an unbound copy of this rule. 

511 

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

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

514 provided to the new copy. 

515 """ 

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

517 

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

519 """ 

520 Provides kwargs for instantiating empty copy with empty() 

521 

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

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

524 has custom keyword arguments that are needed at instantiation. 

525 

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

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

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

529 """ 

530 defaults = None 

531 if self.defaults: 

532 defaults = dict(self.defaults) 

533 return dict( 

534 defaults=defaults, 

535 subdomain=self.subdomain, 

536 methods=self.methods, 

537 build_only=self.build_only, 

538 endpoint=self.endpoint, 

539 strict_slashes=self.strict_slashes, 

540 redirect_to=self.redirect_to, 

541 alias=self.alias, 

542 host=self.host, 

543 ) 

544 

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

546 yield self 

547 

548 def refresh(self) -> None: 

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

550 rule in place. 

551 

552 :internal: 

553 """ 

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

555 

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

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

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

559 

560 :internal: 

561 """ 

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

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

564 self.map = map 

565 if self.strict_slashes is None: 

566 self.strict_slashes = map.strict_slashes 

567 if self.merge_slashes is None: 

568 self.merge_slashes = map.merge_slashes 

569 if self.subdomain is None: 

570 self.subdomain = map.default_subdomain 

571 self.compile() 

572 

573 def get_converter( 

574 self, 

575 variable_name: str, 

576 converter_name: str, 

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

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

579 ) -> BaseConverter: 

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

581 

582 .. versionadded:: 0.9 

583 """ 

584 if converter_name not in self.map.converters: 

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

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

587 

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

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

590 

591 if self.map.sort_parameters: 

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

593 

594 return _urlencode(items) 

595 

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

597 content = "" 

598 static = True 

599 argument_weights = [] 

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

601 final = False 

602 convertor_number = 0 

603 

604 pos = 0 

605 while pos < len(rule): 

606 match = _part_re.match(rule, pos) 

607 if match is None: 

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

609 

610 data = match.groupdict() 

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

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

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

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

615 

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

617 if static: 

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

619 content = re.escape(content) 

620 static = False 

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

622 convobj = self.get_converter( 

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

624 ) 

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

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

627 if not convobj.part_isolating: 

628 final = True 

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

630 convertor_number += 1 

631 argument_weights.append(convobj.weight) 

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

633 

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

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

636 if final: 

637 content += "/" 

638 else: 

639 if not static: 

640 content += r"\Z" 

641 weight = Weighting( 

642 -len(static_weights), 

643 static_weights, 

644 -len(argument_weights), 

645 argument_weights, 

646 ) 

647 yield RulePart( 

648 content=content, 

649 final=final, 

650 static=static, 

651 suffixed=False, 

652 weight=weight, 

653 ) 

654 content = "" 

655 static = True 

656 argument_weights = [] 

657 static_weights = [] 

658 final = False 

659 convertor_number = 0 

660 

661 pos = match.end() 

662 

663 suffixed = False 

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

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

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

667 suffixed = True 

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

669 if not static: 

670 content += r"\Z" 

671 weight = Weighting( 

672 -len(static_weights), 

673 static_weights, 

674 -len(argument_weights), 

675 argument_weights, 

676 ) 

677 yield RulePart( 

678 content=content, 

679 final=final, 

680 static=static, 

681 suffixed=suffixed, 

682 weight=weight, 

683 ) 

684 if suffixed: 

685 yield RulePart( 

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

687 ) 

688 

689 def compile(self) -> None: 

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

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

692 

693 if self.map.host_matching: 

694 domain_rule = self.host or "" 

695 else: 

696 domain_rule = self.subdomain or "" 

697 self._parts = [] 

698 self._trace = [] 

699 self._converters = {} 

700 if domain_rule == "": 

701 self._parts = [ 

702 RulePart( 

703 content="", 

704 final=False, 

705 static=True, 

706 suffixed=False, 

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

708 ) 

709 ] 

710 else: 

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

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

713 rule = self.rule 

714 if self.merge_slashes: 

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

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

717 

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

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

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

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

722 

723 @staticmethod 

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

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

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

727 exec(code, globs, locs) 

728 return locs[name] # type: ignore 

729 

730 def _compile_builder( 

731 self, append_unknown: bool = True 

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

733 defaults = self.defaults or {} 

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

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

736 

737 opl = dom_ops 

738 for is_dynamic, data in self._trace: 

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

740 opl = url_ops 

741 continue 

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

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

744 # resolve it to a constant ahead of time 

745 if is_dynamic and data in defaults: 

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

747 opl.append((False, data)) 

748 elif not is_dynamic: 

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

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

751 else: 

752 opl.append((True, data)) 

753 

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

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

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

757 return ret 

758 

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

760 parts = [ 

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

762 for is_dynamic, elem in ops 

763 ] 

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

765 # constant fold 

766 ret = [parts[0]] 

767 for p in parts[1:]: 

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

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

770 else: 

771 ret.append(p) 

772 return ret 

773 

774 dom_parts = _parts(dom_ops) 

775 url_parts = _parts(url_ops) 

776 if not append_unknown: 

777 body = [] 

778 else: 

779 body = [_IF_KWARGS_URL_ENCODE_AST] 

780 url_parts.extend(_URL_ENCODE_AST_NAMES) 

781 

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

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

784 return parts[0] 

785 return ast.JoinedStr(parts) 

786 

787 body.append( 

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

789 ) 

790 

791 pargs = [ 

792 elem 

793 for is_dynamic, elem in dom_ops + url_ops 

794 if is_dynamic and elem not in defaults 

795 ] 

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

797 

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

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

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

801 for arg in pargs + kargs: 

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

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

804 for _ in kargs: 

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

806 func_ast.body = body 

807 

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

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

810 module = ast.parse("") 

811 module.body = [func_ast] 

812 

813 # mark everything as on line 1, offset 0 

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

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

816 for node in ast.walk(module): 

817 if "lineno" in node._attributes: 

818 node.lineno = 1 

819 if "end_lineno" in node._attributes: 

820 node.end_lineno = node.lineno 

821 if "col_offset" in node._attributes: 

822 node.col_offset = 0 

823 if "end_col_offset" in node._attributes: 

824 node.end_col_offset = node.col_offset 

825 

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

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

828 

829 def build( 

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

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

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

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

834 

835 :internal: 

836 """ 

837 try: 

838 if append_unknown: 

839 return self._build_unknown(**values) 

840 else: 

841 return self._build(**values) 

842 except ValidationError: 

843 return None 

844 

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

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

847 

848 :internal: 

849 """ 

850 return bool( 

851 not self.build_only 

852 and self.defaults 

853 and self.endpoint == rule.endpoint 

854 and self != rule 

855 and self.arguments == rule.arguments 

856 ) 

857 

858 def suitable_for( 

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

860 ) -> bool: 

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

862 

863 :internal: 

864 """ 

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

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

867 if ( 

868 method is not None 

869 and self.methods is not None 

870 and method not in self.methods 

871 ): 

872 return False 

873 

874 defaults = self.defaults or () 

875 

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

877 # the value dictionary otherwise it's not suitable 

878 for key in self.arguments: 

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

880 return False 

881 

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

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

884 if defaults: 

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

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

887 return False 

888 

889 return True 

890 

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

892 """The build compare key for sorting. 

893 

894 :internal: 

895 """ 

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

897 

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

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

900 

901 __hash__ = None # type: ignore 

902 

903 def __str__(self) -> str: 

904 return self.rule 

905 

906 def __repr__(self) -> str: 

907 if self.map is None: 

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

909 parts = [] 

910 for is_dynamic, data in self._trace: 

911 if is_dynamic: 

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

913 else: 

914 parts.append(data) 

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

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

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