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

311 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-09 07:17 +0000

1from __future__ import annotations 

2 

3import typing as t 

4import warnings 

5from pprint import pformat 

6from threading import Lock 

7from urllib.parse import quote 

8from urllib.parse import urljoin 

9from urllib.parse import urlunsplit 

10 

11from .._internal import _get_environ 

12from .._internal import _wsgi_decoding_dance 

13from ..datastructures import ImmutableDict 

14from ..datastructures import MultiDict 

15from ..exceptions import BadHost 

16from ..exceptions import HTTPException 

17from ..exceptions import MethodNotAllowed 

18from ..exceptions import NotFound 

19from ..urls import _urlencode 

20from ..wsgi import get_host 

21from .converters import DEFAULT_CONVERTERS 

22from .exceptions import BuildError 

23from .exceptions import NoMatch 

24from .exceptions import RequestAliasRedirect 

25from .exceptions import RequestPath 

26from .exceptions import RequestRedirect 

27from .exceptions import WebsocketMismatch 

28from .matcher import StateMachineMatcher 

29from .rules import _simple_rule_re 

30from .rules import Rule 

31 

32if t.TYPE_CHECKING: 

33 from _typeshed.wsgi import WSGIApplication 

34 from _typeshed.wsgi import WSGIEnvironment 

35 from .converters import BaseConverter 

36 from .rules import RuleFactory 

37 from ..wrappers.request import Request 

38 

39 

40class Map: 

41 """The map class stores all the URL rules and some configuration 

42 parameters. Some of the configuration values are only stored on the 

43 `Map` instance since those affect all rules, others are just defaults 

44 and can be overridden for each rule. Note that you have to specify all 

45 arguments besides the `rules` as keyword arguments! 

46 

47 :param rules: sequence of url rules for this map. 

48 :param default_subdomain: The default subdomain for rules without a 

49 subdomain defined. 

50 :param strict_slashes: If a rule ends with a slash but the matched 

51 URL does not, redirect to the URL with a trailing slash. 

52 :param merge_slashes: Merge consecutive slashes when matching or 

53 building URLs. Matches will redirect to the normalized URL. 

54 Slashes in variable parts are not merged. 

55 :param redirect_defaults: This will redirect to the default rule if it 

56 wasn't visited that way. This helps creating 

57 unique URLs. 

58 :param converters: A dict of converters that adds additional converters 

59 to the list of converters. If you redefine one 

60 converter this will override the original one. 

61 :param sort_parameters: If set to `True` the url parameters are sorted. 

62 See `url_encode` for more details. 

63 :param sort_key: The sort key function for `url_encode`. 

64 :param host_matching: if set to `True` it enables the host matching 

65 feature and disables the subdomain one. If 

66 enabled the `host` parameter to rules is used 

67 instead of the `subdomain` one. 

68 

69 .. versionchanged:: 3.0 

70 The ``charset`` and ``encoding_errors`` parameters were removed. 

71 

72 .. versionchanged:: 1.0 

73 If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match. 

74 

75 .. versionchanged:: 1.0 

76 The ``merge_slashes`` parameter was added. 

77 

78 .. versionchanged:: 0.7 

79 The ``encoding_errors`` and ``host_matching`` parameters were added. 

80 

81 .. versionchanged:: 0.5 

82 The ``sort_parameters`` and ``sort_key`` paramters were added. 

83 """ 

84 

85 #: A dict of default converters to be used. 

86 default_converters = ImmutableDict(DEFAULT_CONVERTERS) 

87 

88 #: The type of lock to use when updating. 

89 #: 

90 #: .. versionadded:: 1.0 

91 lock_class = Lock 

92 

93 def __init__( 

94 self, 

95 rules: t.Iterable[RuleFactory] | None = None, 

96 default_subdomain: str = "", 

97 strict_slashes: bool = True, 

98 merge_slashes: bool = True, 

99 redirect_defaults: bool = True, 

100 converters: t.Mapping[str, type[BaseConverter]] | None = None, 

101 sort_parameters: bool = False, 

102 sort_key: t.Callable[[t.Any], t.Any] | None = None, 

103 host_matching: bool = False, 

104 ) -> None: 

105 self._matcher = StateMachineMatcher(merge_slashes) 

106 self._rules_by_endpoint: dict[str, list[Rule]] = {} 

107 self._remap = True 

108 self._remap_lock = self.lock_class() 

