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

364 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-25 06:11 +0000

1import ast 

2import re 

3import typing as t 

4from dataclasses import dataclass 

5from string import Template 

6from types import CodeType 

7 

8from .._internal import _to_bytes 

9from ..urls import url_encode 

10from ..urls import url_quote 

11from .converters import ValidationError 

12 

13if t.TYPE_CHECKING: 

14 from .converters import BaseConverter 

15 from .map import Map 

16 

17 

18class Weighting(t.NamedTuple): 

19 number_static_weights: int 

20 static_weights: t.List[t.Tuple[int, int]] 

21 number_argument_weights: int 

22 argument_weights: t.List[int] 

23 

24 

25@dataclass 

26class RulePart: 

27 """A part of a rule. 

28 

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

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

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

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

33 

34 """ 

35 

36 content: str 

37 final: bool 

38 static: bool 

39 weight: Weighting 

40 

41 

42_part_re = re.compile( 

43 r""" 

44 (?: 

45 (?P<slash>\/) # a slash 

46 | 

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

48 | 

49 (?: 

50 < 

51 (?: 

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

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

54 \: # variable delimiter 

55 )? 

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

57 > 

58 ) 

59 ) 

60 """, 

61 re.VERBOSE, 

62) 

63 

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

65_converter_args_re = re.compile( 

66 r""" 

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

68 (?P<value> 

69 True|False| 

70 \d+.\d+| 

71 \d+.| 

72 \d+| 

73 [\w\d_.]+| 

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

75 )\s*, 

76 """, 

77 re.VERBOSE, 

78) 

79 

80 

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

82 

83 

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

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

86 

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

88 """ 

89 try: 

90 return value.index(target, pos) 

91 except ValueError: 

92 return len(value) 

93 

94 

95def _pythonize(value: str) -> t.Union[None, bool, int, float, str]: 

96 if value in _PYTHON_CONSTANTS: 

97 return _PYTHON_CONSTANTS[value] 

98 for convert in int, float: 

99 try: 

100 return convert(value) # type: ignore 

101 except ValueError: 

102 pass 

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

104 value = value[1:-1] 

105 return str(value) 

106 

107 

108def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]: 

109 argstr += "," 

110 args = [] 

111 kwargs = {} 

112 

113 for item in _converter_args_re.finditer(argstr): 

114 value = item.group("stringval") 

115 if value is None: 

116 value = item.group("value") 

117 value = _pythonize(value) 

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

119 args.append(value) 

120 else: 

121 name = item.group("name") 

122 kwargs[name] = value 

123 

124 return tuple(args), kwargs 

125 

126 

127class RuleFactory: 

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

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

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

131 """ 

132 

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

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

135 an iterable of rules.""" 

136 raise NotImplementedError() 

137 

138 

139class Subdomain(RuleFactory): 

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

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

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

143 

144 url_map = Map([ 

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

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

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

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

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

150 ]) 

151 ]) 

152 

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

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

155 for the current request. 

156 """ 

157 

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

159 self.subdomain = subdomain 

160 self.rules = rules 

161 

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

163 for rulefactory in self.rules: 

164 for rule in rulefactory.get_rules(map): 

165 rule = rule.empty() 

166 rule.subdomain = self.subdomain 

167 yield rule 

168 

169 

170class Submount(RuleFactory): 

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

172 

173 url_map = Map([ 

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

175 Submount('/blog', [ 

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

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

178 ]) 

179 ]) 

180 

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

182 """ 

183 

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

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

186 self.rules = rules 

187 

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

189 for rulefactory in self.rules: 

190 for rule in rulefactory.get_rules(map): 

191 rule = rule.empty() 

192 rule.rule = self.path + rule.rule 

193 yield rule 

194 

195 

196class EndpointPrefix(RuleFactory): 

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

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

199 

200 url_map = Map([ 

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

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

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

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

205 ])]) 

206 ]) 

207 """ 

208 

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

210 self.prefix = prefix 

211 self.rules = rules 

212 

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

214 for rulefactory in self.rules: 

215 for rule in rulefactory.get_rules(map): 

216 rule = rule.empty() 

217 rule.endpoint = self.prefix + rule.endpoint 

218 yield rule 

219 

220 

221class RuleTemplate: 

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

223 the endpoint, rule, defaults or subdomain sections. 

224 

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

226 

227 from werkzeug.routing import Map, Rule, RuleTemplate 

228 

229 resource = RuleTemplate([ 

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

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

232 ]) 

233 

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

235 

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

237 replace the placeholders in all the string parameters. 

238 """ 

239 

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

241 self.rules = list(rules) 

242 

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

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

245 

246 

247class RuleTemplateFactory(RuleFactory): 

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

249 `RuleTemplate` internally. 

250 

251 :internal: 

252 """ 

