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

319 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-09 06:08 +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 charset: charset of the url. defaults to ``"utf-8"`` 

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

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

53 :param merge_slashes: Merge consecutive slashes when matching or 

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

55 Slashes in variable parts are not merged. 

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

57 wasn't visited that way. This helps creating 

58 unique URLs. 

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

60 to the list of converters. If you redefine one 

61 converter this will override the original one. 

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

63 See `url_encode` for more details. 

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

65 :param encoding_errors: the error method to use for decoding 

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

67 feature and disables the subdomain one. If 

68 enabled the `host` parameter to rules is used 

69 instead of the `subdomain` one. 

70 

71 .. versionchanged:: 2.3 

72 The ``charset`` and ``encoding_errors`` parameters are deprecated and will be 

73 removed in Werkzeug 3.0. 

74 

75 .. versionchanged:: 1.0 

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

77 

78 .. versionchanged:: 1.0 

79 The ``merge_slashes`` parameter was added. 

80 

81 .. versionchanged:: 0.7 

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

83 

84 .. versionchanged:: 0.5 

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

86 """ 

87 

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

89 default_converters = ImmutableDict(DEFAULT_CONVERTERS) 

90 

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

92 #: 

93 #: .. versionadded:: 1.0 

94 lock_class = Lock 

95 

96 def __init__( 

97 self, 

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

99 default_subdomain: str = "", 

100 charset: str | None = None, 

101 strict_slashes: bool = True, 

102 merge_slashes: bool = True, 

103 redirect_defaults: bool = True, 

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

105 sort_parameters: bool = False, 

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

107 encoding_errors: str | None = None, 

108 host_matching: bool = False, 

109 ) -> None: 

110 self._matcher = StateMachineMatcher(merge_slashes) 

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

112 self._remap = True 

113 self._remap_lock = self.lock_class() 

114 

115 self.default_subdomain = default_subdomain 

116 

117 if charset is not None: 

118 warnings.warn( 

119 "The 'charset' parameter is deprecated and will be" 

120 " removed in Werkzeug 3.0.", 

121 DeprecationWarning, 

122 stacklevel=2, 

123 ) 

124 else: 

125 charset = "utf-8" 

126 

127 self.charset = charset 

128 

129 if encoding_errors is not None: 

130 warnings.warn( 

131 "The 'encoding_errors' parameter is deprecated and will be" 

132 " removed in Werkzeug 3.0.", 

133 DeprecationWarning, 

134 stacklevel=2, 

135 ) 

136 else: 

137 encoding_errors = "replace" 

138 

139 self.encoding_errors = encoding_errors 

140 self.strict_slashes = strict_slashes 

141 self.merge_slashes = merge_slashes 

142 self.redirect_defaults = redirect_defaults 

143 self.host_matching = host_matching 

144 

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

146 if converters: 

147 self.converters.update(converters) 

148 

149 self.sort_parameters = sort_parameters 

150 self.sort_key = sort_key 

151 

152 for rulefactory in rules or (): 

153 self.add(rulefactory) 

154 

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

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

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

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

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

160 code is automatically added if not provided but endpoints expect 

161 it. 

162 

163 :param endpoint: the endpoint to check. 

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

165 as positional arguments. Each one of them is 

166 checked. 

167 """ 

168 self.update() 

169 arguments = set(arguments) 

170 for rule in self._rules_by_endpoint[endpoint]: 

171 if arguments.issubset(rule.arguments): 

172 return True 

173 return False 

174 

175 @property 

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

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

178 

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

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

181 

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

183 are returned. 

184 :return: an iterator 

185 """ 

186 self.update() 

187 if endpoint is not None: 

188 return iter(self._rules_by_endpoint[endpoint]) 

189 return iter(self._rules) 

190 

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

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

193 rule is not bound to another map. 

194 

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

196 """ 

197 for rule in rulefactory.get_rules(self): 

198 rule.bind(self) 

199 if not rule.build_only: 

200 self._matcher.add(rule) 

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

202 self._remap = True 

203 

204 def bind( 

205 self, 

206 server_name: str, 

207 script_name: str | None = None, 

208 subdomain: str | None = None, 

209 url_scheme: str = "http", 

210 default_method: str = "GET", 

211 path_info: str | None = None, 

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

213 ) -> MapAdapter: 

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

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

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

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

218 redirect exceptions raised by Werkzeug will contain the full canonical 

219 URL. 

220 

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

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

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

224 environment which already contains the path info. 

225 

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

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

228 subdomain feature. 

229 

230 .. versionchanged:: 1.0 

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

232 will match. 

233 

234 .. versionchanged:: 0.15 

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

236 

237 .. versionchanged:: 0.8 

238 ``query_args`` can be a string. 

239 

240 .. versionchanged:: 0.7 

241 Added ``query_args``. 