109 

110 self.default_subdomain = default_subdomain 

111 self.strict_slashes = strict_slashes 

112 self.merge_slashes = merge_slashes 

113 self.redirect_defaults = redirect_defaults 

114 self.host_matching = host_matching 

115 

116 self.converters = self.default_converters.copy() 

117 if converters: 

118 self.converters.update(converters) 

119 

120 self.sort_parameters = sort_parameters 

121 self.sort_key = sort_key 

122 

123 for rulefactory in rules or (): 

124 self.add(rulefactory) 

125 

126 def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool: 

127 """Iterate over all rules and check if the endpoint expects 

128 the arguments provided. This is for example useful if you have 

129 some URLs that expect a language code and others that do not and 

130 you want to wrap the builder a bit so that the current language 

131 code is automatically added if not provided but endpoints expect 

132 it. 

133 

134 :param endpoint: the endpoint to check. 

135 :param arguments: this function accepts one or more arguments 

136 as positional arguments. Each one of them is 

137 checked. 

138 """ 

139 self.update() 

140 arguments = set(arguments) 

141 for rule in self._rules_by_endpoint[endpoint]: 

142 if arguments.issubset(rule.arguments): 

143 return True 

144 return False 

145 

146 @property 

147 def _rules(self) -> list[Rule]: 

148 return [rule for rules in self._rules_by_endpoint.values() for rule in rules] 

149 

150 def iter_rules(self, endpoint: str | None = None) -> t.Iterator[Rule]: 

151 """Iterate over all rules or the rules of an endpoint. 

152 

153 :param endpoint: if provided only the rules for that endpoint 

154 are returned. 

155 :return: an iterator 

156 """ 

157 self.update() 

158 if endpoint is not None: 

159 return iter(self._rules_by_endpoint[endpoint]) 

160 return iter(self._rules) 

161 

162 def add(self, rulefactory: RuleFactory) -> None: 

163 """Add a new rule or factory to the map and bind it. Requires that the 

164 rule is not bound to another map. 

165 

166 :param rulefactory: a :class:`Rule` or :class:`RuleFactory` 

167 """ 

168 for rule in rulefactory.get_rules(self): 

169 rule.bind(self) 

170 if not rule.build_only: 

171 self._matcher.add(rule) 

172 self._rules_by_endpoint.setdefault(rule.endpoint, []).append(rule) 

173 self._remap = True 

174 

175 def bind( 

176 self, 

177 server_name: str, 

178 script_name: str | None = None, 

179 subdomain: str | None = None, 

180 url_scheme: str = "http", 

181 default_method: str = "GET", 

182 path_info: str | None = None, 

183 query_args: t.Mapping[str, t.Any] | str | None = None, 

184 ) -> MapAdapter: 

185 """Return a new :class:`MapAdapter` with the details specified to the 

186 call. Note that `script_name` will default to ``'/'`` if not further 

187 specified or `None`. The `server_name` at least is a requirement 

188 because the HTTP RFC requires absolute URLs for redirects and so all 

189 redirect exceptions raised by Werkzeug will contain the full canonical 

190 URL. 

191 

192 If no path_info is passed to :meth:`match` it will use the default path 

193 info passed to bind. While this doesn't really make sense for 

194 manual bind calls, it's useful if you bind a map to a WSGI 

195 environment which already contains the path info. 

196 

197 `subdomain` will default to the `default_subdomain` for this map if 

198 no defined. If there is no `default_subdomain` you cannot use the 

199 subdomain feature. 

200 

201 .. versionchanged:: 1.0 

202 If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules 

203 will match. 

204 

205 .. versionchanged:: 0.15 

206 ``path_info`` defaults to ``'/'`` if ``None``. 

207 

208 .. versionchanged:: 0.8 

209 ``query_args`` can be a string. 

210 

211 .. versionchanged:: 0.7 

212 Added ``query_args``. 

213 """ 

214 server_name = server_name.lower() 

215 if self.host_matching: 

216 if subdomain is not None: 

217 raise RuntimeError("host matching enabled and a subdomain was provided") 

218 elif subdomain is None: 

219 subdomain = self.default_subdomain 

220 if script_name is None: 

221 script_name = "/" 

222 if path_info is None: 

223 path_info = "/" 

224 

225 # Port isn't part of IDNA, and might push a name over the 63 octet limit. 