253 

254 def __init__( 

255 self, rules: t.Iterable[RuleFactory], context: t.Dict[str, t.Any] 

256 ) -> None: 

257 self.rules = rules 

258 self.context = context 

259 

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

261 for rulefactory in self.rules: 

262 for rule in rulefactory.get_rules(map): 

263 new_defaults = subdomain = None 

264 if rule.defaults: 

265 new_defaults = {} 

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

267 if isinstance(value, str): 

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

269 new_defaults[key] = value 

270 if rule.subdomain is not None: 

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

272 new_endpoint = rule.endpoint 

273 if isinstance(new_endpoint, str): 

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

275 yield Rule( 

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

277 new_defaults, 

278 subdomain, 

279 rule.methods, 

280 rule.build_only, 

281 new_endpoint, 

282 rule.strict_slashes, 

283 ) 

284 

285 

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

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

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

289 if isinstance(tree, ast.Expr): 

290 tree = tree.value # type: ignore 

291 for node in ast.walk(tree): 

292 if isinstance(node, ast.Name): 

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

294 return tree 

295 

296 

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

298_IF_KWARGS_URL_ENCODE_CODE = """\ 

299if kwargs: 

300 params = self._encode_query_vars(kwargs) 

301 q = "?" if params else "" 

302else: 

303 q = params = "" 

304""" 

305_IF_KWARGS_URL_ENCODE_AST = _prefix_names(_IF_KWARGS_URL_ENCODE_CODE) 

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

307 

308 

309class Rule(RuleFactory): 

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

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

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

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

314 

315 `string` 

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

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

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

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

320 

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

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

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

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

325 

326 The converters are defined on the `Map`. 

327 

328 `endpoint` 

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

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

331 because the endpoint is used for URL generation. 

332 

333 `defaults` 

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

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

336 

337 url_map = Map([ 

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

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

340 ]) 

341 

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

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

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

345 generation. 

346 

347 `subdomain` 

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

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

350 not bound to a subdomain this feature is disabled. 

351 

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

353 and all subdomains are forwarded to your application:: 

354 

355 url_map = Map([ 

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

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

358 ]) 

359 

360 `methods` 

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

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

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

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

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

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

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

368 

369 `strict_slashes` 

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

371 not specified the `Map` setting is used. 

372 

373 `merge_slashes` 

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

375 

376 `build_only` 

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

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

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

380 

381 `redirect_to` 

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

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

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

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

386 rule syntax:: 

387 

388 def foo_with_slug(adapter, id): 

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

390 # course has nothing to do with werkzeug. 

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

392 

393 url_map = Map([ 

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

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

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

397 ]) 

398 

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

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

401 

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

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

404 really mean root of that domain. 

405 

406 `alias` 

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

408 endpoint and arguments. 

409 

410 `host` 

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

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

413 that the subdomain feature is disabled. 

414 

415 `websocket` 

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

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

418 requests. 

419 

420 .. versionchanged:: 2.1 

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

422 servers, are considered when routing instead of terminating the 

423 match early. 

424 

425 .. versionadded:: 1.0 

426 Added ``websocket``. 

427 

428 .. versionadded:: 1.0 

429 Added ``merge_slashes``. 

430 

431 .. versionadded:: 0.7 

432 Added ``alias`` and ``host``. 

433 

434 .. versionchanged:: 0.6.1 

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

436 """ 

437 

438 def __init__( 

439 self, 

440 string: str, 

441 defaults: t.Optional[t.Mapping[str, t.Any]] = None, 

442 subdomain: t.Optional[str] = None, 

443 methods: t.Optional[t.Iterable[str]] = None, 

444 build_only: bool = False, 

445 endpoint: t.Optional[str] = None, 

446 strict_slashes: t.Optional[bool] = None, 

447 merge_slashes: t.Optional[bool] = None, 

448 redirect_to: t.Optional[t.Union[str, t.Callable[..., str]]] = None, 

449 alias: bool = False, 

450 host: t.Optional[str] = None, 

451 websocket: bool = False, 

452 ) -> None: 

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

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

455 self.rule = string 

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

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

458 

459 self.map: "Map" = None # type: ignore 

460 self.strict_slashes = strict_slashes 

461 self.merge_slashes = merge_slashes 

462 self.subdomain = subdomain 

463 self.host = host 

464 self.defaults = defaults 

465 self.build_only = build_only 

466 self.alias = alias 

467 self.websocket = websocket 

468 

469 if methods is not None: 

470 if isinstance(methods, str): 

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

472 

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

474 

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

476 methods.add("HEAD") 

477 

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

479 raise ValueError( 

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

481 ) 

482 

483 self.methods = methods 

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

485 self.redirect_to = redirect_to 

486 

487 if defaults: 

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

489 else: 

490 self.arguments = set() 

491 

492 self._converters: t.Dict[str, "BaseConverter"] = {} 

493 self._trace: t.List[t.Tuple[bool, str]] = [] 

494 self._parts: t.List[RulePart] = [] 

495 

496 def empty(self) -> "Rule": 

497 """ 