242 """ 

243 server_name = server_name.lower() 

244 if self.host_matching: 

245 if subdomain is not None: 

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

247 elif subdomain is None: 

248 subdomain = self.default_subdomain 

249 if script_name is None: 

250 script_name = "/" 

251 if path_info is None: 

252 path_info = "/" 

253 

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

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

256 

257 try: 

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

259 except UnicodeError as e: 

260 raise BadHost() from e 

261 

262 return MapAdapter( 

263 self, 

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

265 script_name, 

266 subdomain, 

267 url_scheme, 

268 path_info, 

269 default_method, 

270 query_args, 

271 ) 

272 

273 def bind_to_environ( 

274 self, 

275 environ: WSGIEnvironment | Request, 

276 server_name: str | None = None, 

277 subdomain: str | None = None, 

278 ) -> MapAdapter: 

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

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

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

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

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

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

285 feature. 

286 

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

288 provided it will calculate the current subdomain automatically. 

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

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

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

292 

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

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

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

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

297 the match method. 

298 

299 .. versionchanged:: 1.0.0 

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

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

302 

303 .. versionchanged:: 1.0.0 

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

305 match the incoming WSGI server name. 

306 

307 .. versionchanged:: 0.8 

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

309 name was passed. 

310 

311 .. versionchanged:: 0.5 

312 previously this method accepted a bogus `calculate_subdomain` 

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

314 of that. 

315 

316 :param environ: a WSGI environment. 

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

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

319 """ 

320 env = _get_environ(environ) 

321 wsgi_server_name = get_host(env).lower() 

322 scheme = env["wsgi.url_scheme"] 

323 upgrade = any( 

324 v.strip() == "upgrade" 

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

326 ) 

327 

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

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

330 

331 if server_name is None: 

332 server_name = wsgi_server_name 

333 else: 

334 server_name = server_name.lower() 

335 

336 # strip standard port to match get_host() 

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

338 server_name = server_name[:-3] 

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

340 server_name = server_name[:-4] 

341 

342 if subdomain is None and not self.host_matching: 

343 cur_server_name = wsgi_server_name.split(".") 

344 real_server_name = server_name.split(".") 

345 offset = -len(real_server_name) 

346 

347 if cur_server_name[offset:] != real_server_name: 

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

349 # accessed directly by IP address under some situations. 

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

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

352 # in a 404 error on matching. 

353 warnings.warn( 

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

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

356 stacklevel=2, 

357 ) 

358 subdomain = "<invalid>" 

359 else: 

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

361 

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

363 val = env.get(name) 

364 if val is not None: 

365 return _wsgi_decoding_dance(val, self.charset) 

366 return None 

367 

368 script_name = _get_wsgi_string("SCRIPT_NAME") 

369 path_info = _get_wsgi_string("PATH_INFO") 

370 query_args = _get_wsgi_string("QUERY_STRING") 

371 return Map.bind( 

372 self, 

373 server_name, 

374 script_name, 

375 subdomain, 

376 scheme, 

377 env["REQUEST_METHOD"], 

378 path_info, 

379 query_args=query_args, 

380 ) 

381 

382 def update(self) -> None: 

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

384 in the correct order after things changed. 

385 """ 

386 if not self._remap: 

387 return 

388 

389 with self._remap_lock: 

390 if not self._remap: 

391 return 

392 

393 self._matcher.update() 

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

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

396 self._remap = False 

397 

398 def __repr__(self) -> str: 

399 rules = self.iter_rules() 

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

401 

402 

403class MapAdapter: 

404 

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

406 the URL matching and building based on runtime information. 

407 """ 

408 

409 def __init__( 

410 self, 

411 map: Map, 

412 server_name: str, 

413 script_name: str, 

414 subdomain: str | None, 

415 url_scheme: str, 

416 path_info: str, 

417 default_method: str, 

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

419 ): 

420 self.map = map 

421 self.server_name = server_name 

422 

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

424 script_name += "/" 

425 

426 self.script_name = script_name 

427 self.subdomain = subdomain 

428 self.url_scheme = url_scheme 

429 self.path_info = path_info 

430 self.default_method = default_method 

431 self.query_args = query_args 

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

433 

434 def dispatch( 

435 self, 

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

437 path_info: str | None = None, 

438 method: str | None = None, 

439 catch_http_exceptions: bool = False, 

440 ) -> WSGIApplication: 

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

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

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

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

445 so that applications can display nicer error messages by just 

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

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

448 it will catch the http exceptions. 

449 

450 Here a small example for the dispatch usage:: 

451 

452 from werkzeug.wrappers import Request, Response 

453 from werkzeug.wsgi import responder 

454 from werkzeug.routing import Map, Rule 

455 

456 def on_index(request): 

457 return Response('Hello from the index') 

458 

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

460 views = {'index': on_index} 

461 