226 server_name, port_sep, port = server_name.partition(":") 

227 

228 try: 

229 server_name = server_name.encode("idna").decode("ascii") 

230 except UnicodeError as e: 

231 raise BadHost() from e 

232 

233 return MapAdapter( 

234 self, 

235 f"{server_name}{port_sep}{port}", 

236 script_name, 

237 subdomain, 

238 url_scheme, 

239 path_info, 

240 default_method, 

241 query_args, 

242 ) 

243 

244 def bind_to_environ( 

245 self, 

246 environ: WSGIEnvironment | Request, 

247 server_name: str | None = None, 

248 subdomain: str | None = None, 

249 ) -> MapAdapter: 

250 """Like :meth:`bind` but you can pass it an WSGI environment and it 

251 will fetch the information from that dictionary. Note that because of 

252 limitations in the protocol there is no way to get the current 

253 subdomain and real `server_name` from the environment. If you don't 

254 provide it, Werkzeug will use `SERVER_NAME` and `SERVER_PORT` (or 

255 `HTTP_HOST` if provided) as used `server_name` with disabled subdomain 

256 feature. 

257 

258 If `subdomain` is `None` but an environment and a server name is 

259 provided it will calculate the current subdomain automatically. 

260 Example: `server_name` is ``'example.com'`` and the `SERVER_NAME` 

261 in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated 

262 subdomain will be ``'staging.dev'``. 

263 

264 If the object passed as environ has an environ attribute, the value of 

265 this attribute is used instead. This allows you to pass request 

266 objects. Additionally `PATH_INFO` added as a default of the 

267 :class:`MapAdapter` so that you don't have to pass the path info to 

268 the match method. 

269 

270 .. versionchanged:: 1.0.0 

271 If the passed server name specifies port 443, it will match 

272 if the incoming scheme is ``https`` without a port. 

273 

274 .. versionchanged:: 1.0.0 

275 A warning is shown when the passed server name does not 

276 match the incoming WSGI server name. 

277 

278 .. versionchanged:: 0.8 

279 This will no longer raise a ValueError when an unexpected server 

280 name was passed. 

281 

282 .. versionchanged:: 0.5 

283 previously this method accepted a bogus `calculate_subdomain` 

284 parameter that did not have any effect. It was removed because 

285 of that. 

286 

287 :param environ: a WSGI environment. 

288 :param server_name: an optional server name hint (see above). 

289 :param subdomain: optionally the current subdomain (see above). 

290 """ 

291 env = _get_environ(environ) 

292 wsgi_server_name = get_host(env).lower() 

293 scheme = env["wsgi.url_scheme"] 

294 upgrade = any( 

295 v.strip() == "upgrade" 

296 for v in env.get("HTTP_CONNECTION", "").lower().split(",") 

297 ) 

298 

299 if upgrade and env.get("HTTP_UPGRADE", "").lower() == "websocket": 

300 scheme = "wss" if scheme == "https" else "ws" 

301 

302 if server_name is None: 

303 server_name = wsgi_server_name 

304 else: 

305 server_name = server_name.lower() 

306 

307 # strip standard port to match get_host() 

308 if scheme in {"http", "ws"} and server_name.endswith(":80"): 

309 server_name = server_name[:-3] 

310 elif scheme in {"https", "wss"} and server_name.endswith(":443"): 

311 server_name = server_name[:-4] 

312 

313 if subdomain is None and not self.host_matching: 

314 cur_server_name = wsgi_server_name.split(".") 

315 real_server_name = server_name.split(".") 

316 offset = -len(real_server_name) 

317 

318 if cur_server_name[offset:] != real_server_name: 

319 # This can happen even with valid configs if the server was 

320 # accessed directly by IP address under some situations. 

321 # Instead of raising an exception like in Werkzeug 0.7 or 

322 # earlier we go by an invalid subdomain which will result 

323 # in a 404 error on matching. 

324 warnings.warn( 

325 f"Current server name {wsgi_server_name!r} doesn't match configured" 

326 f" server name {server_name!r}", 

327 stacklevel=2, 

328 ) 

329 subdomain = "<invalid>" 

330 else: 

331 subdomain = ".".join(filter(None, cur_server_name[:offset])) 

332 

333 def _get_wsgi_string(name: str) -> str | None: 

334 val = env.get(name) 

335 if val is not None: 

336 return _wsgi_decoding_dance(val) 