498 Return an unbound copy of this rule. 

499 

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

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

502 provided to the new copy. 

503 """ 

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

505 

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

507 """ 

508 Provides kwargs for instantiating empty copy with empty() 

509 

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

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

512 has custom keyword arguments that are needed at instantiation. 

513 

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

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

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

517 """ 

518 defaults = None 

519 if self.defaults: 

520 defaults = dict(self.defaults) 

521 return dict( 

522 defaults=defaults, 

523 subdomain=self.subdomain, 

524 methods=self.methods, 

525 build_only=self.build_only, 

526 endpoint=self.endpoint, 

527 strict_slashes=self.strict_slashes, 

528 redirect_to=self.redirect_to, 

529 alias=self.alias, 

530 host=self.host, 

531 ) 

532 

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

534 yield self 

535 

536 def refresh(self) -> None: 

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

538 rule in place. 

539 

540 :internal: 

541 """ 

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

543 

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

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

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

547 

548 :internal: 

549 """ 

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

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

552 self.map = map 

553 if self.strict_slashes is None: 

554 self.strict_slashes = map.strict_slashes 

555 if self.merge_slashes is None: 

556 self.merge_slashes = map.merge_slashes 

557 if self.subdomain is None: 

558 self.subdomain = map.default_subdomain 

559 self.compile() 

560 

561 def get_converter( 

562 self, 

563 variable_name: str, 

564 converter_name: str, 

565 args: t.Tuple, 

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

567 ) -> "BaseConverter": 

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

569 

570 .. versionadded:: 0.9 

571 """ 

572 if converter_name not in self.map.converters: 

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

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

575 

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

577 return url_encode( 

578 query_vars, 

579 charset=self.map.charset, 

580 sort=self.map.sort_parameters, 

581 key=self.map.sort_key, 

582 ) 

583 

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

585 content = "" 

586 static = True 

587 argument_weights = [] 

588 static_weights: t.List[t.Tuple[int, int]] = [] 

589 final = False 

590 

591 pos = 0 

592 while pos < len(rule): 

593 match = _part_re.match(rule, pos) 

594 if match is None: 

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

596 

597 data = match.groupdict() 

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

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

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

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

602 

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

604 if static: 

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

606 content = re.escape(content) 

607 static = False 

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

609 convobj = self.get_converter( 

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

611 ) 

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

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

614 if not convobj.part_isolating: 

615 final = True 

616 content += f"({convobj.regex})" 

617 argument_weights.append(convobj.weight) 

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

619 

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

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

622 if final: 

623 content += "/" 

624 else: 

625 if not static: 

626 content += r"\Z" 

627 weight = Weighting( 

628 -len(static_weights), 

629 static_weights, 

630 -len(argument_weights), 

631 argument_weights, 

632 ) 

633 yield RulePart( 

634 content=content, final=final, static=static, weight=weight 

635 ) 

636 content = "" 

637 static = True 

638 argument_weights = [] 

639 static_weights = [] 

640 final = False 

641 

642 pos = match.end() 

643 

644 if not static: 

645 content += r"\Z" 

646 weight = Weighting( 

647 -len(static_weights), 

648 static_weights, 

649 -len(argument_weights), 

650 argument_weights, 

651 ) 

652 yield RulePart(content=content, final=final, static=static, weight=weight) 

653 

654 def compile(self) -> None: 

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

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

657 

658 if self.map.host_matching: 

659 domain_rule = self.host or "" 

660 else: 

661 domain_rule = self.subdomain or "" 

662 self._parts = [] 

663 self._trace = [] 

664 self._converters = {} 

665 if domain_rule == "": 

666 self._parts = [ 

667 RulePart( 

668 content="", final=False, static=True, weight=Weighting(0, [], 0, []) 

669 ) 

670 ] 

671 else: 

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

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

674 rule = self.rule 

675 if self.merge_slashes: 

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

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

678 

679 self._build: t.Callable[..., t.Tuple[str, str]] 

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

681 self._build_unknown: t.Callable[..., t.Tuple[str, str]] 

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

683 

684 @staticmethod 

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

686 globs: t.Dict[str, t.Any] = {} 

687 locs: t.Dict[str, t.Any] = {} 

688 exec(code, globs, locs) 