462 @responder 

463 def application(environ, start_response): 

464 request = Request(environ) 

465 urls = url_map.bind_to_environ(environ) 

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

467 catch_http_exceptions=True) 

468 

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

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

471 

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

473 first argument and the value dict as second. Has 

474 to dispatch to the actual view function with this 

475 information. (see above) 

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

477 path info specified on binding. 

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

479 method specified on binding. 

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

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

482 """ 

483 try: 

484 try: 

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

486 except RequestRedirect as e: 

487 return e 

488 return view_func(endpoint, args) 

489 except HTTPException as e: 

490 if catch_http_exceptions: 

491 return e 

492 raise 

493 

494 @t.overload 

495 def match( # type: ignore 

496 self, 

497 path_info: str | None = None, 

498 method: str | None = None, 

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

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

501 websocket: bool | None = None, 

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

503 ... 

504 

505 @t.overload 

506 def match( 

507 self, 

508 path_info: str | None = None, 

509 method: str | None = None, 

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

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

512 websocket: bool | None = None, 

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

514 ... 

515 

516 def match( 

517 self, 

518 path_info: str | None = None, 

519 method: str | None = None, 

520 return_rule: bool = False, 

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

522 websocket: bool | None = None, 

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

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

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

526 following things can then happen: 

527 

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

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

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

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

532 

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

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

535 This is useful for RESTful applications. 

536 

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

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

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

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

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

542 similar to all other subclasses of `HTTPException`. 

543 

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

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

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

547 request. 

548 

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

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

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

552 

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

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

555 explicitly). 

556 

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

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

559 redirect pages. 

560 

561 Here is a small example for matching: 

562 

563 >>> m = Map([ 

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

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

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

567 ... ]) 

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

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

570 ('index', {}) 

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

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

573 

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

575 

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

577 Traceback (most recent call last): 

578 ... 

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

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

581 Traceback (most recent call last): 

582 ... 

583 NotFound: 404 Not Found 

584 

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

586 path info specified on binding. 

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

588 method specified on binding. 

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

590 endpoint (defaults to `False`). 

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

592 automatic redirects as string or dictionary. It's 

593 currently not possible to use the query arguments 

594 for URL matching. 

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

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

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

598 

599 .. versionadded:: 1.0 

600 Added ``websocket``. 

601 

602 .. versionchanged:: 0.8 

603 ``query_args`` can be a string. 

604 

605 .. versionadded:: 0.7 

606 Added ``query_args``. 

607 

608 .. versionadded:: 0.6 

609 Added ``return_rule``. 

610 """ 

611 self.map.update() 

612 if path_info is None: 

613 path_info = self.path_info 

614 if query_args is None: 

615 query_args = self.query_args or {} 

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

617 

618 if websocket is None: 

619 websocket = self.websocket 

620 

621 domain_part = self.server_name 

622 

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

624 domain_part = self.subdomain 

625 

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

627 

628 try: 

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

630 except RequestPath as e: 

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

632 new_path = quote( 

633 e.path_info, safe="!$&'()*+,/:;=@", encoding=self.map.charset 

634 ) 

635 raise RequestRedirect( 

636 self.make_redirect_url(new_path, query_args) 

637 ) from None 

638 except RequestAliasRedirect as e: 

639 raise RequestRedirect( 

640 self.make_alias_redirect_url( 

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

642 e.endpoint, 

643 e.matched_values, 

644 method, 

645 query_args, 

646 ) 

647 ) from None 

648 except NoMatch as e: 

649 if e.have_match_for: 

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

651 

652 if e.websocket_mismatch: 

653 raise WebsocketMismatch() from None 

654 

655 raise NotFound() from None 

656 else: 

657 rule, rv = result 

658 

659 if self.map.redirect_defaults: 

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

661 if redirect_url is not None: 

662 raise RequestRedirect(redirect_url) 

663 

664 if rule.redirect_to is not None: 

665 if isinstance(rule.redirect_to, str): 

666 

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

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

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

670 

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

672 else: 

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

674 

675 if self.subdomain: 

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

677 else: 

678 netloc = self.server_name 

679 

680 raise RequestRedirect( 

681 urljoin( 

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

683 redirect_url, 

684 ) 

685 ) 

686 

687 if return_rule: 

688 return rule, rv 

689 else: 

690 return rule.endpoint, rv 

691 

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

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

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

695 

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

697 path info specified on binding. 

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

699 method specified on binding. 

700 """ 

701 try: 

702 self.match(path_info, method) 

703 except RequestRedirect: 

704 pass 

705 except HTTPException: 

706 return False 

707 return True 

708 

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

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

711 

712 .. versionadded:: 0.7 

713 """ 

714 try: 

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

716 except MethodNotAllowed as e: 

717 return e.valid_methods # type: ignore 

718 except HTTPException: 

719 pass 