337 return None 

338 

339 script_name = _get_wsgi_string("SCRIPT_NAME") 

340 path_info = _get_wsgi_string("PATH_INFO") 

341 query_args = _get_wsgi_string("QUERY_STRING") 

342 return Map.bind( 

343 self, 

344 server_name, 

345 script_name, 

346 subdomain, 

347 scheme, 

348 env["REQUEST_METHOD"], 

349 path_info, 

350 query_args=query_args, 

351 ) 

352 

353 def update(self) -> None: 

354 """Called before matching and building to keep the compiled rules 

355 in the correct order after things changed. 

356 """ 

357 if not self._remap: 

358 return 

359 

360 with self._remap_lock: 

361 if not self._remap: 

362 return 

363 

364 self._matcher.update() 

365 for rules in self._rules_by_endpoint.values(): 

366 rules.sort(key=lambda x: x.build_compare_key()) 

367 self._remap = False 

368 

369 def __repr__(self) -> str: 

370 rules = self.iter_rules() 

371 return f"{type(self).__name__}({pformat(list(rules))})" 

372 

373 

374class MapAdapter: 

375 

376 """Returned by :meth:`Map.bind` or :meth:`Map.bind_to_environ` and does 

377 the URL matching and building based on runtime information. 

378 """ 

379 

380 def __init__( 

381 self, 

382 map: Map, 

383 server_name: str, 

384 script_name: str, 

385 subdomain: str | None, 

386 url_scheme: str, 

387 path_info: str, 

388 default_method: str, 

389 query_args: t.Mapping[str, t.Any] | str | None = None, 

390 ): 

391 self.map = map 

392 self.server_name = server_name 

393 

394 if not script_name.endswith("/"): 

395 script_name += "/" 

396 

397 self.script_name = script_name 

398 self.subdomain = subdomain 

399 self.url_scheme = url_scheme 

400 self.path_info = path_info 

401 self.default_method = default_method 

402 self.query_args = query_args 

403 self.websocket = self.url_scheme in {"ws", "wss"} 

404 

405 def dispatch( 

406 self, 

407 view_func: t.Callable[[str, t.Mapping[str, t.Any]], WSGIApplication], 

408 path_info: str | None = None, 

409 method: str | None = None, 

410 catch_http_exceptions: bool = False, 

411 ) -> WSGIApplication: 

412 """Does the complete dispatching process. `view_func` is called with 

413 the endpoint and a dict with the values for the view. It should 

414 look up the view function, call it, and return a response object 

415 or WSGI application. http exceptions are not caught by default 

416 so that applications can display nicer error messages by just 

417 catching them by hand. If you want to stick with the default 

418 error messages you can pass it ``catch_http_exceptions=True`` and 

419 it will catch the http exceptions. 

420 

421 Here a small example for the dispatch usage:: 

422 

423 from werkzeug.wrappers import Request, Response 

424 from werkzeug.wsgi import responder 

425 from werkzeug.routing import Map, Rule 

426 

427 def on_index(request): 

428 return Response('Hello from the index') 

429 

430 url_map = Map([Rule('/', endpoint='index')]) 

431 views = {'index': on_index} 

432 

433 @responder 

434 def application(environ, start_response): 

435 request = Request(environ) 

436 urls = url_map.bind_to_environ(environ) 

437 return urls.dispatch(lambda e, v: views[e](request, **v), 

438 catch_http_exceptions=True) 

439 

440 Keep in mind that this method might return exception objects, too, so 

441 use :class:`Response.force_type` to get a response object. 

442 

443 :param view_func: a function that is called with the endpoint as 

444 first argument and the value dict as second. Has 

445 to dispatch to the actual view function with this 

446 information. (see above) 

447 :param path_info: the path info to use for matching. Overrides the 

448 path info specified on binding. 

449 :param method: the HTTP method used for matching. Overrides the 

450 method specified on binding. 

451 :param catch_http_exceptions: set to `True` to catch any of the 

452 werkzeug :class:`HTTPException`\\s. 

453 """ 

454 try: 

455 try: 

456 endpoint, args = self.match(path_info, method) 

457 except RequestRedirect as e: 

458 return e 

459 return view_func(endpoint, args) 

460 except HTTPException as e: 

461 if catch_http_exceptions: 

462 return e 

463 raise 

464 

465 @t.overload 