689 return locs[name] # type: ignore 

690 

691 def _compile_builder( 

692 self, append_unknown: bool = True 

693 ) -> t.Callable[..., t.Tuple[str, str]]: 

694 defaults = self.defaults or {} 

695 dom_ops: t.List[t.Tuple[bool, str]] = [] 

696 url_ops: t.List[t.Tuple[bool, str]] = [] 

697 

698 opl = dom_ops 

699 for is_dynamic, data in self._trace: 

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

701 opl = url_ops 

702 continue 

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

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

705 # resolve it to a constant ahead of time 

706 if is_dynamic and data in defaults: 

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

708 opl.append((False, data)) 

709 elif not is_dynamic: 

710 opl.append( 

711 (False, url_quote(_to_bytes(data, self.map.charset), safe="/:|+")) 

712 ) 

713 else: 

714 opl.append((True, data)) 

715 

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

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

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

719 return ret 

720 

721 def _parts(ops: t.List[t.Tuple[bool, str]]) -> t.List[ast.AST]: 

722 parts = [ 

723 _convert(elem) if is_dynamic else ast.Str(s=elem) 

724 for is_dynamic, elem in ops 

725 ] 

726 parts = parts or [ast.Str("")] 

727 # constant fold 

728 ret = [parts[0]] 

729 for p in parts[1:]: 

730 if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str): 

731 ret[-1] = ast.Str(ret[-1].s + p.s) 

732 else: 

733 ret.append(p) 

734 return ret 

735 

736 dom_parts = _parts(dom_ops) 

737 url_parts = _parts(url_ops) 

738 if not append_unknown: 

739 body = [] 

740 else: 

741 body = [_IF_KWARGS_URL_ENCODE_AST] 

742 url_parts.extend(_URL_ENCODE_AST_NAMES) 

743 

744 def _join(parts: t.List[ast.AST]) -> ast.AST: 

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

746 return parts[0] 

747 return ast.JoinedStr(parts) 

748 

749 body.append( 

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

751 ) 

752 

753 pargs = [ 

754 elem 

755 for is_dynamic, elem in dom_ops + url_ops 

756 if is_dynamic and elem not in defaults 

757 ] 

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

759 

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

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

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

763 for arg in pargs + kargs: 

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

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

766 for _ in kargs: 

767 func_ast.args.defaults.append(ast.Str("")) 

768 func_ast.body = body 

769 

770 # use `ast.parse` instead of `ast.Module` for better portability 

771 # Python 3.8 changes the signature of `ast.Module` 

772 module = ast.parse("") 

773 module.body = [func_ast] 

774 

775 # mark everything as on line 1, offset 0 

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

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

778 for node in ast.walk(module): 

779 if "lineno" in node._attributes: 

780 node.lineno = 1 

781 if "end_lineno" in node._attributes: 

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

783 if "col_offset" in node._attributes: 

784 node.col_offset = 0 

785 if "end_col_offset" in node._attributes: 

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

787 

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

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

790 

791 def build( 

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

793 ) -> t.Optional[t.Tuple[str, str]]: 

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

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

796 

797 :internal: 

798 """ 

799 try: 

800 if append_unknown: 

801 return self._build_unknown(**values) 

802 else: 

803 return self._build(**values) 

804 except ValidationError: 

805 return None 

806 

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

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

809 

810 :internal: 

811 """ 

812 return bool( 

813 not self.build_only 

814 and self.defaults 

815 and self.endpoint == rule.endpoint 

816 and self != rule 

817 and self.arguments == rule.arguments 

818 ) 

819 

820 def suitable_for( 

821 self, values: t.Mapping[str, t.Any], method: t.Optional[str] = None 

822 ) -> bool: 

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

824 

825 :internal: 

826 """ 

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

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

829 if ( 

830 method is not None 

831 and self.methods is not None 

832 and method not in self.methods 

833 ): 

834 return False 

835 

836 defaults = self.defaults or () 

837 

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

839 # the value dictionary otherwise it's not suitable 

840 for key in self.arguments: 

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

842 return False 

843 

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

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

846 if defaults: 

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

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

849 return False 

850 

851 return True 

852 

853 def build_compare_key(self) -> t.Tuple[int, int, int]: 

854 """The build compare key for sorting. 

855 

856 :internal: 

857 """ 

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

859 

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

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

862 

863 __hash__ = None # type: ignore 

864 

865 def __str__(self) -> str: 

866 return self.rule 

867 

868 def __repr__(self) -> str: 

869 if self.map is None: 

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

871 parts = [] 

872 for is_dynamic, data in self._trace: 

873 if is_dynamic: 

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

875 else: 

876 parts.append(data) 

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

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

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