720 return [] 

721 

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

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

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

725 a full host name. 

726 """ 

727 if self.map.host_matching: 

728 if domain_part is None: 

729 return self.server_name 

730 

731 return domain_part 

732 

733 if domain_part is None: 

734 subdomain = self.subdomain 

735 else: 

736 subdomain = domain_part 

737 

738 if subdomain: 

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

740 else: 

741 return self.server_name 

742 

743 def get_default_redirect( 

744 self, 

745 rule: Rule, 

746 method: str, 

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

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

749 ) -> str | None: 

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

751 This is used for default redirecting only. 

752 

753 :internal: 

754 """ 

755 assert self.map.redirect_defaults 

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

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

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

759 # with the highest priority up for building. 

760 if r is rule: 

761 break 

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

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

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

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

766 return None 

767 

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

769 if not isinstance(query_args, str): 

770 return _urlencode(query_args, encoding=self.map.charset) 

771 return query_args 

772 

773 def make_redirect_url( 

774 self, 

775 path_info: str, 

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

777 domain_part: str | None = None, 

778 ) -> str: 

779 """Creates a redirect URL. 

780 

781 :internal: 

782 """ 

783 if query_args is None: 

784 query_args = self.query_args 

785 

786 if query_args: 

787 query_str = self.encode_query_args(query_args) 

788 else: 

789 query_str = None 

790 

791 scheme = self.url_scheme or "http" 

792 host = self.get_host(domain_part) 

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

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

795 

796 def make_alias_redirect_url( 

797 self, 

798 path: str, 

799 endpoint: str, 

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

801 method: str, 

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

803 ) -> str: 

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

805 url = self.build( 

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

807 ) 

808 if query_args: 

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

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

811 return url 

812 

813 def _partial_build( 

814 self, 

815 endpoint: str, 

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

817 method: str | None, 

818 append_unknown: bool, 

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

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

821 rule that accepts this endpoint, values and method. 

822 

823 :internal: 

824 """ 

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

826 if method is None: 

827 rv = self._partial_build( 

828 endpoint, values, self.default_method, append_unknown 

829 ) 

830 if rv is not None: 

831 return rv 

832 

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

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

835 # host is found, go with first result. 

836 first_match = None 

837 

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

839 if rule.suitable_for(values, method): 

840 build_rv = rule.build(values, append_unknown) 

841 

842 if build_rv is not None: 

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

844 if self.map.host_matching: 

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

846 return rv 

847 elif first_match is None: 

848 first_match = rv 

849 else: 

850 return rv 

851 

852 return first_match 

853 

854 def build( 

855 self, 

856 endpoint: str, 

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

858 method: str | None = None, 

859 force_external: bool = False, 

860 append_unknown: bool = True, 

861 url_scheme: str | None = None, 

862 ) -> str: 

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

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

865 arguments for the placeholders. 

866 

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

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

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

870 target URL is on a different subdomain. 

871 

872 >>> m = Map([ 

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

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

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

876 ... ]) 

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

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

879 '/' 

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

881 '/downloads/42' 

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

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

884 

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

886 bytes back. Non ASCII characters are urlencoded with the 

887 charset defined on the map instance. 

888 

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

890 URL querystring parameters: 

891 

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

893 '/?q=My+Searchstring' 

894 

895 When processing those additional values, lists are furthermore 

896 interpreted as multiple values (as per 

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

898 

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

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

901 

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

903 

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

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

906 

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

908 raised. 

909 

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

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

912 different methods for the same endpoint specified. 

913 

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

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

916 appended to the URL as query parameters. 

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

918 URLs for different methods on the same endpoint. 

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

920 scheme is not provided, this will generate 

921 a protocol-relative URL. 

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

923 URL as query string argument. Disable this 

924 if you want the builder to ignore those. 

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

926 :attr:`url_scheme`. 

927 

928 .. versionchanged:: 2.0 

929 Added the ``url_scheme`` parameter. 

930 

931 .. versionadded:: 0.6 

932 Added the ``append_unknown`` parameter. 

933 """ 

934 self.map.update() 

935 

936 if values: 

937 if isinstance(values, MultiDict): 

938 values = { 

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

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

941 if len(v) != 0 

942 } 

943 else: # plain dict 

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

945 else: 

946 values = {} 

947 

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

949 if rv is None: 

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

951 

952 domain_part, path, websocket = rv 

953 host = self.get_host(domain_part) 

954 

955 if url_scheme is None: 

956 url_scheme = self.url_scheme 

957 

958 # Always build WebSocket routes with the scheme (browsers 

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

960 # routes are built with an HTTP scheme. 

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

962 

963 if websocket: 

964 force_external = True 

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

966 elif url_scheme: 

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

968 

969 # shortcut this. 

970 if not force_external and ( 

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

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

973 ): 

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

975 

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

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