466 def match( # type: ignore 

467 self, 

468 path_info: str | None = None, 

469 method: str | None = None, 

470 return_rule: t.Literal[False] = False, 

471 query_args: t.Mapping[str, t.Any] | str | None = None, 

472 websocket: bool | None = None, 

473 ) -> tuple[str, t.Mapping[str, t.Any]]: 

474 ... 

475 

476 @t.overload 

477 def match( 

478 self, 

479 path_info: str | None = None, 

480 method: str | None = None, 

481 return_rule: t.Literal[True] = True, 

482 query_args: t.Mapping[str, t.Any] | str | None = None, 

483 websocket: bool | None = None, 

484 ) -> tuple[Rule, t.Mapping[str, t.Any]]: 

485 ... 

486 

487 def match( 

488 self, 

489 path_info: str | None = None, 

490 method: str | None = None, 

491 return_rule: bool = False, 

492 query_args: t.Mapping[str, t.Any] | str | None = None, 

493 websocket: bool | None = None, 

494 ) -> tuple[str | Rule, t.Mapping[str, t.Any]]: 

495 """The usage is simple: you just pass the match method the current 

496 path info as well as the method (which defaults to `GET`). The 

497 following things can then happen: 

498 

499 - you receive a `NotFound` exception that indicates that no URL is 

500 matching. A `NotFound` exception is also a WSGI application you 

501 can call to get a default page not found page (happens to be the 

502 same object as `werkzeug.exceptions.NotFound`) 

503 

504 - you receive a `MethodNotAllowed` exception that indicates that there 

505 is a match for this URL but not for the current request method. 

506 This is useful for RESTful applications. 

507 

508 - you receive a `RequestRedirect` exception with a `new_url` 

509 attribute. This exception is used to notify you about a request 

510 Werkzeug requests from your WSGI application. This is for example the 

511 case if you request ``/foo`` although the correct URL is ``/foo/`` 

512 You can use the `RequestRedirect` instance as response-like object 

513 similar to all other subclasses of `HTTPException`. 

514 

515 - you receive a ``WebsocketMismatch`` exception if the only 

516 match is a WebSocket rule but the bind is an HTTP request, or 

517 if the match is an HTTP rule but the bind is a WebSocket 

518 request. 

519 

520 - you get a tuple in the form ``(endpoint, arguments)`` if there is 

521 a match (unless `return_rule` is True, in which case you get a tuple 

522 in the form ``(rule, arguments)``) 

523 

524 If the path info is not passed to the match method the default path 

525 info of the map is used (defaults to the root URL if not defined 

526 explicitly). 

527 

528 All of the exceptions raised are subclasses of `HTTPException` so they 

529 can be used as WSGI responses. They will all render generic error or 

530 redirect pages. 

531 

532 Here is a small example for matching: 

533 

534 >>> m = Map([ 

535 ... Rule('/', endpoint='index'), 

536 ... Rule('/downloads/', endpoint='downloads/index'), 

537 ... Rule('/downloads/<int:id>', endpoint='downloads/show') 

538 ... ]) 

539 >>> urls = m.bind("example.com", "/") 

540 >>> urls.match("/", "GET") 

541 ('index', {}) 

542 >>> urls.match("/downloads/42") 

543 ('downloads/show', {'id': 42}) 

544 

545 And here is what happens on redirect and missing URLs: 

546 

547 >>> urls.match("/downloads") 

548 Traceback (most recent call last): 

549 ... 

550 RequestRedirect: http://example.com/downloads/ 

551 >>> urls.match("/missing") 

552 Traceback (most recent call last): 

553 ... 

554 NotFound: 404 Not Found 

555 

556 :param path_info: the path info to use for matching. Overrides the 

557 path info specified on binding. 

558 :param method: the HTTP method used for matching. Overrides the 

559 method specified on binding. 

560 :param return_rule: return the rule that matched instead of just the 

561 endpoint (defaults to `False`). 

562 :param query_args: optional query arguments that are used for 

563 automatic redirects as string or dictionary. It's 

564 currently not possible to use the query arguments 

565 for URL matching. 

566 :param websocket: Match WebSocket instead of HTTP requests. A 

567 websocket request has a ``ws`` or ``wss`` 

568 :attr:`url_scheme`. This overrides that detection. 

569 

570 .. versionadded:: 1.0 

571 Added ``websocket``. 

572 

573 .. versionchanged:: 0.8 

574 ``query_args`` can be a string. 

575 

576 .. versionadded:: 0.7 

577 Added ``query_args``. 

578 

579 .. versionadded:: 0.6 

580 Added ``return_rule``. 

581 """ 

582 self.map.update() 

583 if path_info is None: 

584 path_info = self.path_info 

585 if query_args is None: 

586 query_args = self.query_args or {} 

587 method = (method or self.default_method).upper() 

588 

589 if websocket is None: 

590 websocket = self.websocket 

591 

592 domain_part = self.server_name 

593 

594 if not self.map.host_matching and self.subdomain is not None: 

595 domain_part = self.subdomain 

596 

597 path_part = f"/{path_info.lstrip('/')}" if path_info else "" 

598 

599 try: 

600 result = self.map._matcher.match(domain_part, path_part, method, websocket) 

601 except RequestPath as e: 

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

603 new_path = quote(e.path_info, safe="!$&'()*+,/:;=@") 

604 raise RequestRedirect( 

605 self.make_redirect_url(new_path, query_args) 

606 ) from None 

607 except RequestAliasRedirect as e: 

608 raise RequestRedirect( 

609 self.make_alias_redirect_url( 

610 f"{domain_part}|{path_part}", 

611 e.endpoint, 

612 e.matched_values, 

613 method, 

614 query_args, 

615 ) 

616 ) from None 

617 except NoMatch as e: 

618 if e.have_match_for: 

619 raise MethodNotAllowed(valid_methods=list(e.have_match_for)) from None 

620 

621 if e.websocket_mismatch: 

622 raise WebsocketMismatch() from None 

623 

624 raise NotFound() from None 

625 else: 

626 rule, rv = result 

627 

628 if self.map.redirect_defaults: 

629 redirect_url = self.get_default_redirect(rule, method, rv, query_args) 

630 if redirect_url is not None: 

631 raise RequestRedirect(redirect_url) 

632 

633 if rule.redirect_to is not None: 

634 if isinstance(rule.redirect_to, str): 

635 

636 def _handle_match(match: t.Match[str]) -> str: 

637 value = rv[match.group(1)] 

638 return rule._converters[match.group(1)].to_url(value) 

639 

640 redirect_url = _simple_rule_re.sub(_handle_match, rule.redirect_to) 

641 else: 

642 redirect_url = rule.redirect_to(self, **rv) 

643 

644 if self.subdomain: 

645 netloc = f"{self.subdomain}.{self.server_name}" 

646 else: 

647 netloc = self.server_name 

648 

649 raise RequestRedirect( 

650 urljoin( 

651 f"{self.url_scheme or 'http'}://{netloc}{self.script_name}", 

652 redirect_url, 

653 ) 

654 ) 

655 

656 if return_rule: 

657 return rule, rv 

658 else: 

659 return rule.endpoint, rv 

660 

661 def test(self, path_info: str | None = None, method: str | None = None) -> bool: 

662 """Test if a rule would match. Works like `match` but returns `True` 

663 if the URL matches, or `False` if it does not exist. 

664 

665 :param path_info: the path info to use for matching. Overrides the 

666 path info specified on binding. 

667 :param method: the HTTP method used for matching. Overrides the 

668 method specified on binding. 

669 """ 

670 try: 

671 self.match(path_info, method) 

672 except RequestRedirect: 

673 pass 

674 except HTTPException: 

675 return False 

676 return True 

677 

678 def allowed_methods(self, path_info: str | None = None) -> t.Iterable[str]: 

679 """Returns the valid methods that match for a given path. 

680 

681 .. versionadded:: 0.7 

682 """ 

683 try: 

684 self.match(path_info, method="--") 

685 except MethodNotAllowed as e: 

686 return e.valid_methods # type: ignore 

687 except HTTPException: 

688 pass 

689 return [] 

690 

691 def get_host(self, domain_part: str | None) -> str: 

692 """Figures out the full host name for the given domain part. The 

693 domain part is a subdomain in case host matching is disabled or 

694 a full host name. 

695 """ 

696 if self.map.host_matching: 

697 if domain_part is None: 

698 return self.server_name 

699 

700 return domain_part 

701 

702 if domain_part is None: 

703 subdomain = self.subdomain 

704 else: 

705 subdomain = domain_part 

706 

707 if subdomain: 

708 return f"{subdomain}.{self.server_name}" 

709 else: 

710 return self.server_name 

711 

712 def get_default_redirect( 

713 self, 

714 rule: Rule, 

715 method: str, 

716 values: t.MutableMapping[str, t.Any], 

717 query_args: t.Mapping[str, t.Any] | str, 

718 ) -> str | None: 

719 """A helper that returns the URL to redirect to if it finds one. 

720 This is used for default redirecting only. 

721 

722 :internal: 

723 """ 

724 assert self.map.redirect_defaults 

725 for r in self.map._rules_by_endpoint[rule.endpoint]: 

726 # every rule that comes after this one, including ourself 

727 # has a lower priority for the defaults. We order the ones 

728 # with the highest priority up for building. 

729 if r is rule: 

730 break 

731 if r.provides_defaults_for(rule) and r.suitable_for(values, method): 

732 values.update(r.defaults) # type: ignore 

733 domain_part, path = r.build(values) # type: ignore 

734 return self.make_redirect_url(path, query_args, domain_part=domain_part) 

735 return None 

736 

737 def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str: 

738 if not isinstance(query_args, str): 

739 return _urlencode(query_args) 

740 return query_args 

741 

742 def make_redirect_url( 

743 self, 

744 path_info: str, 

745 query_args: t.Mapping[str, t.Any] | str | None = None, 

746 domain_part: str | None = None, 

747 ) -> str: 

748 """Creates a redirect URL. 

749 

750 :internal: 

751 """ 

752 if query_args is None: 

753 query_args = self.query_args 

754 

755 if query_args: 

756 query_str = self.encode_query_args(query_args) 

757 else: 

758 query_str = None 

759 

760 scheme = self.url_scheme or "http" 

761 host = self.get_host(domain_part) 

762 path = "/".join((self.script_name.strip("/"), path_info.lstrip("/"))) 

763 return urlunsplit((scheme, host, path, query_str, None)) 

764 

765 def make_alias_redirect_url( 

766 self, 

767 path: str, 

768 endpoint: str, 

769 values: t.Mapping[str, t.Any], 

770 method: str, 

771 query_args: t.Mapping[str, t.Any] | str, 

772 ) -> str: 

773 """Internally called to make an alias redirect URL.""" 

774 url = self.build( 

775 endpoint, values, method, append_unknown=False, force_external=True 

776 ) 

777 if query_args: 

778 url += f"?{self.encode_query_args(query_args)}" 

779 assert url != path, "detected invalid alias setting. No canonical URL found" 

780 return url 

781 

782 def _partial_build( 

783 self, 

784 endpoint: str, 

785 values: t.Mapping[str, t.Any], 

786 method: str | None, 

787 append_unknown: bool, 

788 ) -> tuple[str, str, bool] | None: 

789 """Helper for :meth:`build`. Returns subdomain and path for the 

790 rule that accepts this endpoint, values and method. 

791 

792 :internal: 

793 """ 

794 # in case the method is none, try with the default method first 

795 if method is None: 

796 rv = self._partial_build( 

797 endpoint, values, self.default_method, append_unknown 

798 ) 

799 if rv is not None: 

800 return rv 

801 

802 # Default method did not match or a specific method is passed. 

803 # Check all for first match with matching host. If no matching 

804 # host is found, go with first result. 

805 first_match = None 

806 

807 for rule in self.map._rules_by_endpoint.get(endpoint, ()): 

808 if rule.suitable_for(values, method): 

809 build_rv = rule.build(values, append_unknown) 

810 

811 if build_rv is not None: 

812 rv = (build_rv[0], build_rv[1], rule.websocket) 

813 if self.map.host_matching: 

814 if rv[0] == self.server_name: 

815 return rv 

816 elif first_match is None: 

817 first_match = rv 

818 else: 

819 return rv 

820 

821 return first_match 

822 

823 def build( 

824 self, 

825 endpoint: str, 

826 values: t.Mapping[str, t.Any] | None = None, 

827 method: str | None = None, 

828 force_external: bool = False, 

829 append_unknown: bool = True, 

830 url_scheme: str | None = None, 

831 ) -> str: 

832 """Building URLs works pretty much the other way round. Instead of 

833 `match` you call `build` and pass it the endpoint and a dict of 

834 arguments for the placeholders. 

835 

836 The `build` function also accepts an argument called `force_external` 

837 which, if you set it to `True` will force external URLs. Per default 

838 external URLs (include the server name) will only be used if the 

839 target URL is on a different subdomain. 

840 

841 >>> m = Map([ 

842 ... Rule('/', endpoint='index'), 

843 ... Rule('/downloads/', endpoint='downloads/index'), 

844 ... Rule('/downloads/<int:id>', endpoint='downloads/show') 

845 ... ]) 

846 >>> urls = m.bind("example.com", "/") 

847 >>> urls.build("index", {}) 

848 '/' 

849 >>> urls.build("downloads/show", {'id': 42}) 

850 '/downloads/42' 

851 >>> urls.build("downloads/show", {'id': 42}, force_external=True) 

852 'http://example.com/downloads/42' 

853 

854 Because URLs cannot contain non ASCII data you will always get 

855 bytes back. Non ASCII characters are urlencoded with the 

856 charset defined on the map instance. 

857 

858 Additional values are converted to strings and appended to the URL as 

859 URL querystring parameters: 

860 

861 >>> urls.build("index", {'q': 'My Searchstring'}) 

862 '/?q=My+Searchstring' 

863 

864 When processing those additional values, lists are furthermore 

865 interpreted as multiple values (as per 

866 :py:class:`werkzeug.datastructures.MultiDict`): 

867 

868 >>> urls.build("index", {'q': ['a', 'b', 'c']}) 

869 '/?q=a&q=b&q=c' 

870 

871 Passing a ``MultiDict`` will also add multiple values: 

872 

873 >>> urls.build("index", MultiDict((('p', 'z'), ('q', 'a'), ('q', 'b')))) 

874 '/?p=z&q=a&q=b' 

875 

876 If a rule does not exist when building a `BuildError` exception is 

877 raised. 

878 

879 The build method accepts an argument called `method` which allows you 

880 to specify the method you want to have an URL built for if you have 

881 different methods for the same endpoint specified. 

882 

883 :param endpoint: the endpoint of the URL to build. 

884 :param values: the values for the URL to build. Unhandled values are 

885 appended to the URL as query parameters. 

886 :param method: the HTTP method for the rule if there are different 

887 URLs for different methods on the same endpoint. 

888 :param force_external: enforce full canonical external URLs. If the URL 

889 scheme is not provided, this will generate 

890 a protocol-relative URL. 

891 :param append_unknown: unknown parameters are appended to the generated 

892 URL as query string argument. Disable this 

893 if you want the builder to ignore those. 

894 :param url_scheme: Scheme to use in place of the bound 

895 :attr:`url_scheme`. 

896 

897 .. versionchanged:: 2.0 

898 Added the ``url_scheme`` parameter. 

899 

900 .. versionadded:: 0.6 

901 Added the ``append_unknown`` parameter. 

902 """ 

903 self.map.update() 

904 

905 if values: 

906 if isinstance(values, MultiDict): 

907 values = { 

908 k: (v[0] if len(v) == 1 else v) 

909 for k, v in dict.items(values) 

910 if len(v) != 0 

911 } 

912 else: # plain dict 

913 values = {k: v for k, v in values.items() if v is not None} 

914 else: 

915 values = {} 

916 

917 rv = self._partial_build(endpoint, values, method, append_unknown) 

918 if rv is None: 

919 raise BuildError(endpoint, values, method, self) 

920 

921 domain_part, path, websocket = rv 

922 host = self.get_host(domain_part) 

923 

924 if url_scheme is None: 

925 url_scheme = self.url_scheme 

926 

927 # Always build WebSocket routes with the scheme (browsers 

928 # require full URLs). If bound to a WebSocket, ensure that HTTP 

929 # routes are built with an HTTP scheme. 

930 secure = url_scheme in {"https", "wss"} 

931 

932 if websocket: 

933 force_external = True 

934 url_scheme = "wss" if secure else "ws" 

935 elif url_scheme: 

936 url_scheme = "https" if secure else "http" 

937 

938 # shortcut this. 

939 if not force_external and ( 

940 (self.map.host_matching and host == self.server_name) 

941 or (not self.map.host_matching and domain_part == self.subdomain) 

942 ): 

943 return f"{self.script_name.rstrip('/')}/{path.lstrip('/')}" 

944 

945 scheme = f"{url_scheme}:" if url_scheme else "" 

946 return f"{scheme}//{host}{self.script_name[:-1]}/{path.lstrip('/')}"