Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/yarl/_url.py: 39%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1511 statements  

1import re 

2import warnings 

3from collections.abc import Mapping, Sequence 

4from enum import Enum, StrEnum 

5from functools import _CacheInfo, lru_cache 

6from importlib.util import find_spec 

7from ipaddress import ip_address 

8from typing import ( 

9 TYPE_CHECKING, 

10 Any, 

11 Literal, 

12 NoReturn, 

13 Self, 

14 TypedDict, 

15 TypeVar, 

16 cast, 

17 overload, 

18) 

19from urllib.parse import ( 

20 SplitResult, 

21 quote, 

22 scheme_chars, 

23 unquote_to_bytes, 

24 uses_relative, 

25) 

26 

27import idna 

28from multidict import MultiDict, MultiDictProxy, istr 

29from propcache.api import under_cached_property as cached_property 

30 

31from ._parse import ( 

32 EMPTY_AUTHORITY, 

33 EMPTY_FRAGMENT, 

34 EMPTY_QUERY, 

35 IP_FUTURE_RE, 

36 SPECIAL_SCHEMES, 

37 UNSAFE_URL_BYTES_TO_REMOVE, 

38 WHATWG_C0_CONTROL_OR_SPACE, 

39 ZONE_ID_RE, 

40 SplitURLType, 

41 _check_netloc, 

42 check_zone_id, 

43 has_dot_prefix, 

44 make_netloc, 

45 query_to_pairs, 

46 split_netloc, 

47 split_netloc_rfc, 

48 split_url, 

49 unsplit_result, 

50 unsplit_result_empty, 

51) 

52from ._path import ( 

53 drive_letter, 

54 is_drive_letter, 

55 normalize_drive_letter, 

56 normalize_file_path, 

57 normalize_path, 

58 normalize_path_segments, 

59 remove_dot_segments, 

60 starts_with_drive_letter, 

61) 

62from ._query import ( 

63 Query, 

64 QueryVariable, 

65 SimpleQuery, 

66 get_str_query, 

67 get_str_query_from_iterable, 

68 get_str_query_from_sequence_iterable, 

69) 

70from ._quoters import ( 

71 FRAGMENT_QUOTER, 

72 FRAGMENT_REQUOTER, 

73 PATH_QUOTER, 

74 PATH_REQUOTER, 

75 PATH_SAFE_UNQUOTER, 

76 QS_UNQUOTER, 

77 QUERY_QUOTER, 

78 QUERY_REQUOTER, 

79 QUOTER, 

80 REQUOTER, 

81 UNQUOTER, 

82 WHATWG_FRAGMENT_REQUOTER, 

83 WHATWG_PATH_REQUOTER, 

84 WHATWG_QUERY_REQUOTER, 

85 ZONE_ID_UNQUOTER, 

86 human_quote, 

87) 

88 

89# Avoid Pydantic import if not used (increases yarl's import time by 3-7x). 

90HAS_PYDANTIC = find_spec("pydantic_core") is not None 

91if TYPE_CHECKING: 

92 from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler 

93 from pydantic.json_schema import JsonSchemaValue 

94 from pydantic_core import CoreSchema 

95 

96 

97DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443, "ftp": 21} 

98USES_RELATIVE = frozenset(uses_relative) 

99_SCHEME_CHARS = frozenset(scheme_chars) 

100 

101# Special schemes https://url.spec.whatwg.org/#special-scheme 

102# are not allowed to have an empty host https://url.spec.whatwg.org/#url-representation 

103SCHEME_REQUIRES_HOST = frozenset(("http", "https", "ws", "wss", "ftp")) 

104 

105 

106# reg-name: unreserved / pct-encoded / sub-delims 

107# this pattern matches anything that is *not* in those classes. and is only used 

108# on lower-cased ASCII values. 

109NOT_REG_NAME = re.compile( 

110 r""" 

111 # any character not in the unreserved or sub-delims sets, plus % 

112 # (validated with the additional check for pct-encoded sequences below) 

113 [^a-z0-9\-._~!$&'()*+,;=%] 

114 | 

115 # % only allowed if it is part of a pct-encoded 

116 # sequence of 2 hex digits. 

117 %(?![0-9a-f]{2}) 

118 """, 

119 re.VERBOSE, 

120) 

121# The characters that a WHATWG domain can have and an RFC 3986 reg-name 

122# cannot: WHATWG only rejects the forbidden domain code points. 

123_WHATWG_ONLY_HOST_RE = re.compile(r'["`{}]') 

124# NOT_REG_NAME with those characters allowed. 

125_NOT_WHATWG_DOMAIN_RE = re.compile(r"[^a-z0-9\-._~!$&'()*+,;=%\"`{}]|%(?![0-9a-f]{2})") 

126 

127# Invisible default-ignorable / format code points that must not appear in a 

128# host (soft hyphen, zero-width space, word joiner, bidi controls, variation 

129# selectors, ...). Depending on the code point IDNA either silently deletes it 

130# (so ``e<ZWSP>vil.com`` encodes to ``evil.com``) or folds it into a different 

131# punycode host; either way the parsed host differs from the string an 

132# application validated. The set is the union of two authoritative sources, 

133# matching the two encoders _idna_encode dispatches to: 

134# 

135# 1. Unicode Default_Ignorable_Code_Point (uts46=True path via the ``idna`` 

136# package). Ranges taken from the DerivedCoreProperties data file: 

137# https://www.unicode.org/Public/UCD/latest/ucd/DerivedCoreProperties.txt 

138# (the E0000..E0FFF block is contiguous under this property). 

139# 2. RFC 3454 (Stringprep) Table B.1 "commonly mapped to nothing", used by the 

140# stdlib ``str.encode("idna")`` / IDNA2003 nameprep fallback: 

141# https://www.rfc-editor.org/rfc/rfc3454#appendix-B.1 

142# This is the source of U+1806, which is not Default_Ignorable. 

143# 

144# Coverage is pinned to the installed ``idna``/Unicode data by a sweep test 

145# (test_default_ignorable_covers_idna_stripped in tests/test_url.py) that 

146# brute-forces every code point through _idna_encode and fails if any point it 

147# silently deletes is not matched here. 

148_DEFAULT_IGNORABLE_RE = re.compile( 

149 "[" 

150 "\u00ad" # SOFT HYPHEN 

151 "\u034f" # COMBINING GRAPHEME JOINER 

152 "\u061c" # ARABIC LETTER MARK 

153 "\u115f-\u1160" # HANGUL CHOSEONG/JUNGSEONG FILLER 

154 "\u17b4-\u17b5" # KHMER VOWEL INHERENT AQ/AA 

155 "\u1806" # MONGOLIAN TODO SOFT HYPHEN (nameprep maps to nothing) 

156 "\u180b-\u180f" # MONGOLIAN FVS ONE..FOUR and VOWEL SEPARATOR 

157 "\u200b-\u200f" # ZERO WIDTH SPACE..RIGHT-TO-LEFT MARK 

158 "\u202a-\u202e" # bidi embedding/override controls 

159 "\u2060-\u206f" # WORD JOINER..NOMINAL DIGIT SHAPES 

160 "\u3164" # HANGUL FILLER 

161 "\ufe00-\ufe0f" # VARIATION SELECTOR-1..16 

162 "\ufeff" # ZERO WIDTH NO-BREAK SPACE (BOM) 

163 "\uffa0" # HALFWIDTH HANGUL FILLER 

164 "\ufff0-\ufff8" # reserved default-ignorables 

165 "\U0001bca0-\U0001bca3" # SHORTHAND FORMAT controls 

166 "\U0001d173-\U0001d17a" # MUSICAL SYMBOL begin/end controls 

167 "\U000e0000-\U000e0fff" # tags and VARIATION SELECTOR SUPPLEMENT 

168 "]" 

169) 

170 

171# Zone IDs are OS-specific text strings with no format defined by the RFCs: 

172# https://datatracker.ietf.org/doc/html/rfc4007#section-11.2 

173# RFC 9844 §6.3 recommends rejecting characters inappropriate for the 

174# environment; for yarl we reject ASCII control characters (CTL): 

175# https://datatracker.ietf.org/doc/html/rfc9844#section-6-3 

176_ZONE_ID_UNSAFE_RE = re.compile(r"[\x00-\x1f\x7f]") 

177# A character of a zone identifier that RFC 6874 percent-encodes. 

178_ZONE_ID_REQUOTE_RE = re.compile(r"%(?![0-9A-Fa-f]{2})|[^0-9A-Za-z._~%-]") 

179 

180 

181def _quote_match(match: re.Match[str]) -> str: 

182 return quote(match[0], safe="") 

183 

184 

185_T = TypeVar("_T") 

186 

187 

188class UndefinedType(Enum): 

189 """Singleton type for use with not set sentinel values.""" 

190 

191 _singleton = 0 

192 

193 

194UNDEFINED = UndefinedType._singleton 

195 

196 

197class Mode(StrEnum): 

198 """URL standard a :class:`URL` follows.""" 

199 

200 RFC = "rfc" 

201 WHATWG = "whatwg" 

202 

203 

204Mode.__module__ = "yarl" 

205_WHATWG = Mode.WHATWG 

206# Members hash and compare as their values, so this maps both the members and 

207# the plain strings; much faster than calling ``Mode(value)``. 

208_MODE_BY_VALUE: dict[str, Mode] = {c.value: c for c in Mode} 

209 

210ModeType = Mode | Literal["rfc", "whatwg"] 

211 

212 

213def _to_mode(value: ModeType) -> Mode: 

214 """Normalize a compatibility argument, rejecting unknown values.""" 

215 try: 

216 mode = _MODE_BY_VALUE.get(value) 

217 except TypeError: # unhashable, e.g. a list 

218 mode = None 

219 if mode is None: 

220 raise ValueError(f"{value!r} is not a valid Mode") 

221 return mode 

222 

223 

224class CacheInfo(TypedDict): 

225 """Host encoding cache.""" 

226 

227 idna_encode: _CacheInfo 

228 idna_decode: _CacheInfo 

229 ip_address: _CacheInfo 

230 host_validate: _CacheInfo 

231 encode_host: _CacheInfo 

232 

233 

234class _InternalURLCache(TypedDict, total=False): 

235 _val: SplitURLType 

236 _join_path: str 

237 _origin: "URL" 

238 absolute: bool 

239 hash: int 

240 scheme: str 

241 raw_authority: str 

242 authority: str 

243 raw_user: str | None 

244 user: str | None 

245 raw_password: str | None 

246 password: str | None 

247 raw_host: str | None 

248 host: str | None 

249 host_subcomponent: str | None 

250 host_port_subcomponent: str | None 

251 port: int | None 

252 explicit_port: int | None 

253 raw_path: str 

254 path: str 

255 _parsed_query: list[tuple[str, str]] 

256 query: "MultiDictProxy[str]" 

257 raw_query_string: str 

258 query_string: str 

259 path_qs: str 

260 raw_path_qs: str 

261 raw_fragment: str 

262 fragment: str 

263 raw_parts: tuple[str, ...] 

264 parts: tuple[str, ...] 

265 parent: "URL" 

266 raw_name: str 

267 name: str 

268 raw_suffix: str 

269 suffix: str 

270 raw_suffixes: tuple[str, ...] 

271 suffixes: tuple[str, ...] 

272 

273 

274def rewrite_module(obj: _T) -> _T: 

275 obj.__module__ = "yarl" 

276 return obj 

277 

278 

279def _encode_relative_scheme_colon(path: str) -> str: 

280 """Re-encode a scheme-shaped leading ``:`` in a relative path to ``%3A``.""" 

281 colon_pos = path.find(":") 

282 if colon_pos <= 0: 

283 return path 

284 for c in path[:colon_pos]: 

285 if c not in _SCHEME_CHARS: 

286 return path 

287 return path[:colon_pos] + "%3A" + path[colon_pos + 1 :] 

288 

289 

290def _encode_relative_colon(path: str, mode: Mode) -> str: 

291 """Re-encode a ``:`` in a relative path that would not parse back. 

292 

293 WHATWG mode only encodes one that would read as a scheme. RFC 3986 

294 section 4.2 allows no ``:`` in the first segment at all, and RFC 3986 

295 mode rejects one when it parses a string, so it encodes all of them. 

296 """ 

297 if mode is _WHATWG: 

298 return _encode_relative_scheme_colon(path) 

299 first, slash, rest = path.partition("/") 

300 return first.replace(":", "%3A") + slash + rest 

301 

302 

303def _check_missing_host(scheme: str, mode: Mode) -> None: 

304 """Reject an authority that has userinfo or a port but no host. 

305 

306 Schemes that need a host always reject it. RFC 3986 allows an empty 

307 host otherwise, WHATWG rejects it for every scheme. 

308 """ 

309 if scheme in SCHEME_REQUIRES_HOST: 

310 raise ValueError( 

311 f"Invalid URL: host is required for absolute urls with the {scheme} scheme" 

312 ) 

313 if mode is Mode.WHATWG: 

314 raise ValueError("Invalid URL: host is required with userinfo or a port") 

315 

316 

317def _check_whatwg_file_authority(authority: str) -> None: 

318 """Reject the userinfo and the port of a file URL in WHATWG mode. 

319 

320 The WHATWG file host state reads the authority as a host only, which 

321 cannot have "@" or ":", so "file://user@host/" and "file://host:1/" 

322 fail. RFC 3986 mode keeps both. A Windows drive letter is not a host 

323 with an empty port: callers move it into the path first, see 

324 _whatwg_file_drive(). 

325 """ 

326 if "@" in authority or ":" in authority.rpartition("]")[2]: 

327 raise ValueError( 

328 f"Invalid URL: a file URL cannot have userinfo or a port, got {authority!r}" 

329 ) 

330 

331 

332# The characters the userinfo percent-encode set of the WHATWG URL Standard 

333# leaves alone: the unreserved characters, the sub-delims other than ";" and 

334# "=", and "%", which is kept as written, also when it starts no 

335# percent-encoded octet. 

336_WHATWG_USERINFO_UNSAFE_RE = re.compile(r"[^A-Za-z0-9\-._~!$&'()*+,%]+") 

337 

338 

339def _quote_userinfo_match(match: "re.Match[str]") -> str: 

340 return quote(match.group(), safe="", errors="ignore") 

341 

342 

343def _whatwg_requote_userinfo(value: str) -> str: 

344 """Encode a parsed username or password as WHATWG mode does. 

345 

346 The WHATWG URL Standard splits the userinfo at the last "@" and 

347 percent-encodes the username and the password with the userinfo 

348 percent-encode set, so "http://a b;@c@h" has the username "a%20b%3B%40c". 

349 A "%" is kept as written, "http://a%b@h" stays as it is. RFC 3986 mode 

350 accepts only a valid userinfo, which it requotes. See quote_userinfo() 

351 in _parse.py for a value that is not encoded. 

352 """ 

353 if value.isascii() and value.isalnum(): 

354 return value 

355 return _WHATWG_USERINFO_UNSAFE_RE.sub(_quote_userinfo_match, value) 

356 

357 

358def _empty_path(scheme: str, mode: Mode) -> str: 

359 """Return the path of a URL whose path became empty. 

360 

361 A file URL has a path in WHATWG mode, "/" at least, so "file:///a" has 

362 the parent "file:///". Every other URL keeps the empty path. 

363 """ 

364 return "/" if scheme == "file" and mode is _WHATWG else "" 

365 

366 

367def _whatwg_file_drive(netloc: str, path: str) -> str: 

368 """Return the path of a file URL whose authority is a drive letter. 

369 

370 The WHATWG file host state reads a Windows drive letter in the authority 

371 as the start of the path, so "file://C:/x" and "file://C|/x" are 

372 "file:///C:/x", with an empty host. 

373 """ 

374 return f"/{netloc[0]}:{path}" 

375 

376 

377def _normalize_path(path: str, scheme: str, mode: Mode) -> str: 

378 """Drop '.' and '..' from path, keeping the drive letter of a file URL. 

379 

380 WHATWG mode keeps it, see normalize_file_path(). 

381 """ 

382 if scheme == "file" and mode is _WHATWG: 

383 return normalize_file_path(path) 

384 return normalize_path(path) 

385 

386 

387def _file_join_path(base: str, path: str) -> str: 

388 """Resolve the path of a reference against the path of a file URL base. 

389 

390 The WHATWG file and file slash states, in WHATWG mode: a path that 

391 starts with a drive letter replaces the path of the base, an absolute 

392 path keeps the drive letter of the base, and the drive letter alone is 

393 never dropped from the base, so "x" against "file:///C:" is 

394 "file:///C:/x". 

395 """ 

396 if path[0] == "/": 

397 if not starts_with_drive_letter(path, 1) and (drive := drive_letter(base)): 

398 path = f"/{drive}{path}" 

399 elif starts_with_drive_letter(path): 

400 path = f"/{path}" 

401 else: 

402 if base[:1] != "/": 

403 # A rootless base, or an empty one, which only encoded=True 

404 # leaves in a file URL. 

405 base = f"/{base}" 

406 if len(base) == 3 and drive_letter(base): 

407 path = f"{base}/{path}" 

408 else: 

409 path = base[: base.rfind("/") + 1] + path 

410 return normalize_file_path(path) if "." in path else path 

411 

412 

413def _is_pipe_drive_authority(join_path: str) -> bool: 

414 """Tell if a reference saved for join() has an authority like "C|". 

415 

416 Only a file URL reads it as a drive letter, it is no host for others. 

417 """ 

418 return ( 

419 join_path[:2] == "//" 

420 and join_path[3:4] == "|" 

421 and starts_with_drive_letter(join_path, 2) 

422 ) 

423 

424 

425def _rfc_host_error(host: str) -> str: 

426 """Explain why RFC 3986 mode rejected a host that WHATWG mode accepts. 

427 

428 A domain can have '"', "`", "{" and "}", which an RFC 3986 reg-name 

429 cannot, see _encode_host(). RFC 3987 converts an internationalized host 

430 with IDNA, now IDNA2008 (RFC 5891), which disallows code points such as 

431 emoji. WHATWG mode keeps the IDNA2003 fallback of _idna_encode(), as 

432 UTS #46 accepts them. 

433 """ 

434 if invalid := _WHATWG_ONLY_HOST_RE.search(host): 

435 return ( 

436 f"Host {host!r} cannot contain {invalid.group()!r}" 

437 f" (at position {invalid.start()})" 

438 ) 

439 return f"Host {host!r} is not a valid IDNA2008 name" 

440 

441 

442# The RFC 3986 mode check below is guarded with ``mode is not _WHATWG`` at 

443# the call sites, so that the default mode does not pay for a call on the 

444# URL.build() hot path (CodSpeed). 

445def _check_rfc_authority(authority: str) -> None: 

446 """Reject "@" in the userinfo in RFC 3986 mode. 

447 

448 RFC 3986 userinfo cannot contain "@", so "sc://a@b@c/" has no valid 

449 parse; WHATWG splits at the last "@" and percent-encodes the others. 

450 """ 

451 if authority.count("@") > 1: 

452 raise ValueError(f"Invalid URL: userinfo cannot contain '@' in {authority!r}") 

453 

454 

455def _char_ranges(*pairs: tuple[int, int]) -> str: 

456 return "".join(f"{chr(first)}-{chr(last)}" for first, last in pairs) 

457 

458 

459# The characters of an IRI outside ASCII, RFC 3987 section 2.2, less the 

460# bidi formatting characters that section 4.1 forbids (U+200E, U+200F and 

461# U+202A to U+202E). 

462_UCSCHAR = _char_ranges( 

463 (0xA0, 0x200D), 

464 (0x2010, 0x2029), 

465 (0x202F, 0xD7FF), 

466 (0xF900, 0xFDCF), 

467 (0xFDF0, 0xFFEF), 

468 *((plane << 16, (plane << 16) + 0xFFFD) for plane in range(1, 14)), 

469 (0xE1000, 0xEFFFD), 

470) 

471_IPRIVATE = _char_ranges((0xE000, 0xF8FF), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD)) 

472_UNRESERVED = r"A-Za-z0-9\-._~" 

473_IUNRESERVED = _UNRESERVED + _UCSCHAR 

474_SUB_DELIMS = "!$&'()*+,;=" 

475_RFC_SPLIT_RE = re.compile( 

476 r"(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?", re.S 

477) 

478_RFC_SCHEME_RE = re.compile(r"[A-Za-z][A-Za-z0-9+\-.]*") 

479 

480 

481def _rfc_re(chars: str) -> re.Pattern[str]: 

482 return re.compile(f"(?:[{chars}]|%[0-9A-Fa-f]{{2}})*") 

483 

484 

485# Several "@" are left to _check_rfc_authority(), which says so. 

486_RFC_USERINFO_RE = _rfc_re(f"{_IUNRESERVED}{_SUB_DELIMS}:@") 

487_RFC_REG_NAME_RE = _rfc_re(f"{_IUNRESERVED}{_SUB_DELIMS}") 

488_RFC_PATH_RE = _rfc_re(f"{_IUNRESERVED}{_SUB_DELIMS}:@/") 

489_RFC_QUERY_RE = _rfc_re(f"{_IUNRESERVED}{_SUB_DELIMS}:@/?{_IPRIVATE}") 

490_RFC_FRAGMENT_RE = _rfc_re(f"{_IUNRESERVED}{_SUB_DELIMS}:@/?") 

491_RFC_PORT_RE = re.compile("[0-9]*") 

492# The characters of IPv6address and IPvFuture, and of a zone identifier 

493# (RFC 6874); the parser checks the rest. 

494_RFC_IP_LITERAL_RE = _rfc_re(f"{_UNRESERVED}{_SUB_DELIMS}:") 

495 

496 

497def _check_rfc_component( 

498 url_str: str, name: str, value: str | None, pattern: re.Pattern[str] 

499) -> None: 

500 if value is None: 

501 return 

502 end = cast(re.Match[str], pattern.match(value)).end() 

503 if end == len(value): 

504 return 

505 char = value[end] 

506 if char == "%" and pattern is not _RFC_PORT_RE: 

507 what = "a '%' not followed by two hexadecimal digits" 

508 else: 

509 what = repr(char) 

510 raise ValueError( 

511 f"Invalid URL: RFC 3986 and RFC 3987 do not allow {what} in the {name}" 

512 f" of {url_str!r}" 

513 ) 

514 

515 

516def _check_rfc_url(url_str: str) -> None: 

517 """Reject a URL string outside the grammar of RFC 3986 and RFC 3987. 

518 

519 RFC 3986 mode takes IRIs (RFC 3987): URL() percent-encodes their 

520 non-ASCII characters and encodes a non-ASCII host with IDNA2008. Other 

521 characters, such as a space, "<" or a "%" that does not start a 

522 percent-encoded octet, make the string invalid, where WHATWG mode 

523 percent-encodes or removes them. The characters of an IP-literal host are 

524 checked here, its syntax when it is parsed. 

525 """ 

526 if "\\" in url_str: 

527 raise ValueError(f"Invalid URL: RFC 3986 does not allow '\\' in {url_str!r}") 

528 # RFC 3986 Appendix B, with no preprocessing. 

529 match = cast(re.Match[str], _RFC_SPLIT_RE.fullmatch(url_str)) 

530 scheme, authority, path, query, fragment = match.groups() 

531 if scheme is not None and _RFC_SCHEME_RE.fullmatch(scheme) is None: 

532 raise ValueError( 

533 f"Invalid URL: {scheme!r} is not a valid scheme in {url_str!r}" 

534 ) 

535 if authority is not None: 

536 _check_rfc_authority(authority) 

537 userinfo, _, hostport = authority.rpartition("@") 

538 _check_rfc_component(url_str, "userinfo", userinfo, _RFC_USERINFO_RE) 

539 if hostport.startswith("["): 

540 literal, _, after = hostport[1:].partition("]") 

541 _check_rfc_component(url_str, "host", literal, _RFC_IP_LITERAL_RE) 

542 port = after.partition(":")[2] 

543 else: 

544 host, _, port = hostport.partition(":") 

545 _check_rfc_component(url_str, "host", host, _RFC_REG_NAME_RE) 

546 _check_rfc_component(url_str, "port", port, _RFC_PORT_RE) 

547 elif scheme is None and ":" in path.partition("/")[0]: 

548 # RFC 3986 section 4.2: "./" must come first to make it relative. 

549 raise ValueError( 

550 "Invalid URL: the first segment of a relative path cannot contain" 

551 f" ':' in {url_str!r}" 

552 ) 

553 _check_rfc_component(url_str, "path", path, _RFC_PATH_RE) 

554 _check_rfc_component(url_str, "query", query, _RFC_QUERY_RE) 

555 _check_rfc_component(url_str, "fragment", fragment, _RFC_FRAGMENT_RE) 

556 

557 

558def _special_authority_url( 

559 scheme: str, path: str, query: str, fragment: str, empty: int, encoded: bool 

560) -> "URL": 

561 """Parse "http:host/p" or "http:///host/p" in WHATWG mode. 

562 

563 The WHATWG URL Standard skips any slashes after the scheme of a special 

564 URL and reads an authority, so "http:/example.com/" and 

565 "http:///example.com/" are "http://example.com/". Against a base with the 

566 same scheme, an input without "//" is the relative reference 

567 "/example.com/" instead; its path is kept for join(). 

568 """ 

569 url_str = f"{scheme}://{path.lstrip('/')}" 

570 if query or empty & EMPTY_QUERY: 

571 url_str = f"{url_str}?{query}" 

572 if fragment or empty & EMPTY_FRAGMENT: 

573 url_str = f"{url_str}#{fragment}" 

574 if encoded: 

575 url = _pre_encoded_url(url_str, _WHATWG) 

576 else: 

577 url = _encode_url(url_str, _WHATWG) 

578 path = WHATWG_PATH_REQUOTER(path) 

579 if not empty & EMPTY_AUTHORITY: 

580 url._cache["_join_path"] = path 

581 return url 

582 

583 

584def _is_special_authority_path(scheme: str, path: str, mode: Mode) -> bool: 

585 """Tell if the path of a URL without a host is its authority in WHATWG mode. 

586 

587 Only a path with text after the leading slashes is, so "http:", "http:/" 

588 and "http:///" stay as they are: against a base with the same scheme the 

589 WHATWG parser reads the first two as references to the base, and alone 

590 it rejects all of them. 

591 """ 

592 return mode is _WHATWG and scheme in SCHEME_REQUIRES_HOST and path.lstrip("/") != "" 

593 

594 

595def _backslashes(url_str: str) -> tuple[str, bool]: 

596 """Handle "\\" in a URL string before it is parsed in WHATWG mode. 

597 

598 RFC 3986 has no "\\" in its grammar, so RFC 3986 mode rejects it (see 

599 _check_rfc_url()). The WHATWG URL Standard reads it as "/" in the 

600 authority and the path of a special URL, and so does WHATWG mode; other 

601 schemes keep it, percent-encoded, as do the query and the fragment. 

602 

603 Return the string with "\\" read as "/" and whether it is a relative 

604 reference, whose "\\" is read as "/" only when it is joined with a 

605 special base: against another base, "\\/a" must not become the 

606 network-path reference "//a". 

607 """ 

608 # Find the scheme the way split_url() does. 

609 url = url_str.strip(WHATWG_C0_CONTROL_OR_SPACE) 

610 for b in UNSAFE_URL_BYTES_TO_REMOVE: 

611 url = url.replace(b, "") 

612 i = url.find(":") 

613 relative = True 

614 if i > 0 and all(c in _SCHEME_CHARS for c in url[:i]): 

615 if url[:i].lower() not in SPECIAL_SCHEMES: 

616 return url_str, False 

617 relative = False 

618 end = len(url) 

619 for c in "?#": 

620 if (pos := url.find(c)) >= 0 and pos < end: 

621 end = pos 

622 return url[:end].replace("\\", "/") + url[end:], relative 

623 

624 

625def _encode_url(url_str: str, mode: Mode, backslashes: bool = True) -> "URL": 

626 """Parse unencoded URL.""" 

627 cache: _InternalURLCache = {} 

628 if backslashes and "\\" in url_str: 

629 special_str, relative = _backslashes(url_str) 

630 if not relative: 

631 url_str = special_str 

632 elif not url_str.lstrip(WHATWG_C0_CONTROL_OR_SPACE).startswith("//"): 

633 # Keep "\\" as a character; join() reads it as "/" when the base 

634 # is special. 

635 url = _encode_url(url_str, mode, False) 

636 url._cache["_join_path"] = special_str 

637 return url 

638 else: 

639 # A network-path reference has an authority either way, and one 

640 # with "\\" in it would be rejected. 

641 url_str = special_str 

642 host: str | None 

643 scheme, netloc, path, query, fragment, empty = split_url(url_str) 

644 if not netloc and _is_special_authority_path(scheme, path, mode): 

645 return _special_authority_url(scheme, path, query, fragment, empty, False) 

646 if scheme == "file": 

647 if mode is _WHATWG: 

648 if is_drive_letter(netloc): 

649 path = _whatwg_file_drive(netloc, path) 

650 netloc = "" 

651 empty |= EMPTY_AUTHORITY 

652 if "|" in path: 

653 path = normalize_drive_letter(path) 

654 elif ( 

655 not scheme 

656 and mode is _WHATWG 

657 and ( 

658 is_drive_letter(netloc) 

659 or ("|" in path and normalize_drive_letter(path) is not path) 

660 ) 

661 ): 

662 # A drive letter means something else when the reference is joined 

663 # with a file URL, see join(); it is kept for that. 

664 cache["_join_path"] = ( 

665 f"//{netloc}{path}" if netloc or empty & EMPTY_AUTHORITY else path 

666 ) 

667 if netloc[1:] == "|": 

668 # Not a host, so the file URL is the only meaning it has. 

669 path = _whatwg_file_drive(netloc, path) 

670 netloc = "" 

671 empty |= EMPTY_AUTHORITY 

672 if not netloc: # netloc 

673 if empty & EMPTY_AUTHORITY and scheme in SCHEME_REQUIRES_HOST: 

674 # The WHATWG host parser fails on the empty host, and RFC 9110 

675 # section 4.2.1 rejects it: "http://", "http:///" and "http://?q" 

676 # are invalid. "http:" and "http:?q" have no authority, they are 

677 # references to a base. 

678 _check_missing_host(scheme, mode) 

679 elif ( 

680 empty & EMPTY_AUTHORITY 

681 and not scheme 

682 and mode is _WHATWG 

683 and ("%" in path or "[" in path) 

684 and "_join_path" not in cache 

685 ): 

686 # Against an http base join() reads the path of "///h/p" as an 

687 # authority, where "%38" and "[" must stay as written; the path 

688 # quoter would decode the first and encode the second. "//" keeps 

689 # the empty authority for a file base. 

690 cache["_join_path"] = f"//{path}" 

691 host = "" 

692 else: 

693 if ":" in netloc or "@" in netloc or "[" in netloc: 

694 # Complex netloc 

695 if mode is _WHATWG: 

696 username, password, host, port = split_netloc(netloc) 

697 else: 

698 username, password, host, port = split_netloc_rfc(netloc) 

699 else: 

700 username = password = port = None 

701 host = netloc 

702 if host is None: 

703 # The authority is not empty, so it has userinfo or a port. 

704 _check_missing_host(scheme, mode) 

705 host = "" 

706 if port is None and username is None: 

707 # Only an empty port, which RFC 3986 section 6.2.3 drops: 

708 # "sc://:/" is "sc:///", the authority is kept. 

709 empty |= EMPTY_AUTHORITY 

710 # The parser historically encoded without validation, which let 

711 # control characters (NUL/C0) and IDNA-normalized delimiters into 

712 # the host, producing a ``str(url)`` that yarl cannot re-parse 

713 # (#1829). Validate like the builder APIs do, but keep accepting an 

714 # empty IPv6 zone identifier, which parsing has always allowed (#998). 

715 if ( 

716 scheme not in SPECIAL_SCHEMES 

717 and mode is _WHATWG 

718 and _is_opaque_host(scheme, host) 

719 ): 

720 host = _encode_opaque_host(host) 

721 else: 

722 encoded, whatwg_host, not_rfc = _encode_host( 

723 host, validate_host=True, reject_empty_zone=False 

724 ) 

725 if not_rfc and mode is not _WHATWG: 

726 raise ValueError(_rfc_host_error(host)) 

727 host = encoded 

728 if ( 

729 whatwg_host is not host 

730 and mode is _WHATWG 

731 and (scheme in SPECIAL_SCHEMES or host[0] == "[") 

732 ): 

733 host = _whatwg_special_host(host, whatwg_host) 

734 if mode is _WHATWG and scheme == "file": 

735 _check_whatwg_file_authority(netloc) 

736 if host == "localhost": 

737 # The WHATWG file host state turns "localhost" into the empty 

738 # host: "file://localhost/p" is "file:///p". 

739 host = "" 

740 empty |= EMPTY_AUTHORITY 

741 # Remove brackets as host encoder adds back brackets for IPv6 addresses. 

742 # An IPvFuture address keeps them, see split_netloc(). 

743 # An empty host (RFC mode, e.g. "sc://user@:8080") is None, as for 

744 # built and pre-encoded URLs. 

745 cache["raw_host"] = ( 

746 host[1:-1] if "[" in host and host[1] != "v" else host 

747 ) or None 

748 cache["explicit_port"] = port 

749 if password is None and username is None: 

750 # Fast path for URLs without user, password 

751 netloc = host if port is None else f"{host}:{port}" 

752 cache["raw_user"] = None 

753 cache["raw_password"] = None 

754 else: 

755 if mode is _WHATWG: 

756 # WHATWG writes ":" only before a password that is not 

757 # empty: "http://a:@h" is "http://a@h", "http://:@h" is 

758 # "http://h". RFC 3986 keeps the empty userinfo and an 

759 # empty username is "", see split_netloc_rfc(). 

760 raw_user = _whatwg_requote_userinfo(username) if username else None 

761 raw_password = _whatwg_requote_userinfo(password) if password else None 

762 else: 

763 raw_user = REQUOTER(username) if username else username 

764 raw_password = REQUOTER(password) if password else password 

765 netloc = make_netloc(raw_user, raw_password, host, port) 

766 cache["raw_user"] = raw_user 

767 cache["raw_password"] = raw_password 

768 

769 if path: 

770 # WHATWG mode keeps a "%" that does not start a "%XX" escape, RFC 

771 # mode encodes it as "%25" 

772 path = (WHATWG_PATH_REQUOTER if mode is _WHATWG else PATH_REQUOTER)(path) 

773 if "." in path and (netloc or (mode is _WHATWG and scheme == "file")): 

774 # A file URL always has an authority in WHATWG mode, also an 

775 # empty one, and its path is absolute: "file:///a/../b" and 

776 # "file:a/../b" are "file:///b". 

777 if not netloc and path[0] != "/": 

778 # As a reference the path is relative to the path of a file 

779 # base, so join() takes the path as it is written. 

780 cache["_join_path"] = path 

781 path = f"/{path}" 

782 empty |= EMPTY_AUTHORITY 

783 path = _normalize_path(path, scheme, mode) 

784 elif not scheme and not netloc: 

785 path = _encode_relative_scheme_colon(path) 

786 elif mode is not _WHATWG and "." in path: 

787 # RFC 3986 section 5.2.2 removes the dot segments of any URI 

788 # with a scheme, also one without an authority ("file:.." is 

789 # "file:"). 

790 path = remove_dot_segments(path) 

791 if not netloc and has_dot_prefix(path) and not empty & EMPTY_AUTHORITY: 

792 # Undo the "/." that str() puts in front of a path starting 

793 # with "//" when there is no authority. 

794 path = path[2:] 

795 if query: 

796 query = (WHATWG_QUERY_REQUOTER if mode is _WHATWG else QUERY_REQUOTER)(query) 

797 if fragment: 

798 fragment = (WHATWG_FRAGMENT_REQUOTER if mode is _WHATWG else FRAGMENT_REQUOTER)( 

799 fragment 

800 ) 

801 

802 if not path and scheme == "file" and mode is _WHATWG: 

803 # A file URL has a path in WHATWG mode, "/" at least: "file:" and 

804 # "file://h" are "file:///" and "file://h/". 

805 if not netloc and not empty & EMPTY_AUTHORITY: 

806 # As a reference "file:" and "file:?q" keep the path of a file 

807 # base, so join() takes the path as it is written. 

808 cache["_join_path"] = "" 

809 empty |= EMPTY_AUTHORITY 

810 path = "/" 

811 cache["scheme"] = scheme 

812 cache["raw_path"] = "/" if not path and netloc else path 

813 cache["raw_query_string"] = query 

814 cache["raw_fragment"] = fragment 

815 

816 self = object.__new__(URL) 

817 self._scheme = scheme 

818 self._netloc = netloc 

819 self._path = path 

820 self._query = query 

821 self._fragment = fragment 

822 self._empty = empty 

823 self._cache = cache 

824 self._mode = mode 

825 return self 

826 

827 

828def _pre_encoded_url(url_str: str, mode: Mode) -> "URL": 

829 """Parse pre-encoded URL.""" 

830 val = split_url(url_str) 

831 scheme, netloc, path, query, fragment, empty = val 

832 if not netloc and _is_special_authority_path(scheme, path, mode): 

833 return _special_authority_url(scheme, path, query, fragment, empty, True) 

834 self = object.__new__(URL) 

835 ( 

836 self._scheme, 

837 self._netloc, 

838 self._path, 

839 self._query, 

840 self._fragment, 

841 self._empty, 

842 ) = val 

843 if ( 

844 not self._netloc 

845 and has_dot_prefix(self._path) 

846 and not self._empty & EMPTY_AUTHORITY 

847 ): 

848 # Undo the "/." that str() puts in front of a path starting with 

849 # "//" when there is no authority, as the unencoded parser does. 

850 self._path = self._path[2:] 

851 self._cache = {} 

852 self._mode = mode 

853 return self 

854 

855 

856# One cache per compatibility mode keeps the single ``str`` argument, which 

857# ``lru_cache`` uses as the key directly instead of building a tuple. 

858@lru_cache 

859def encode_url(url_str: str) -> "URL": 

860 """Parse unencoded URL in WHATWG mode.""" 

861 return _encode_url(url_str, Mode.WHATWG) 

862 

863 

864@lru_cache 

865def encode_url_rfc(url_str: str) -> "URL": 

866 """Parse unencoded URL in RFC 3986 mode.""" 

867 _check_rfc_url(url_str) 

868 return _encode_url(url_str, Mode.RFC) 

869 

870 

871@lru_cache 

872def pre_encoded_url(url_str: str) -> "URL": 

873 """Parse pre-encoded URL in WHATWG mode.""" 

874 return _pre_encoded_url(url_str, Mode.WHATWG) 

875 

876 

877@lru_cache 

878def pre_encoded_url_rfc(url_str: str) -> "URL": 

879 """Parse pre-encoded URL in RFC 3986 mode.""" 

880 return _pre_encoded_url(url_str, Mode.RFC) 

881 

882 

883@lru_cache 

884def build_pre_encoded_url( 

885 scheme: str, 

886 authority: str, 

887 user: str | None, 

888 password: str | None, 

889 host: str, 

890 port: int | None, 

891 path: str, 

892 query_string: str, 

893 fragment: str, 

894 mode: Mode, 

895) -> "URL": 

896 """Build a pre-encoded URL from parts.""" 

897 self = object.__new__(URL) 

898 self._scheme = scheme 

899 if authority: 

900 self._netloc = authority 

901 elif host: 

902 if port is not None: 

903 port = None if port == DEFAULT_PORTS.get(scheme) else port 

904 if user is None and password is None: 

905 self._netloc = host if port is None else f"{host}:{port}" 

906 else: 

907 if not user and mode is _WHATWG: 

908 # WHATWG has no empty userinfo, see make_netloc(). 

909 user = None 

910 self._netloc = make_netloc(user, password, host, port) 

911 else: 

912 self._netloc = "" 

913 if path and not scheme and not self._netloc and ":" in path: 

914 path = _encode_relative_colon(path, mode) 

915 self._path = path 

916 self._query = query_string 

917 self._fragment = fragment 

918 self._empty = 0 

919 self._cache = {} 

920 self._mode = mode 

921 return self 

922 

923 

924def _kept_empty(keep_query: bool, keep_fragment: bool) -> int: 

925 """Return the mask of empty components kept by a path replacement.""" 

926 return ( 

927 EMPTY_AUTHORITY 

928 | (EMPTY_QUERY if keep_query else 0) 

929 | (EMPTY_FRAGMENT if keep_fragment else 0) 

930 ) 

931 

932 

933def from_parts_uncached( 

934 scheme: str, 

935 netloc: str, 

936 path: str, 

937 query: str, 

938 fragment: str, 

939 mode: Mode, 

940 empty: int = 0, 

941) -> "URL": 

942 """Create a new URL from parts. 

943 

944 *empty* marks which empty components are present, see split_url(); it 

945 must not have bits for components that are not empty. 

946 """ 

947 self = object.__new__(URL) 

948 self._scheme = scheme 

949 self._netloc = netloc 

950 if path and not scheme and not netloc and ":" in path: 

951 path = _encode_relative_colon(path, mode) 

952 self._path = path 

953 self._query = query 

954 self._fragment = fragment 

955 self._empty = empty 

956 self._cache = {} 

957 self._mode = mode 

958 return self 

959 

960 

961from_parts = lru_cache(from_parts_uncached) 

962 

963 

964@rewrite_module 

965class URL: 

966 # Don't derive from str 

967 # follow pathlib.Path design 

968 # probably URL will not suffer from pathlib problems: 

969 # it's intended for libraries like aiohttp, 

970 # not to be passed into standard library functions like os.open etc. 

971 

972 # URL grammar (RFC 3986) 

973 # pct-encoded = "%" HEXDIG HEXDIG 

974 # reserved = gen-delims / sub-delims 

975 # gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 

976 # sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 

977 # / "*" / "+" / "," / ";" / "=" 

978 # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 

979 # URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 

980 # hier-part = "//" authority path-abempty 

981 # / path-absolute 

982 # / path-rootless 

983 # / path-empty 

984 # scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) 

985 # authority = [ userinfo "@" ] host [ ":" port ] 

986 # userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) 

987 # host = IP-literal / IPv4address / reg-name 

988 # IP-literal = "[" ( IPv6address / IPvFuture ) "]" 

989 # IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) 

990 # IPv6address = 6( h16 ":" ) ls32 

991 # / "::" 5( h16 ":" ) ls32 

992 # / [ h16 ] "::" 4( h16 ":" ) ls32 

993 # / [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 

994 # / [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 

995 # / [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 

996 # / [ *4( h16 ":" ) h16 ] "::" ls32 

997 # / [ *5( h16 ":" ) h16 ] "::" h16 

998 # / [ *6( h16 ":" ) h16 ] "::" 

999 # ls32 = ( h16 ":" h16 ) / IPv4address 

1000 # ; least-significant 32 bits of address 

1001 # h16 = 1*4HEXDIG 

1002 # ; 16 bits of address represented in hexadecimal 

1003 # IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet 

1004 # dec-octet = DIGIT ; 0-9 

1005 # / %x31-39 DIGIT ; 10-99 

1006 # / "1" 2DIGIT ; 100-199 

1007 # / "2" %x30-34 DIGIT ; 200-249 

1008 # / "25" %x30-35 ; 250-255 

1009 # reg-name = *( unreserved / pct-encoded / sub-delims ) 

1010 # port = *DIGIT 

1011 # path = path-abempty ; begins with "/" or is empty 

1012 # / path-absolute ; begins with "/" but not "//" 

1013 # / path-noscheme ; begins with a non-colon segment 

1014 # / path-rootless ; begins with a segment 

1015 # / path-empty ; zero characters 

1016 # path-abempty = *( "/" segment ) 

1017 # path-absolute = "/" [ segment-nz *( "/" segment ) ] 

1018 # path-noscheme = segment-nz-nc *( "/" segment ) 

1019 # path-rootless = segment-nz *( "/" segment ) 

1020 # path-empty = 0<pchar> 

1021 # segment = *pchar 

1022 # segment-nz = 1*pchar 

1023 # segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) 

1024 # ; non-zero-length segment without any colon ":" 

1025 # pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 

1026 # query = *( pchar / "/" / "?" ) 

1027 # fragment = *( pchar / "/" / "?" ) 

1028 # URI-reference = URI / relative-ref 

1029 # relative-ref = relative-part [ "?" query ] [ "#" fragment ] 

1030 # relative-part = "//" authority path-abempty 

1031 # / path-absolute 

1032 # / path-noscheme 

1033 # / path-empty 

1034 # absolute-URI = scheme ":" hier-part [ "?" query ] 

1035 __slots__ = ( 

1036 "_cache", 

1037 "_scheme", 

1038 "_netloc", 

1039 "_path", 

1040 "_query", 

1041 "_fragment", 

1042 "_empty", 

1043 "_mode", 

1044 ) 

1045 

1046 _cache: _InternalURLCache 

1047 _scheme: str 

1048 _netloc: str 

1049 _path: str 

1050 _query: str 

1051 _fragment: str 

1052 # EMPTY_AUTHORITY, EMPTY_QUERY and EMPTY_FRAGMENT bits for components 

1053 # that are present but empty, e.g. the query of "http://h/?" 

1054 _empty: int 

1055 _mode: Mode 

1056 

1057 def __new__( 

1058 cls, 

1059 val: "str | SplitResult | URL | UndefinedType" = UNDEFINED, 

1060 *, 

1061 encoded: bool = False, 

1062 strict: bool | None = None, 

1063 # Internally ``None`` stands for ``Mode.WHATWG``; it is not a 

1064 # documented value. It is the default rather than the enum member 

1065 # because ``mode is None`` compares against a constant, while 

1066 # ``mode is Mode.WHATWG`` (or a module-level alias) needs a 

1067 # global lookup; on the cached ``URL(str)`` hot path that lookup alone 

1068 # was measured at about 3% of the whole call (CodSpeed). The 

1069 # ``== "whatwg"`` test then matches both the string and the enum 

1070 # member without calling ``_to_mode()``. 

1071 mode: ModeType = None, # type: ignore[assignment] 

1072 ) -> "URL": 

1073 if strict is not None: # pragma: no cover 

1074 warnings.warn("strict parameter is ignored") 

1075 if type(val) is str: 

1076 if mode is None or mode == "whatwg": # type: ignore[redundant-expr] 

1077 return pre_encoded_url(val) if encoded else encode_url(val) 

1078 _to_mode(mode) # reject unknown values; the rest is RFC 

1079 return pre_encoded_url_rfc(val) if encoded else encode_url_rfc(val) 

1080 mode = _WHATWG if mode is None else _to_mode(mode) # type: ignore[redundant-expr] 

1081 if type(val) is cls: 

1082 if val._mode is mode: 

1083 return val 

1084 return _moved_url(val, val._scheme, mode) 

1085 if type(val) is SplitResult: 

1086 if not encoded: 

1087 raise ValueError("Cannot apply decoding to SplitResult") 

1088 return from_parts(*val, mode) 

1089 if isinstance(val, str): 

1090 return URL(str(val), encoded=encoded, mode=mode) 

1091 if val is UNDEFINED: 

1092 # Special case for UNDEFINED since it might be unpickling and we do 

1093 # not want to cache as the `__set_state__` call would mutate the URL 

1094 # object in the `pre_encoded_url` or `encoded_url` caches. 

1095 self = object.__new__(URL) 

1096 self._scheme = self._netloc = self._path = self._query = self._fragment = "" 

1097 self._empty = 0 

1098 self._cache = {} 

1099 self._mode = mode 

1100 return self 

1101 raise TypeError("Constructor parameter should be str") 

1102 

1103 @classmethod 

1104 def build( 

1105 cls, 

1106 *, 

1107 scheme: str = "", 

1108 authority: str = "", 

1109 user: str | None = None, 

1110 password: str | None = None, 

1111 host: str = "", 

1112 port: int | None = None, 

1113 path: str = "", 

1114 query: Query | None = None, 

1115 query_string: str = "", 

1116 fragment: str = "", 

1117 encoded: bool = False, 

1118 # Internally ``None`` stands for ``Mode.WHATWG``, as in ``URL()``. 

1119 mode: ModeType = None, # type: ignore[assignment] 

1120 ) -> "URL": 

1121 """Creates and returns a new URL""" 

1122 mode = _WHATWG if mode is None else _to_mode(mode) # type: ignore[redundant-expr] 

1123 

1124 if authority and (user or password or host or port): 

1125 raise ValueError( 

1126 'Can\'t mix "authority" with "user", "password", "host" or "port".' 

1127 ) 

1128 if port is not None and not isinstance(port, int): 

1129 raise TypeError(f"The port is required to be int, got {type(port)!r}.") 

1130 if port and not host: 

1131 raise ValueError('Can\'t build URL with "port" but without "host".') 

1132 if query and query_string: 

1133 raise ValueError('Only one of "query" or "query_string" should be passed') 

1134 if ( 

1135 scheme is None # type: ignore[redundant-expr] 

1136 or authority is None # type: ignore[redundant-expr] 

1137 or host is None # type: ignore[redundant-expr] 

1138 or path is None # type: ignore[redundant-expr] 

1139 or query_string is None # type: ignore[redundant-expr] 

1140 or fragment is None 

1141 ): 

1142 raise TypeError( 

1143 'NoneType is illegal for "scheme", "authority", "host", "path", ' 

1144 '"query_string", and "fragment" args, use empty string instead.' 

1145 ) 

1146 

1147 if query: 

1148 query_string = get_str_query(query) or "" 

1149 

1150 if encoded: 

1151 return build_pre_encoded_url( 

1152 scheme, 

1153 authority, 

1154 user, 

1155 password, 

1156 host, 

1157 port, 

1158 path, 

1159 query_string, 

1160 fragment, 

1161 mode, 

1162 ) 

1163 

1164 if scheme == "file" and mode is _WHATWG: 

1165 # As the parser does, see _whatwg_file_drive() and 

1166 # normalize_drive_letter(). 

1167 if is_drive_letter(authority) and path[:1] in ("", "/"): 

1168 path = _whatwg_file_drive(authority, path) 

1169 authority = "" 

1170 if "|" in path: 

1171 path = normalize_drive_letter(path) 

1172 

1173 self = object.__new__(URL) 

1174 self._scheme = scheme 

1175 self._empty = 0 

1176 _host: str | None = None 

1177 if authority: 

1178 if mode is _WHATWG: 

1179 user, password, _host, port = split_netloc(authority) 

1180 else: 

1181 _check_rfc_authority(authority) 

1182 user, password, _host, port = split_netloc_rfc(authority) 

1183 if not _host: 

1184 _check_missing_host(scheme, mode) 

1185 if ( 

1186 _host 

1187 and scheme not in SPECIAL_SCHEMES 

1188 and mode is _WHATWG 

1189 and _is_opaque_host(scheme, _host) 

1190 ): 

1191 _host = _encode_opaque_host(_host) 

1192 elif _host: 

1193 if "%" in _host and ":" in _host and _host[0] != "[": 

1194 check_zone_id(_host) 

1195 encoded_host, whatwg_host, not_rfc = _encode_host( 

1196 _host, validate_host=False 

1197 ) 

1198 if not_rfc and mode is not _WHATWG: 

1199 raise ValueError(_rfc_host_error(_host)) 

1200 _host = encoded_host 

1201 if ( 

1202 whatwg_host is not _host 

1203 and mode is _WHATWG 

1204 and (scheme in SPECIAL_SCHEMES or _host[0] == "[") 

1205 ): 

1206 _host = _whatwg_special_host(_host, whatwg_host) 

1207 else: 

1208 _host = "" 

1209 if port is None and user is None: 

1210 # As the parser does, see _encode_url(). 

1211 self._empty = EMPTY_AUTHORITY 

1212 elif ( 

1213 host 

1214 and scheme not in SPECIAL_SCHEMES 

1215 and mode is _WHATWG 

1216 and _is_opaque_host(scheme, host) 

1217 ): 

1218 _host = _encode_opaque_host(host) 

1219 elif host: 

1220 _host, whatwg_host, not_rfc = _encode_host(host, validate_host=True) 

1221 if not_rfc and mode is not _WHATWG: 

1222 raise ValueError(_rfc_host_error(host)) 

1223 if ( 

1224 whatwg_host is not _host 

1225 and mode is _WHATWG 

1226 and (scheme in SPECIAL_SCHEMES or _host[0] == "[") 

1227 ): 

1228 _host = _whatwg_special_host(_host, whatwg_host) 

1229 else: 

1230 self._netloc = "" 

1231 

1232 if _host is not None: 

1233 if port is not None: 

1234 port = None if port == DEFAULT_PORTS.get(scheme) else port 

1235 if user is None and password is None: 

1236 self._netloc = _host if port is None else f"{_host}:{port}" 

1237 else: 

1238 if not password and mode is _WHATWG: 

1239 # As the parser does, see _encode_url(). 

1240 password = None 

1241 if not user and mode is _WHATWG: 

1242 # An empty user is None, see make_netloc(). 

1243 user = None 

1244 self._netloc = make_netloc( 

1245 user, password, _host, port, True, mode is _WHATWG 

1246 ) 

1247 

1248 path = PATH_QUOTER(path) if path else path 

1249 if path and (self._netloc or self._empty): 

1250 if "." in path: 

1251 path = _normalize_path(path, scheme, mode) 

1252 if path[0] != "/": 

1253 msg = ( 

1254 "Path in a URL with authority should " 

1255 "start with a slash ('/') if set" 

1256 ) 

1257 raise ValueError(msg) 

1258 elif path and scheme and mode is not _WHATWG and "." in path: 

1259 # Without an authority, as the parser does in RFC 3986 mode. 

1260 path = remove_dot_segments(path) 

1261 elif path and scheme == "file" and "." in path: 

1262 # A file URL has an authority in WHATWG mode, also an empty one, 

1263 # and its path is absolute. 

1264 path = normalize_file_path(path if path[0] == "/" else f"/{path}") 

1265 

1266 if path and not self._scheme and not self._netloc and ":" in path: 

1267 path = _encode_relative_colon(path, mode) 

1268 self._path = path 

1269 if not query and query_string: 

1270 query_string = QUERY_QUOTER(query_string) 

1271 self._query = query_string 

1272 self._fragment = FRAGMENT_QUOTER(fragment) if fragment else fragment 

1273 self._cache = {} 

1274 self._mode = mode 

1275 if scheme == "file" and mode is _WHATWG: 

1276 if self._netloc: 

1277 # As the parser does, see _check_whatwg_file_authority(). The 

1278 # path was checked and normalized as one with an authority 

1279 # above. 

1280 _check_whatwg_file_authority(authority or self._netloc) 

1281 if _host == "localhost": 

1282 self._netloc = "" 

1283 self._empty = EMPTY_AUTHORITY 

1284 if not path: 

1285 # A file URL has an authority and a path in WHATWG mode, the 

1286 # path is "/" at least. 

1287 self._path = "/" 

1288 if not self._netloc: 

1289 self._empty = EMPTY_AUTHORITY 

1290 return self 

1291 

1292 def __init_subclass__(cls) -> NoReturn: 

1293 raise TypeError(f"Inheriting a class {cls!r} from URL is forbidden") 

1294 

1295 def __str__(self) -> str: 

1296 if self._empty or not self._netloc: 

1297 # Rare: empty components, or no authority, which a special 

1298 # scheme in WHATWG mode still prints as "//". 

1299 return self._str_with_empty() 

1300 if ( 

1301 not self._path 

1302 and (self._query or self._fragment) 

1303 and ( 

1304 self._scheme in SPECIAL_SCHEMES 

1305 or not self._scheme 

1306 or self._scheme.lower() in SPECIAL_SCHEMES 

1307 ) 

1308 ): 

1309 # A special scheme has the path "/" at least, so "http://h?q" is 

1310 # "http://h/?q". A non-special URL keeps its empty path, as 

1311 # RFC 3986 and WHATWG write it: "sc://h?q". A relative reference 

1312 # has no scheme to tell, and keeps the "/". URL.build() keeps 

1313 # the scheme as given, hence lower(). 

1314 path = "/" 

1315 else: 

1316 path = self._path 

1317 if (port := self.explicit_port) is not None and port == DEFAULT_PORTS.get( 

1318 self._scheme 

1319 ): 

1320 # port normalization - using None for default ports to remove from rendering 

1321 # https://datatracker.ietf.org/doc/html/rfc3986.html#section-6.2.3 

1322 host = self.host_subcomponent 

1323 netloc = make_netloc(self.raw_user, self.raw_password, host, None) 

1324 else: 

1325 netloc = self._netloc 

1326 return unsplit_result(self._scheme, netloc, path, self._query, self._fragment) 

1327 

1328 def _str_with_empty(self) -> str: 

1329 """Render a URL with empty components or without an authority.""" 

1330 if ( 

1331 not self._path 

1332 and self._netloc 

1333 and ( 

1334 self._scheme in SPECIAL_SCHEMES 

1335 or not self._scheme 

1336 or self._scheme.lower() in SPECIAL_SCHEMES 

1337 ) 

1338 ): 

1339 # As in __str__(), "sc://h?" keeps its empty path. 

1340 path = "/" 

1341 else: 

1342 path = self._path 

1343 if (port := self.explicit_port) is not None and port == DEFAULT_PORTS.get( 

1344 self._scheme 

1345 ): 

1346 host = self.host_subcomponent 

1347 netloc = make_netloc(self.raw_user, self.raw_password, host, None) 

1348 else: 

1349 netloc = self._netloc 

1350 empty = self._str_empty if not netloc else self._empty 

1351 if not empty: 

1352 return unsplit_result( 

1353 self._scheme, netloc, path, self._query, self._fragment 

1354 ) 

1355 return unsplit_result_empty( 

1356 self._scheme, netloc, path, self._query, self._fragment, empty 

1357 ) 

1358 

1359 def __repr__(self) -> str: 

1360 if self._mode is Mode.WHATWG: 

1361 return f"{self.__class__.__name__}('{str(self)}')" 

1362 return f"{self.__class__.__name__}('{str(self)}', mode='{self._mode}')" 

1363 

1364 def __bytes__(self) -> bytes: 

1365 return str(self).encode("ascii") 

1366 

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

1368 if type(other) is not URL: 

1369 return NotImplemented 

1370 

1371 path1 = "/" if not self._path and self._netloc else self._path 

1372 path2 = "/" if not other._path and other._netloc else other._path 

1373 return ( 

1374 self._scheme == other._scheme 

1375 and self._netloc == other._netloc 

1376 and path1 == path2 

1377 and self._query == other._query 

1378 and self._fragment == other._fragment 

1379 and ( 

1380 self._empty == other._empty 

1381 if self._netloc 

1382 else self._cmp_empty == other._cmp_empty 

1383 ) 

1384 ) 

1385 

1386 def __hash__(self) -> int: 

1387 if (ret := self._cache.get("hash")) is None: 

1388 path = "/" if not self._path and self._netloc else self._path 

1389 if (self._empty or not self._netloc) and (cmp_empty := self._cmp_empty): 

1390 ret = hash( 

1391 ( 

1392 self._scheme, 

1393 self._netloc, 

1394 path, 

1395 self._query, 

1396 self._fragment, 

1397 cmp_empty, 

1398 ) 

1399 ) 

1400 else: 

1401 ret = hash( 

1402 (self._scheme, self._netloc, path, self._query, self._fragment) 

1403 ) 

1404 self._cache["hash"] = ret 

1405 return ret 

1406 

1407 def __le__(self, other: object) -> bool: 

1408 if type(other) is not URL: 

1409 return NotImplemented 

1410 return (self._val, self._cmp_empty) <= (other._val, other._cmp_empty) 

1411 

1412 def __lt__(self, other: object) -> bool: 

1413 if type(other) is not URL: 

1414 return NotImplemented 

1415 return (self._val, self._cmp_empty) < (other._val, other._cmp_empty) 

1416 

1417 def __ge__(self, other: object) -> bool: 

1418 if type(other) is not URL: 

1419 return NotImplemented 

1420 return (self._val, self._cmp_empty) >= (other._val, other._cmp_empty) 

1421 

1422 def __gt__(self, other: object) -> bool: 

1423 if type(other) is not URL: 

1424 return NotImplemented 

1425 return (self._val, self._cmp_empty) > (other._val, other._cmp_empty) 

1426 

1427 def __truediv__(self, name: str) -> "URL": 

1428 if not isinstance(name, str): 

1429 return NotImplemented 

1430 return self._make_child((str(name),)) 

1431 

1432 def __mod__(self, query: Query) -> "URL": 

1433 return self.update_query(query) 

1434 

1435 def __bool__(self) -> bool: 

1436 return bool( 

1437 self._netloc or self._path or self._query or self._fragment or self._empty 

1438 ) 

1439 

1440 def __getstate__( 

1441 self, 

1442 ) -> tuple[SplitURLType, str, int] | tuple[SplitURLType, str, int, str]: 

1443 # Return a plain tuple rather than a ``SplitResult``. Constructing a 

1444 # ``SplitResult`` via ``tuple.__new__`` skips its ``__init__`` and on 

1445 # Python 3.15+ leaves ``_keep_empty`` unset, which breaks pickling: the 

1446 # new ``SplitResult.__getstate__`` indexes a state that ends up as 

1447 # ``None`` (gh-1632). ``__setstate__`` already unpacks both shapes, so 

1448 # pickles produced by older yarl releases (which embed a real 

1449 # ``SplitResult``) still load correctly. The compatibility mode goes 

1450 # second and the mask of empty components third; older releases 

1451 # ignore trailing items of the state. What join() reads for "http:g" 

1452 # (see _special_authority_url()) or for a relative reference with 

1453 # "\\" (see _backslashes()) goes last, when there is one. 

1454 if (join_path := self._cache.get("_join_path")) is not None: 

1455 return (self._val, self._mode.value, self._empty, join_path) 

1456 return (self._val, self._mode.value, self._empty) 

1457 

1458 def __setstate__( 

1459 self, 

1460 # ``(val,)`` or ``(val, mode)`` from older releases, 

1461 # ``(val, mode, empty)``, ``(val, mode, empty, join_path)``, or the 

1462 # legacy default style 

1463 # ``(None, {"_val": val})``. 

1464 state: tuple[Any, ...], 

1465 ) -> None: 

1466 mode = Mode.WHATWG 

1467 empty = 0 

1468 cache: _InternalURLCache = {} 

1469 if state[0] is None and isinstance(state[1], dict): 

1470 # default style pickle 

1471 val = state[1]["_val"] 

1472 else: 

1473 val, *rest = state 

1474 if rest: 

1475 mode = Mode(rest[0]) 

1476 if rest[1:]: 

1477 empty = rest[1] 

1478 if rest[2:]: 

1479 cache["_join_path"] = rest[2] 

1480 self._scheme, self._netloc, self._path, self._query, self._fragment = val 

1481 self._empty = empty 

1482 self._cache = cache 

1483 self._mode = mode 

1484 

1485 def _keep_join_path(self, url: "URL") -> None: 

1486 """Give url, a copy of self with another query or fragment, what 

1487 join() reads instead of self's path: the path of "http:g" (see 

1488 _special_authority_url()) or, for a relative reference without an 

1489 authority, the reference with "\\" read as "/" (see _backslashes()). 

1490 

1491 url must be a new URL, not one shared through the from_parts() cache. 

1492 """ 

1493 url._cache["_join_path"] = self._cache["_join_path"] 

1494 

1495 def _cache_netloc(self) -> None: 

1496 """Cache the netloc parts of the URL.""" 

1497 c = self._cache 

1498 if self._mode is _WHATWG: 

1499 split_loc = split_netloc(self._netloc) 

1500 else: 

1501 # An empty username is "" in RFC 3986 mode, see split_netloc_rfc(). 

1502 split_loc = split_netloc_rfc(self._netloc) 

1503 c["raw_user"], c["raw_password"], c["raw_host"], c["explicit_port"] = split_loc 

1504 

1505 def is_absolute(self) -> bool: 

1506 """A check for absolute URLs. 

1507 

1508 Return True for absolute ones (having scheme or starting 

1509 with //), False otherwise. 

1510 

1511 Is is preferred to call the .absolute property instead 

1512 as it is cached. 

1513 """ 

1514 return self.absolute 

1515 

1516 def is_default_port(self) -> bool: 

1517 """A check for default port. 

1518 

1519 Return True if port is default for specified scheme, 

1520 e.g. 'http://python.org' or 'http://python.org:80', False 

1521 otherwise. 

1522 

1523 Return False for relative URLs. 

1524 

1525 """ 

1526 if (explicit := self.explicit_port) is None: 

1527 # If the explicit port is None, then the URL must be 

1528 # using the default port unless its a relative URL 

1529 # which does not have an implicit port / default port 

1530 return self._netloc != "" 

1531 return explicit == DEFAULT_PORTS.get(self._scheme) 

1532 

1533 def origin(self) -> "URL": 

1534 """Return an URL with scheme, host and port parts only. 

1535 

1536 user, password, path, query and fragment are removed. 

1537 

1538 """ 

1539 # TODO: add a keyword-only option for keeping user/pass maybe? 

1540 return self._origin 

1541 

1542 @property 

1543 def _cmp_empty(self) -> int: 

1544 """The mask of empty components that matters for comparisons. 

1545 

1546 It follows what str() writes out: in WHATWG mode a file URL always 

1547 prints "//", so "file:/p" and "file:///p" are the same URL there, 

1548 while RFC mode prints and compares them differently. 

1549 """ 

1550 if not self._netloc and self._mode is _WHATWG and self._scheme == "file": 

1551 return self._empty | EMPTY_AUTHORITY 

1552 return self._empty 

1553 

1554 @property 

1555 def _str_empty(self) -> int: 

1556 """The mask of empty components that str() writes out. 

1557 

1558 In WHATWG mode a file URL always has an authority, so "//" is 

1559 written even when it is missing. Other special schemes need a host; 

1560 without one the URL is a reference to a base, such as "http:?q". 

1561 """ 

1562 if self._mode is _WHATWG and self._scheme == "file": 

1563 return self._empty | EMPTY_AUTHORITY 

1564 return self._empty 

1565 

1566 @cached_property 

1567 def _val(self) -> SplitURLType: 

1568 return (self._scheme, self._netloc, self._path, self._query, self._fragment) 

1569 

1570 @cached_property 

1571 def _origin(self) -> "URL": 

1572 """Return an URL with scheme, host and port parts only. 

1573 

1574 user, password, path, query and fragment are removed. 

1575 """ 

1576 if not (netloc := self._netloc): 

1577 raise ValueError("URL should be absolute") 

1578 if not (scheme := self._scheme): 

1579 raise ValueError("URL should have scheme") 

1580 if "@" in netloc: 

1581 # The host is None for an authority without one ("user@:8080"). 

1582 encoded_host = self.host_subcomponent or "" 

1583 netloc = make_netloc(None, None, encoded_host, self.explicit_port) 

1584 elif not self._path and not self._query and not self._fragment: 

1585 if not self._empty: 

1586 return self 

1587 return from_parts(scheme, netloc, "", "", "", self._mode) 

1588 

1589 def relative(self) -> "URL": 

1590 """Return a relative part of the URL. 

1591 

1592 scheme, user, password, host and port are removed. 

1593 

1594 """ 

1595 if not self._netloc: 

1596 raise ValueError("URL should be absolute") 

1597 return from_parts( 

1598 "", "", self._path, self._query, self._fragment, self._mode, self._empty 

1599 ) 

1600 

1601 @cached_property 

1602 def absolute(self) -> bool: 

1603 """A check for absolute URLs. 

1604 

1605 Return True for absolute ones (having scheme or starting 

1606 with //), False otherwise. 

1607 

1608 """ 

1609 # `netloc`` is an empty string for relative URLs 

1610 # Checking `netloc` is faster than checking `hostname` 

1611 # because `hostname` is a property that does some extra work 

1612 # to parse the host from the `netloc` 

1613 return self._netloc != "" 

1614 

1615 @cached_property 

1616 def scheme(self) -> str: 

1617 """Scheme for absolute URLs. 

1618 

1619 Empty string for relative URLs or URLs starting with // 

1620 

1621 """ 

1622 return self._scheme 

1623 

1624 @cached_property 

1625 def raw_authority(self) -> str: 

1626 """Encoded authority part of URL. 

1627 

1628 Empty string for relative URLs. 

1629 

1630 """ 

1631 return self._netloc 

1632 

1633 @cached_property 

1634 def authority(self) -> str: 

1635 """Decoded authority part of URL. 

1636 

1637 Empty string for relative URLs. 

1638 

1639 """ 

1640 if (host := self.host) is None and self._netloc: 

1641 # An authority without a host, e.g. "user@:8080" in RFC mode. 

1642 host = "" 

1643 return make_netloc(self.user, self.password, host, self.port) 

1644 

1645 @cached_property 

1646 def raw_user(self) -> str | None: 

1647 """Encoded user part of URL. 

1648 

1649 None if user is missing. 

1650 

1651 """ 

1652 # not .username 

1653 self._cache_netloc() 

1654 return self._cache["raw_user"] 

1655 

1656 @cached_property 

1657 def user(self) -> str | None: 

1658 """Decoded user part of URL. 

1659 

1660 None if user is missing. 

1661 

1662 """ 

1663 if (raw_user := self.raw_user) is None: 

1664 return None 

1665 return UNQUOTER(raw_user) 

1666 

1667 @cached_property 

1668 def raw_password(self) -> str | None: 

1669 """Encoded password part of URL. 

1670 

1671 None if password is missing. 

1672 

1673 """ 

1674 self._cache_netloc() 

1675 return self._cache["raw_password"] 

1676 

1677 @cached_property 

1678 def password(self) -> str | None: 

1679 """Decoded password part of URL. 

1680 

1681 None if password is missing. 

1682 

1683 """ 

1684 if (raw_password := self.raw_password) is None: 

1685 return None 

1686 return UNQUOTER(raw_password) 

1687 

1688 @property 

1689 def mode(self) -> Mode: 

1690 """Standard the URL follows: RFC 3986 or the WHATWG URL Standard.""" 

1691 return self._mode 

1692 

1693 @cached_property 

1694 def raw_host(self) -> str | None: 

1695 """Encoded host part of URL. 

1696 

1697 None for relative URLs. 

1698 

1699 When working with IPv6 addresses, use the `host_subcomponent` property instead 

1700 as it will return the host subcomponent with brackets. 

1701 """ 

1702 # Use host instead of hostname for sake of shortness 

1703 # May add .hostname prop later 

1704 self._cache_netloc() 

1705 return self._cache["raw_host"] 

1706 

1707 @cached_property 

1708 def host(self) -> str | None: 

1709 """Decoded host part of URL. 

1710 

1711 None for relative URLs. 

1712 

1713 For IPv6 hosts that carry an RFC 6874 zone identifier, the 

1714 ``%25`` zone separator is decoded back to ``%`` and the zone 

1715 identifier is percent-decoded; the encoded form is still 

1716 available via :attr:`raw_host` and :attr:`host_subcomponent`. 

1717 

1718 The opaque host of a non-special URL in WHATWG mode has its 

1719 non-ASCII characters percent-decoded, the rest stays as it is. 

1720 

1721 """ 

1722 if (raw := self.raw_host) is None: 

1723 return None 

1724 if ( 

1725 self._scheme not in SPECIAL_SCHEMES 

1726 and self._mode is _WHATWG 

1727 and _is_opaque_host(self._scheme, raw) 

1728 ): 

1729 return _decode_opaque_host(raw) 

1730 if raw and (raw[-1].isdigit() or raw[-1] == "]") or ":" in raw: 

1731 # IP addresses are never IDNA encoded. The RFC 6874 zone 

1732 # separator "%25" of an IPv6 address decodes to "%"; the zone 

1733 # identifier is decoded except for "%", the delimiters of an 

1734 # authority and octets that are not UTF-8, so that passing 

1735 # the result to with_host() gives the same URL back. 

1736 if "%25" in raw: 

1737 if ":" not in raw: 

1738 return raw.replace("%25", "%") 

1739 address, _, zone = raw.partition("%25") 

1740 return f"{address}%{ZONE_ID_UNQUOTER(zone)}" 

1741 return raw 

1742 return _idna_decode(raw) 

1743 

1744 @cached_property 

1745 def host_subcomponent(self) -> str | None: 

1746 """Return the host subcomponent part of URL. 

1747 

1748 None for relative URLs. 

1749 

1750 https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 

1751 

1752 `IP-literal = "[" ( IPv6address / IPvFuture ) "]"` 

1753 

1754 Examples: 

1755 - `http://example.com:8080` -> `example.com` 

1756 - `http://example.com:80` -> `example.com` 

1757 - `https://127.0.0.1:8443` -> `127.0.0.1` 

1758 - `https://[::1]:8443` -> `[::1]` 

1759 - `http://[::1]` -> `[::1]` 

1760 

1761 """ 

1762 if (raw := self.raw_host) is None: 

1763 return None 

1764 # An IPvFuture address keeps its brackets in the raw host. 

1765 return f"[{raw}]" if ":" in raw and raw[-1] != "]" else raw 

1766 

1767 @cached_property 

1768 def host_port_subcomponent(self) -> str | None: 

1769 """Return the host and port subcomponent part of URL. 

1770 

1771 Trailing dots are removed from the host part. 

1772 

1773 This value is suitable for use in the Host header of an HTTP request. 

1774 

1775 None for relative URLs. 

1776 

1777 https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 

1778 `IP-literal = "[" ( IPv6address / IPvFuture ) "]"` 

1779 https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.3 

1780 port = *DIGIT 

1781 

1782 Examples: 

1783 - `http://example.com:8080` -> `example.com:8080` 

1784 - `http://example.com:80` -> `example.com` 

1785 - `http://example.com.:80` -> `example.com` 

1786 - `https://127.0.0.1:8443` -> `127.0.0.1:8443` 

1787 - `https://[::1]:8443` -> `[::1]:8443` 

1788 - `http://[::1]` -> `[::1]` 

1789 

1790 """ 

1791 if (raw := self.raw_host) is None: 

1792 return None 

1793 if raw and raw[-1] == ".": 

1794 # Remove all trailing dots from the netloc as while 

1795 # they are valid FQDNs in DNS, TLS validation fails. 

1796 # See https://github.com/aio-libs/aiohttp/issues/3636. 

1797 # To avoid string manipulation we only call rstrip if 

1798 # the last character is a dot. 

1799 raw = raw.rstrip(".") 

1800 if ":" in raw and raw[-1] != "]": 

1801 raw = f"[{raw}]" 

1802 port = self.explicit_port 

1803 if port is None or port == DEFAULT_PORTS.get(self._scheme): 

1804 return raw 

1805 return f"{raw}:{port}" 

1806 

1807 @cached_property 

1808 def port(self) -> int | None: 

1809 """Port part of URL, with scheme-based fallback. 

1810 

1811 None for relative URLs or URLs without explicit port and 

1812 scheme without default port substitution. 

1813 

1814 """ 

1815 if (explicit_port := self.explicit_port) is not None: 

1816 return explicit_port 

1817 return DEFAULT_PORTS.get(self._scheme) 

1818 

1819 @cached_property 

1820 def explicit_port(self) -> int | None: 

1821 """Port part of URL, without scheme-based fallback. 

1822 

1823 None for relative URLs or URLs without explicit port. 

1824 

1825 """ 

1826 self._cache_netloc() 

1827 return self._cache["explicit_port"] 

1828 

1829 @cached_property 

1830 def raw_path(self) -> str: 

1831 """Encoded path of URL. 

1832 

1833 / for absolute URLs without path part. 

1834 

1835 """ 

1836 return self._path if self._path or not self._netloc else "/" 

1837 

1838 @cached_property 

1839 def path(self) -> str: 

1840 """Decoded path of URL. 

1841 

1842 / for absolute URLs without path part. 

1843 

1844 """ 

1845 return UNQUOTER(self._path) if self._path else "/" if self._netloc else "" 

1846 

1847 @cached_property 

1848 def path_safe(self) -> str: 

1849 """Decoded path of URL. 

1850 

1851 / for absolute URLs without path part. 

1852 

1853 / (%2F) and % (%25) are not decoded 

1854 

1855 """ 

1856 if self._path: 

1857 return PATH_SAFE_UNQUOTER(self._path) 

1858 return "/" if self._netloc else "" 

1859 

1860 @cached_property 

1861 def _parsed_query(self) -> list[tuple[str, str]]: 

1862 """Parse query part of URL.""" 

1863 return query_to_pairs(self._query) 

1864 

1865 @cached_property 

1866 def query(self) -> "MultiDictProxy[str]": 

1867 """A MultiDictProxy representing parsed query parameters in decoded 

1868 representation. 

1869 

1870 Empty value if URL has no query part. 

1871 

1872 """ 

1873 return MultiDictProxy(MultiDict(self._parsed_query)) 

1874 

1875 @cached_property 

1876 def raw_query_string(self) -> str: 

1877 """Encoded query part of URL. 

1878 

1879 Empty string if query is missing. 

1880 

1881 """ 

1882 return self._query 

1883 

1884 @cached_property 

1885 def query_string(self) -> str: 

1886 """Decoded query part of URL. 

1887 

1888 Empty string if query is missing. 

1889 

1890 """ 

1891 return QS_UNQUOTER(self._query) if self._query else "" 

1892 

1893 @cached_property 

1894 def path_qs(self) -> str: 

1895 """Decoded path of URL with query.""" 

1896 if q := self.query_string: 

1897 return f"{self.path}?{q}" 

1898 if (empty := self._empty) and empty & EMPTY_QUERY: 

1899 return f"{self.path}?" 

1900 return self.path 

1901 

1902 @cached_property 

1903 def raw_path_qs(self) -> str: 

1904 """Encoded path of URL with query.""" 

1905 if q := self._query: 

1906 return f"{self._path}?{q}" if self._path or not self._netloc else f"/?{q}" 

1907 if (empty := self._empty) and empty & EMPTY_QUERY: 

1908 return f"{self._path}?" if self._path or not self._netloc else "/?" 

1909 return self._path if self._path or not self._netloc else "/" 

1910 

1911 @cached_property 

1912 def raw_fragment(self) -> str: 

1913 """Encoded fragment part of URL. 

1914 

1915 Empty string if fragment is missing. 

1916 

1917 """ 

1918 return self._fragment 

1919 

1920 @cached_property 

1921 def fragment(self) -> str: 

1922 """Decoded fragment part of URL. 

1923 

1924 Empty string if fragment is missing. 

1925 

1926 """ 

1927 return UNQUOTER(self._fragment) if self._fragment else "" 

1928 

1929 @cached_property 

1930 def raw_parts(self) -> tuple[str, ...]: 

1931 """A tuple containing encoded *path* parts. 

1932 

1933 ('/',) for absolute URLs if *path* is missing. 

1934 

1935 """ 

1936 path = self._path 

1937 if self._netloc or self._empty & EMPTY_AUTHORITY: 

1938 return ("/", *path[1:].split("/")) if path else ("/",) 

1939 if path and path[0] == "/": 

1940 return ("/", *path[1:].split("/")) 

1941 return tuple(path.split("/")) 

1942 

1943 @cached_property 

1944 def parts(self) -> tuple[str, ...]: 

1945 """A tuple containing decoded *path* parts. 

1946 

1947 ('/',) for absolute URLs if *path* is missing. 

1948 

1949 """ 

1950 return tuple(UNQUOTER(part) for part in self.raw_parts) 

1951 

1952 @cached_property 

1953 def parent(self) -> "URL": 

1954 """A new URL with last part of path removed and cleaned up query and 

1955 fragment. 

1956 

1957 """ 

1958 path = self._path 

1959 authority_empty = self._empty & EMPTY_AUTHORITY 

1960 if not path or path == "/": 

1961 if self._fragment or self._query or self._empty & ~EMPTY_AUTHORITY: 

1962 return from_parts( 

1963 self._scheme, 

1964 self._netloc, 

1965 path, 

1966 "", 

1967 "", 

1968 self._mode, 

1969 authority_empty, 

1970 ) 

1971 return self 

1972 parts = path.split("/") 

1973 if not (path := "/".join(parts[:-1])) and self._scheme == "file": 

1974 # A file URL has a path in WHATWG mode, "/" at least. 

1975 path = _empty_path(self._scheme, self._mode) 

1976 return from_parts( 

1977 self._scheme, 

1978 self._netloc, 

1979 path, 

1980 "", 

1981 "", 

1982 self._mode, 

1983 authority_empty, 

1984 ) 

1985 

1986 @cached_property 

1987 def raw_name(self) -> str: 

1988 """The last part of raw_parts.""" 

1989 parts = self.raw_parts 

1990 if not self._netloc: 

1991 return parts[-1] 

1992 parts = parts[1:] 

1993 return parts[-1] if parts else "" 

1994 

1995 @cached_property 

1996 def name(self) -> str: 

1997 """The last part of parts.""" 

1998 return UNQUOTER(self.raw_name) 

1999 

2000 @cached_property 

2001 def raw_suffix(self) -> str: 

2002 name = self.raw_name 

2003 i = name.rfind(".") 

2004 return name[i:] if 0 < i < len(name) - 1 else "" 

2005 

2006 @cached_property 

2007 def suffix(self) -> str: 

2008 return UNQUOTER(self.raw_suffix) 

2009 

2010 @cached_property 

2011 def raw_suffixes(self) -> tuple[str, ...]: 

2012 name = self.raw_name 

2013 if name.endswith("."): 

2014 return () 

2015 name = name.lstrip(".") 

2016 return tuple("." + suffix for suffix in name.split(".")[1:]) 

2017 

2018 @cached_property 

2019 def suffixes(self) -> tuple[str, ...]: 

2020 return tuple(UNQUOTER(suffix) for suffix in self.raw_suffixes) 

2021 

2022 def _make_child(self, paths: "Sequence[str]", encoded: bool = False) -> "URL": 

2023 """ 

2024 add paths to self._path, accounting for absolute vs relative paths, 

2025 keep existing, but do not create new, empty segments 

2026 """ 

2027 parsed: list[str] = [] 

2028 needs_normalize: bool = False 

2029 if ( 

2030 self._scheme == "file" 

2031 and self._mode is _WHATWG 

2032 and self._path in ("", "/") 

2033 and paths 

2034 and not encoded 

2035 ): 

2036 # The first segment of a file URL can be a drive letter, see 

2037 # normalize_drive_letter(). 

2038 paths = [normalize_drive_letter(paths[0]), *paths[1:]] 

2039 for idx, path in enumerate(reversed(paths)): 

2040 # empty segment of last is not removed 

2041 last = idx == 0 

2042 if path and path[0] == "/": 

2043 raise ValueError( 

2044 f"Appending path {path!r} starting from slash is forbidden" 

2045 ) 

2046 # We need to quote the path if it is not already encoded 

2047 # This cannot be done at the end because the existing 

2048 # path is already quoted and we do not want to double quote 

2049 # the existing path. 

2050 path = path if encoded else PATH_QUOTER(path) 

2051 needs_normalize |= "." in path 

2052 segments = path.split("/") 

2053 segments.reverse() 

2054 # remove trailing empty segment for all but the last path 

2055 parsed += segments[1:] if not last and segments[0] == "" else segments 

2056 

2057 if (path := self._path) and (old_segments := path.split("/")): 

2058 # If the old path ends with a slash, the last segment is an empty string 

2059 # and should be removed before adding the new path segments. 

2060 old = old_segments[:-1] if old_segments[-1] == "" else old_segments 

2061 old.reverse() 

2062 parsed += old 

2063 

2064 # If the netloc is present, inject a leading slash when adding a 

2065 # path to an absolute URL where there was none before. 

2066 netloc = self._netloc 

2067 authority_empty = self._empty & EMPTY_AUTHORITY 

2068 # A file URL has an authority in WHATWG mode, also an empty one. 

2069 has_authority = ( 

2070 netloc 

2071 or authority_empty 

2072 or (self._scheme == "file" and self._mode is _WHATWG) 

2073 ) 

2074 if has_authority and parsed and parsed[-1] != "": 

2075 parsed.append("") 

2076 

2077 parsed.reverse() 

2078 if not has_authority or not needs_normalize: 

2079 path = "/".join(parsed) 

2080 if needs_normalize and self._scheme and self._mode is not _WHATWG: 

2081 # Without an authority, as the parser does in RFC 3986 mode. 

2082 path = remove_dot_segments(path) 

2083 return from_parts( 

2084 self._scheme, 

2085 netloc, 

2086 path, 

2087 "", 

2088 "", 

2089 self._mode, 

2090 authority_empty, 

2091 ) 

2092 

2093 if self._scheme == "file" and self._mode is _WHATWG: 

2094 # Keep the drive letter, as the parser does. 

2095 path = normalize_file_path("/".join(parsed)) 

2096 else: 

2097 path = "/".join(normalize_path_segments(parsed)) 

2098 # If normalizing the path segments removed the leading slash, add it back. 

2099 if path and path[0] != "/": 

2100 path = f"/{path}" 

2101 return from_parts( 

2102 self._scheme, netloc, path, "", "", self._mode, authority_empty 

2103 ) 

2104 

2105 def with_scheme(self, scheme: str) -> "URL": 

2106 """Return a new URL with scheme replaced.""" 

2107 # N.B. doesn't cleanup query/fragment 

2108 if not isinstance(scheme, str): 

2109 raise TypeError("Invalid scheme type") 

2110 lower_scheme = scheme.lower() 

2111 netloc = self._netloc 

2112 if not netloc and lower_scheme in SCHEME_REQUIRES_HOST: 

2113 msg = ( 

2114 "scheme replacement is not allowed for " 

2115 f"relative URLs for the {lower_scheme} scheme" 

2116 ) 

2117 raise ValueError(msg) 

2118 if lower_scheme == "file" or (self._scheme not in SPECIAL_SCHEMES and netloc): 

2119 # A special scheme in WHATWG mode had its host parsed already, but 

2120 # only for a file URL is "localhost" dropped, and the path of a 

2121 # file URL is not empty. 

2122 return _moved_url(self, lower_scheme, self._mode) 

2123 return from_parts( 

2124 lower_scheme, 

2125 netloc, 

2126 self._path, 

2127 self._query, 

2128 self._fragment, 

2129 self._mode, 

2130 self._empty, 

2131 ) 

2132 

2133 def with_user(self, user: str | None) -> "URL": 

2134 """Return a new URL with user replaced. 

2135 

2136 Autoencode user if needed. 

2137 

2138 Clear user/password if user is None. 

2139 

2140 """ 

2141 # N.B. doesn't cleanup query/fragment 

2142 if user: 

2143 if not isinstance(user, str): 

2144 raise TypeError("Invalid user type") 

2145 user = QUOTER(user) 

2146 if (";" in user or "=" in user) and self._mode is _WHATWG: 

2147 # Inlined quote_userinfo(), with_user() is a benchmarked path. 

2148 user = user.replace(";", "%3B").replace("=", "%3D") 

2149 password = self.raw_password 

2150 elif user is None: 

2151 password = None 

2152 elif isinstance(user, str): 

2153 # An empty user keeps the empty userinfo in RFC 3986 mode, 

2154 # "http://@h"; WHATWG has none, see make_netloc(). 

2155 if self._mode is _WHATWG: 

2156 user = None 

2157 password = self.raw_password 

2158 else: 

2159 raise TypeError("Invalid user type") 

2160 if not (netloc := self._netloc): 

2161 raise ValueError("user replacement is not allowed for relative URLs") 

2162 encoded_host = self.host_subcomponent or "" 

2163 netloc = make_netloc(user, password, encoded_host, self.explicit_port) 

2164 if self._scheme == "file" and self._mode is _WHATWG: 

2165 _check_whatwg_file_authority(netloc) 

2166 return from_parts( 

2167 self._scheme, 

2168 netloc, 

2169 self._path, 

2170 self._query, 

2171 self._fragment, 

2172 self._mode, 

2173 self._empty, 

2174 ) 

2175 

2176 def with_password(self, password: str | None) -> "URL": 

2177 """Return a new URL with password replaced. 

2178 

2179 Autoencode password if needed. 

2180 

2181 Clear password if argument is None. 

2182 

2183 """ 

2184 # N.B. doesn't cleanup query/fragment 

2185 if password is None: 

2186 pass 

2187 elif isinstance(password, str): 

2188 if password: 

2189 password = QUOTER(password) 

2190 if (";" in password or "=" in password) and self._mode is _WHATWG: 

2191 # Inlined quote_userinfo(), as in with_user(). 

2192 password = password.replace(";", "%3B").replace("=", "%3D") 

2193 elif self._mode is _WHATWG: 

2194 # WHATWG has no empty password, see _encode_url(). 

2195 password = None 

2196 else: 

2197 raise TypeError("Invalid password type") 

2198 if not (netloc := self._netloc): 

2199 raise ValueError("password replacement is not allowed for relative URLs") 

2200 encoded_host = self.host_subcomponent or "" 

2201 port = self.explicit_port 

2202 netloc = make_netloc(self.raw_user, password, encoded_host, port) 

2203 if self._scheme == "file" and self._mode is _WHATWG: 

2204 _check_whatwg_file_authority(netloc) 

2205 return from_parts( 

2206 self._scheme, 

2207 netloc, 

2208 self._path, 

2209 self._query, 

2210 self._fragment, 

2211 self._mode, 

2212 self._empty, 

2213 ) 

2214 

2215 def with_host(self, host: str) -> "URL": 

2216 """Return a new URL with host replaced. 

2217 

2218 Autoencode host if needed. 

2219 

2220 Changing host for relative URLs is not allowed, use .join() 

2221 instead. 

2222 

2223 """ 

2224 # N.B. doesn't cleanup query/fragment 

2225 if not isinstance(host, str): 

2226 raise TypeError("Invalid host type") 

2227 if not (netloc := self._netloc): 

2228 raise ValueError("host replacement is not allowed for relative URLs") 

2229 if not host: 

2230 raise ValueError("host removing is not allowed") 

2231 if ( 

2232 self._scheme not in SPECIAL_SCHEMES 

2233 and self._mode is _WHATWG 

2234 and _is_opaque_host(self._scheme, host) 

2235 ): 

2236 encoded_host = _encode_opaque_host(host) 

2237 else: 

2238 encoded_host, whatwg_host, not_rfc = _encode_host(host, validate_host=True) 

2239 if not_rfc and self._mode is not _WHATWG: 

2240 raise ValueError(_rfc_host_error(host)) 

2241 if ( 

2242 whatwg_host is not encoded_host 

2243 and self._mode is _WHATWG 

2244 and (self._scheme in SPECIAL_SCHEMES or encoded_host[0] == "[") 

2245 ): 

2246 encoded_host = _whatwg_special_host(encoded_host, whatwg_host) 

2247 port = self.explicit_port 

2248 netloc = make_netloc(self.raw_user, self.raw_password, encoded_host, port) 

2249 if self._scheme == "file" and self._mode is _WHATWG: 

2250 # As the parser does, see _check_whatwg_file_authority(). 

2251 _check_whatwg_file_authority(netloc) 

2252 if encoded_host == "localhost": 

2253 return from_parts( 

2254 self._scheme, 

2255 "", 

2256 self._path or "/", 

2257 self._query, 

2258 self._fragment, 

2259 self._mode, 

2260 self._empty | EMPTY_AUTHORITY, 

2261 ) 

2262 return from_parts( 

2263 self._scheme, 

2264 netloc, 

2265 self._path, 

2266 self._query, 

2267 self._fragment, 

2268 self._mode, 

2269 self._empty, 

2270 ) 

2271 

2272 def with_port(self, port: int | None) -> "URL": 

2273 """Return a new URL with port replaced. 

2274 

2275 Clear port to default if None is passed. 

2276 

2277 """ 

2278 # N.B. doesn't cleanup query/fragment 

2279 if port is not None: 

2280 if isinstance(port, bool) or not isinstance(port, int): 

2281 raise TypeError(f"port should be int or None, got {type(port)}") 

2282 if not (0 <= port <= 65535): 

2283 raise ValueError(f"port must be between 0 and 65535, got {port}") 

2284 if not (netloc := self._netloc): 

2285 raise ValueError("port replacement is not allowed for relative URLs") 

2286 encoded_host = self.host_subcomponent or "" 

2287 netloc = make_netloc(self.raw_user, self.raw_password, encoded_host, port) 

2288 if self._scheme == "file" and self._mode is _WHATWG: 

2289 _check_whatwg_file_authority(netloc) 

2290 return from_parts( 

2291 self._scheme, 

2292 netloc, 

2293 self._path, 

2294 self._query, 

2295 self._fragment, 

2296 self._mode, 

2297 self._empty, 

2298 ) 

2299 

2300 def with_path( 

2301 self, 

2302 path: str, 

2303 *, 

2304 encoded: bool = False, 

2305 keep_query: bool = False, 

2306 keep_fragment: bool = False, 

2307 ) -> "URL": 

2308 """Return a new URL with path replaced.""" 

2309 netloc = self._netloc 

2310 if not encoded: 

2311 if "|" in path and self._scheme == "file" and self._mode is _WHATWG: 

2312 # As the parser does, see normalize_drive_letter(). 

2313 path = normalize_drive_letter(path) 

2314 path = PATH_QUOTER(path) 

2315 if netloc or (self._scheme == "file" and self._mode is _WHATWG): 

2316 # A file URL has an authority in WHATWG mode, also an empty one. 

2317 if "." in path: 

2318 path = _normalize_path(path, self._scheme, self._mode) 

2319 elif self._scheme and self._mode is not _WHATWG and "." in path: 

2320 # Without an authority, as the parser does in RFC 3986 mode. 

2321 path = remove_dot_segments(path) 

2322 if not path: 

2323 path = _empty_path(self._scheme, self._mode) 

2324 elif path[0] != "/": 

2325 path = f"/{path}" 

2326 query = self._query if keep_query else "" 

2327 fragment = self._fragment if keep_fragment else "" 

2328 if empty := self._empty: 

2329 empty &= _kept_empty(keep_query, keep_fragment) 

2330 return from_parts( 

2331 self._scheme, netloc, path, query, fragment, self._mode, empty 

2332 ) 

2333 return from_parts(self._scheme, netloc, path, query, fragment, self._mode) 

2334 

2335 @overload 

2336 def with_query(self, query: Query) -> "URL": ... 

2337 

2338 @overload 

2339 def with_query(self, **kwargs: QueryVariable) -> "URL": ... 

2340 

2341 def with_query(self, *args: Any, **kwargs: Any) -> "URL": 

2342 """Return a new URL with query part replaced. 

2343 

2344 Accepts any Mapping (e.g. dict, multidict.MultiDict instances) 

2345 or str, autoencode the argument if needed. 

2346 

2347 A sequence of (key, value) pairs is supported as well. 

2348 

2349 It also can take an arbitrary number of keyword arguments. 

2350 

2351 Clear query if None is passed. 

2352 

2353 """ 

2354 # N.B. doesn't cleanup query/fragment 

2355 query = get_str_query(*args, **kwargs) or "" 

2356 url = from_parts_uncached( 

2357 self._scheme, 

2358 self._netloc, 

2359 self._path, 

2360 query, 

2361 self._fragment, 

2362 self._mode, 

2363 self._empty and self._empty & ~EMPTY_QUERY, 

2364 ) 

2365 if "_join_path" in self._cache: 

2366 self._keep_join_path(url) 

2367 return url 

2368 

2369 @overload 

2370 def extend_query(self, query: Query) -> "URL": ... 

2371 

2372 @overload 

2373 def extend_query(self, **kwargs: QueryVariable) -> "URL": ... 

2374 

2375 def extend_query(self, *args: Any, **kwargs: Any) -> "URL": 

2376 """Return a new URL with query part combined with the existing. 

2377 

2378 This method will not remove existing query parameters. 

2379 

2380 Example: 

2381 >>> url = URL('http://example.com/?a=1&b=2') 

2382 >>> url.extend_query(a=3, c=4) 

2383 URL('http://example.com/?a=1&b=2&a=3&c=4') 

2384 """ 

2385 if not (new_query := get_str_query(*args, **kwargs)): 

2386 return self 

2387 if query := self._query: 

2388 # both strings are already encoded so we can use a simple 

2389 # string join 

2390 query += new_query if query[-1] == "&" else f"&{new_query}" 

2391 else: 

2392 query = new_query 

2393 url = from_parts_uncached( 

2394 self._scheme, 

2395 self._netloc, 

2396 self._path, 

2397 query, 

2398 self._fragment, 

2399 self._mode, 

2400 self._empty and self._empty & ~EMPTY_QUERY, 

2401 ) 

2402 if "_join_path" in self._cache: 

2403 self._keep_join_path(url) 

2404 return url 

2405 

2406 @overload 

2407 def update_query(self, query: Query) -> "URL": ... 

2408 

2409 @overload 

2410 def update_query(self, **kwargs: QueryVariable) -> "URL": ... 

2411 

2412 def update_query(self, *args: Any, **kwargs: Any) -> "URL": 

2413 """Return a new URL with query part updated. 

2414 

2415 This method will overwrite existing query parameters. 

2416 

2417 Example: 

2418 >>> url = URL('http://example.com/?a=1&b=2') 

2419 >>> url.update_query(a=3, c=4) 

2420 URL('http://example.com/?a=3&b=2&c=4') 

2421 """ 

2422 in_query: ( 

2423 str 

2424 | Mapping[str, QueryVariable] 

2425 | Sequence[tuple[str | istr, SimpleQuery]] 

2426 | None 

2427 ) 

2428 if kwargs: 

2429 if args: 

2430 msg = "Either kwargs or single query parameter must be present" 

2431 raise ValueError(msg) 

2432 in_query = kwargs 

2433 elif len(args) == 1: 

2434 in_query = args[0] 

2435 else: 

2436 raise ValueError("Either kwargs or single query parameter must be present") 

2437 

2438 if in_query is None: 

2439 query = "" 

2440 elif not in_query: 

2441 query = self._query 

2442 elif isinstance(in_query, Mapping): 

2443 qm: MultiDict[QueryVariable] = MultiDict(self._parsed_query) 

2444 qm.update(in_query) 

2445 query = get_str_query_from_sequence_iterable(qm.items()) 

2446 elif isinstance(in_query, str): 

2447 qstr: MultiDict[str] = MultiDict(self._parsed_query) 

2448 qstr.update(query_to_pairs(in_query)) 

2449 query = get_str_query_from_iterable(qstr.items()) 

2450 elif isinstance(in_query, (bytes, bytearray, memoryview)): 

2451 msg = "Invalid query type: bytes, bytearray and memoryview are forbidden" 

2452 raise TypeError(msg) 

2453 elif isinstance(in_query, Sequence): 

2454 # We don't expect sequence values if we're given a list of pairs 

2455 # already; only mappings like builtin `dict` which can't have the 

2456 # same key pointing to multiple values are allowed to use 

2457 # `_query_seq_pairs`. 

2458 if TYPE_CHECKING: 

2459 in_query = cast(Sequence[tuple[str | istr, SimpleQuery]], in_query) 

2460 qs: MultiDict[SimpleQuery] = MultiDict(self._parsed_query) 

2461 qs.update(in_query) 

2462 query = get_str_query_from_iterable(qs.items()) 

2463 else: 

2464 raise TypeError( 

2465 "Invalid query type: only str, mapping or " 

2466 "sequence of (key, value) pairs is allowed" 

2467 ) 

2468 # An empty query stays when nothing is added, "None" removes it. 

2469 if (empty := self._empty) and (query or in_query is None): 

2470 empty &= ~EMPTY_QUERY 

2471 url = from_parts_uncached( 

2472 self._scheme, 

2473 self._netloc, 

2474 self._path, 

2475 query, 

2476 self._fragment, 

2477 self._mode, 

2478 empty, 

2479 ) 

2480 if "_join_path" in self._cache: 

2481 self._keep_join_path(url) 

2482 return url 

2483 

2484 def without_query_params(self, *query_params: str) -> "URL": 

2485 """Remove some keys from query part and return new URL.""" 

2486 params_to_remove = set(query_params) & self.query.keys() 

2487 if not params_to_remove: 

2488 return self 

2489 return self.with_query( 

2490 tuple( 

2491 (name, value) 

2492 for name, value in self.query.items() 

2493 if name not in params_to_remove 

2494 ) 

2495 ) 

2496 

2497 def with_fragment(self, fragment: str | None) -> "URL": 

2498 """Return a new URL with fragment replaced. 

2499 

2500 Autoencode fragment if needed. 

2501 

2502 Clear fragment to default if None is passed. 

2503 

2504 """ 

2505 # N.B. doesn't cleanup query/fragment 

2506 if fragment is None: 

2507 raw_fragment = "" 

2508 elif not isinstance(fragment, str): 

2509 raise TypeError("Invalid fragment type") 

2510 else: 

2511 raw_fragment = FRAGMENT_QUOTER(fragment) 

2512 if empty := self._empty: 

2513 if self._fragment == raw_fragment and not empty & EMPTY_FRAGMENT: 

2514 return self 

2515 empty &= ~EMPTY_FRAGMENT 

2516 elif self._fragment == raw_fragment: 

2517 return self 

2518 if "_join_path" in self._cache: 

2519 # from_parts() may return a shared URL, which must not get it. 

2520 url = from_parts_uncached( 

2521 self._scheme, 

2522 self._netloc, 

2523 self._path, 

2524 self._query, 

2525 raw_fragment, 

2526 self._mode, 

2527 empty, 

2528 ) 

2529 self._keep_join_path(url) 

2530 return url 

2531 return from_parts( 

2532 self._scheme, 

2533 self._netloc, 

2534 self._path, 

2535 self._query, 

2536 raw_fragment, 

2537 self._mode, 

2538 empty, 

2539 ) 

2540 

2541 def with_name( 

2542 self, 

2543 name: str, 

2544 *, 

2545 keep_query: bool = False, 

2546 keep_fragment: bool = False, 

2547 ) -> "URL": 

2548 """Return a new URL with name (last part of path) replaced. 

2549 

2550 Query and fragment parts are cleaned up. 

2551 

2552 Name is encoded if needed. 

2553 

2554 """ 

2555 # N.B. DOES cleanup query/fragment 

2556 if not isinstance(name, str): 

2557 raise TypeError("Invalid name type") 

2558 if "/" in name: 

2559 raise ValueError("Slash in name is not allowed") 

2560 name = PATH_QUOTER(name) 

2561 if name in (".", ".."): 

2562 raise ValueError(". and .. values are forbidden") 

2563 parts = list(self.raw_parts) 

2564 netloc = self._netloc 

2565 if netloc or self._empty & EMPTY_AUTHORITY: 

2566 if len(parts) == 1: 

2567 parts.append(name) 

2568 else: 

2569 parts[-1] = name 

2570 parts[0] = "" # replace leading '/' 

2571 else: 

2572 parts[-1] = name 

2573 if parts[0] == "/": 

2574 parts[0] = "" # replace leading '/' 

2575 

2576 query = self._query if keep_query else "" 

2577 fragment = self._fragment if keep_fragment else "" 

2578 if empty := self._empty: 

2579 empty &= _kept_empty(keep_query, keep_fragment) 

2580 return from_parts( 

2581 self._scheme, 

2582 netloc, 

2583 "/".join(parts), 

2584 query, 

2585 fragment, 

2586 self._mode, 

2587 empty, 

2588 ) 

2589 return from_parts( 

2590 self._scheme, netloc, "/".join(parts), query, fragment, self._mode 

2591 ) 

2592 

2593 def with_suffix( 

2594 self, 

2595 suffix: str, 

2596 *, 

2597 keep_query: bool = False, 

2598 keep_fragment: bool = False, 

2599 ) -> "URL": 

2600 """Return a new URL with suffix (file extension of name) replaced. 

2601 

2602 Query and fragment parts are cleaned up. 

2603 

2604 suffix is encoded if needed. 

2605 """ 

2606 if not isinstance(suffix, str): 

2607 raise TypeError("Invalid suffix type") 

2608 if suffix and not suffix[0] == "." or suffix == "." or "/" in suffix: 

2609 raise ValueError(f"Invalid suffix {suffix!r}") 

2610 name = self.raw_name 

2611 if not name: 

2612 raise ValueError(f"{self!r} has an empty name") 

2613 old_suffix = self.raw_suffix 

2614 suffix = PATH_QUOTER(suffix) 

2615 name = name + suffix if not old_suffix else name[: -len(old_suffix)] + suffix 

2616 if name in (".", ".."): 

2617 raise ValueError(". and .. values are forbidden") 

2618 parts = list(self.raw_parts) 

2619 netloc = self._netloc 

2620 if netloc or self._empty & EMPTY_AUTHORITY: 

2621 if len(parts) == 1: 

2622 parts.append(name) 

2623 else: 

2624 parts[-1] = name 

2625 parts[0] = "" # replace leading '/' 

2626 else: 

2627 parts[-1] = name 

2628 if parts[0] == "/": 

2629 parts[0] = "" # replace leading '/' 

2630 

2631 query = self._query if keep_query else "" 

2632 fragment = self._fragment if keep_fragment else "" 

2633 if empty := self._empty: 

2634 empty &= _kept_empty(keep_query, keep_fragment) 

2635 return from_parts( 

2636 self._scheme, 

2637 netloc, 

2638 "/".join(parts), 

2639 query, 

2640 fragment, 

2641 self._mode, 

2642 empty, 

2643 ) 

2644 return from_parts( 

2645 self._scheme, netloc, "/".join(parts), query, fragment, self._mode 

2646 ) 

2647 

2648 def join(self, url: "URL") -> "URL": 

2649 """Join URLs 

2650 

2651 Construct a full (“absolute”) URL by combining a “base URL” 

2652 (self) with another URL (url). 

2653 

2654 Informally, this uses components of the base URL, in 

2655 particular the addressing scheme, the network location and 

2656 (part of) the path, to provide missing components in the 

2657 relative URL. 

2658 

2659 """ 

2660 if type(url) is not URL: 

2661 raise TypeError("url should be URL") 

2662 

2663 scheme = url._scheme or self._scheme 

2664 # A reference with a scheme is used as is (RFC 3986 section 5.2.2). 

2665 # In WHATWG mode "http:g" is still resolved against an http base, the 

2666 # backward-compatible behavior RFC 3986 section 5.4.2 permits. 

2667 if scheme != self._scheme or ( 

2668 url._scheme and (scheme not in USES_RELATIVE or self._mode is not _WHATWG) 

2669 ): 

2670 if ( 

2671 not url._netloc 

2672 and self._mode is _WHATWG 

2673 and scheme != self._scheme 

2674 and scheme in SCHEME_REQUIRES_HOST 

2675 ): 

2676 # "http:" and "http:?q" are references to an http base only; 

2677 # against any other base WHATWG reads them as absolute URLs 

2678 # without a host and fails. 

2679 raise ValueError( 

2680 f"Invalid URL: host is required for absolute urls" 

2681 f" with the {scheme} scheme" 

2682 ) 

2683 # The result follows the base URL's compatibility mode. 

2684 return url if url._mode is self._mode else URL(url, mode=self._mode) 

2685 

2686 if ( 

2687 not self._netloc 

2688 and self._mode is _WHATWG 

2689 and not url._scheme 

2690 and self._scheme 

2691 and self._scheme not in SPECIAL_SCHEMES 

2692 and not self._empty & EMPTY_AUTHORITY 

2693 and self._path[:1] != "/" 

2694 and ( 

2695 url._netloc 

2696 or url._path 

2697 or url._query 

2698 or url._empty & ~EMPTY_FRAGMENT 

2699 or not (url._fragment or url._empty & EMPTY_FRAGMENT) 

2700 ) 

2701 ): 

2702 # WHATWG resolves only a fragment against a base with an opaque 

2703 # path, such as "sc:sd" or "mailto:a@b"; RFC 3986 section 5.2.2 

2704 # merges the paths ("i" against "sc:sd" is "sc:i"). 

2705 raise ValueError( 

2706 f"Invalid URL: cannot resolve {str(url)!r} against" 

2707 f" {str(self)!r}, which has an opaque path" 

2708 ) 

2709 

2710 if "_join_path" in url._cache: 

2711 join_path = url._cache["_join_path"] 

2712 if url._netloc and url._scheme: 

2713 # "http:g" was parsed in WHATWG mode as "http://g/", but 

2714 # against an http base it is the relative reference "g". 

2715 url = from_parts( 

2716 scheme, 

2717 "", 

2718 join_path, 

2719 url._query, 

2720 url._fragment, 

2721 url._mode, 

2722 url._empty, 

2723 ) 

2724 elif url._scheme: 

2725 # "file:a/../b", a rootless file path with dot segments, or 

2726 # "file:" and "file:?q" without a path, see _encode_url(). 

2727 url = from_parts( 

2728 scheme, 

2729 "", 

2730 join_path, 

2731 url._query, 

2732 url._fragment, 

2733 url._mode, 

2734 url._empty & ~EMPTY_AUTHORITY, 

2735 ) 

2736 elif scheme == "file" and self._mode is _WHATWG: 

2737 # WHATWG reads a reference against a file URL in the file 

2738 # state, as it reads one with the "file:" scheme; there "C|" 

2739 # is the drive letter "C:" and "//C:" is no authority. 

2740 # "#" keeps a space before the query, which URL() would strip. 

2741 ref = encode_url(f"file:{join_path}#") 

2742 url = from_parts( 

2743 scheme, 

2744 ref._netloc, 

2745 ref._path, 

2746 url._query, 

2747 url._fragment, 

2748 url._mode, 

2749 ref._empty & EMPTY_AUTHORITY | url._empty & ~EMPTY_AUTHORITY, 

2750 ) 

2751 elif _is_pipe_drive_authority(join_path): 

2752 raise ValueError( 

2753 f"Invalid URL: {join_path!r} has a drive letter for an" 

2754 f" authority, which only a file URL accepts" 

2755 ) 

2756 elif url._empty & EMPTY_AUTHORITY and scheme in SCHEME_REQUIRES_HOST: 

2757 # "///h/p" with "%" or "[" in it, see _encode_url(). 

2758 return self._join_empty_host(url, scheme, join_path) 

2759 elif scheme in SPECIAL_SCHEMES and self._mode is _WHATWG: 

2760 # A relative reference with "\\" (see _backslashes()); the 

2761 # query and the fragment may have changed since it was parsed. 

2762 ref = encode_url(join_path) 

2763 if not ref._netloc and ref._empty & EMPTY_AUTHORITY: 

2764 # "/\\/h/p" is "///h/p", see _join_empty_host(). 

2765 return self._join_empty_host( 

2766 url, scheme, ref._cache.get("_join_path", ref._path) 

2767 ) 

2768 url = from_parts( 

2769 ref._scheme, 

2770 ref._netloc, 

2771 ref._path, 

2772 url._query, 

2773 url._fragment, 

2774 url._mode, 

2775 ref._empty & EMPTY_AUTHORITY | url._empty, 

2776 ) 

2777 

2778 if url._empty or self._empty: 

2779 return self._join_with_empty(url, scheme) 

2780 

2781 if join_netloc := url._netloc: 

2782 join_path = url._path 

2783 join_empty = 0 

2784 if not url._scheme or url._mode is not _WHATWG: 

2785 # A schemeless or RFC-mode reference had no WHATWG host check. 

2786 join_netloc, join_path, join_empty = _moved_parts( 

2787 url, scheme, self._mode, 0 

2788 ) 

2789 return from_parts( 

2790 scheme, 

2791 join_netloc, 

2792 join_path, 

2793 url._query, 

2794 url._fragment, 

2795 self._mode, 

2796 join_empty, 

2797 ) 

2798 

2799 orig_path = self._path 

2800 if join_path := url._path: 

2801 if scheme == "file" and self._mode is _WHATWG: 

2802 path = _file_join_path(orig_path, join_path) 

2803 else: 

2804 if join_path[0] == "/": 

2805 path = join_path 

2806 elif not orig_path: 

2807 path = f"/{join_path}" if self._netloc else join_path 

2808 elif orig_path[-1] == "/": 

2809 path = f"{orig_path}{join_path}" 

2810 else: 

2811 # Merge on the encoded base path, dropping its last 

2812 # segment, so percent-encoded delimiters in the base are 

2813 # kept as is. 

2814 path = orig_path[: orig_path.rfind("/") + 1] + join_path 

2815 if "." in path: 

2816 path = ( 

2817 normalize_path(path) 

2818 if self._netloc or self._mode is _WHATWG or not scheme 

2819 # RFC 3986 section 5.2.4, also for a path without a 

2820 # leading "/": "sd/../i" is "/i". 

2821 else remove_dot_segments(path) 

2822 ) 

2823 else: 

2824 path = orig_path 

2825 

2826 return from_parts( 

2827 scheme, 

2828 self._netloc, 

2829 path, 

2830 url._query if join_path or url._query else self._query, 

2831 url._fragment, 

2832 self._mode, 

2833 ) 

2834 

2835 def _join_with_empty(self, url: "URL", scheme: str) -> "URL": 

2836 """join() for URLs with empty components; the same algorithm as 

2837 join() takes for other URLs, plus the empty component rules.""" 

2838 join_empty = url._empty 

2839 if url._netloc or join_empty & EMPTY_AUTHORITY: 

2840 if not url._netloc and scheme in SCHEME_REQUIRES_HOST: 

2841 # A path edited after parsing, such as by with_name(), has 

2842 # lost the authority as written, see _encode_url(); its "[" 

2843 # stays "%5B", which the host parser rejects. 

2844 return self._join_empty_host(url, scheme, url._path) 

2845 join_netloc = url._netloc 

2846 join_path = url._path 

2847 if not url._scheme or url._mode is not _WHATWG: 

2848 join_netloc, join_path, join_empty = _moved_parts( 

2849 url, scheme, self._mode, join_empty 

2850 ) 

2851 return from_parts( 

2852 scheme, 

2853 join_netloc, 

2854 join_path, 

2855 url._query, 

2856 url._fragment, 

2857 self._mode, 

2858 join_empty, 

2859 ) 

2860 

2861 orig_path = self._path 

2862 has_authority = self._netloc or self._empty & EMPTY_AUTHORITY 

2863 if join_path := url._path: 

2864 if scheme == "file" and self._mode is _WHATWG: 

2865 path = _file_join_path(orig_path, join_path) 

2866 else: 

2867 if join_path[0] == "/": 

2868 path = join_path 

2869 elif not orig_path: 

2870 path = f"/{join_path}" if has_authority else join_path 

2871 elif orig_path[-1] == "/": 

2872 path = f"{orig_path}{join_path}" 

2873 else: 

2874 # Merge on the encoded base path, dropping its last 

2875 # segment, so percent-encoded delimiters in the base are 

2876 # kept as is. 

2877 path = orig_path[: orig_path.rfind("/") + 1] + join_path 

2878 if "." in path: 

2879 path = ( 

2880 normalize_path(path) 

2881 if has_authority or self._mode is _WHATWG or not scheme 

2882 # RFC 3986 section 5.2.4, also for a path without a 

2883 # leading "/": "sd/../i" is "/i". 

2884 else remove_dot_segments(path) 

2885 ) 

2886 else: 

2887 path = orig_path 

2888 

2889 # The query comes from the reference when it has a path or a query, 

2890 # and the fragment always does, per RFC 3986 section 5.2.2. 

2891 if join_path or url._query or join_empty & EMPTY_QUERY: 

2892 query = url._query 

2893 query_empty = join_empty & EMPTY_QUERY 

2894 else: 

2895 query = self._query 

2896 query_empty = self._empty & EMPTY_QUERY 

2897 return from_parts( 

2898 scheme, 

2899 self._netloc, 

2900 path, 

2901 query, 

2902 url._fragment, 

2903 self._mode, 

2904 self._empty & EMPTY_AUTHORITY | query_empty | join_empty & EMPTY_FRAGMENT, 

2905 ) 

2906 

2907 def _join_empty_host(self, url: "URL", scheme: str, path: str) -> "URL": 

2908 """join() for a reference with an empty authority, such as "///a", 

2909 against a base whose scheme needs a host. 

2910 

2911 RFC 3986 section 5.2.2 takes the empty authority of the reference, 

2912 which RFC 9110 section 4.2.1 rejects for http, so RFC 3986 mode 

2913 fails as it does on "http:///a". The WHATWG parser reads such a 

2914 reference in the special authority ignore slashes state, where the 

2915 host follows the slashes: "///a/b" is "http://a/b" and "//" fails. 

2916 path is the path of the reference, as written when the path quoter 

2917 would have changed its authority part, see _encode_url(). 

2918 """ 

2919 if self._mode is not _WHATWG: 

2920 raise ValueError( 

2921 f"Invalid URL: joining {str(url)!r} gives an empty host," 

2922 f" which the {scheme} scheme does not allow" 

2923 ) 

2924 # "#" keeps a space before the query, which URL() would strip. 

2925 joined = URL(f"{scheme}://{path.lstrip('/')}#") 

2926 return from_parts( 

2927 scheme, 

2928 joined._netloc, 

2929 joined._path, 

2930 url._query, 

2931 url._fragment, 

2932 self._mode, 

2933 url._empty & ~EMPTY_AUTHORITY, 

2934 ) 

2935 

2936 def joinpath(self, *other: str, encoded: bool = False) -> "URL": 

2937 """Return a new URL with the elements in other appended to the path.""" 

2938 return self._make_child(other, encoded=encoded) 

2939 

2940 def human_repr(self) -> str: 

2941 """Return decoded human readable string for URL representation.""" 

2942 user = human_quote(self.user, "#/:?@[]\\") 

2943 password = human_quote(self.password, "#/:?@[]\\") 

2944 if (host := self.host) and ":" in host and host[-1] != "]": 

2945 # An IPv6 zone identifier stays percent-encoded, which is the 

2946 # only form the parser accepts. 

2947 host = f"[{self.raw_host}]" if "%" in host else f"[{host}]" 

2948 elif ( 

2949 self._scheme not in SPECIAL_SCHEMES 

2950 and host 

2951 and host is not (raw_host := self.raw_host) 

2952 ): 

2953 # A decoded opaque host that the parser would reject, such as 

2954 # "a\uff0fb", whose NFKC form has a "/", stays percent-encoded. 

2955 try: 

2956 _check_netloc(host) 

2957 except ValueError: 

2958 host = raw_host 

2959 # WHATWG mode reads "\\" in the path as "/", so it stays encoded. 

2960 # An empty path stays empty where str() keeps it so, see __str__(). 

2961 if ( 

2962 self._path 

2963 or self._scheme in SPECIAL_SCHEMES 

2964 or not self._scheme 

2965 or self._scheme.lower() in SPECIAL_SCHEMES 

2966 ): 

2967 path = human_quote(self.path, "#?\\") 

2968 else: 

2969 path = "" 

2970 if TYPE_CHECKING: 

2971 assert path is not None 

2972 if not self._scheme and not self._netloc: 

2973 path = _encode_relative_colon(path, self._mode) 

2974 query_string = "&".join( 

2975 "{}={}".format(human_quote(k, "#&+;="), human_quote(v, "#&+;=")) 

2976 for k, v in self.query.items() 

2977 ) 

2978 fragment = human_quote(self.fragment, "") 

2979 if TYPE_CHECKING: 

2980 assert fragment is not None 

2981 if host is None and self._netloc: 

2982 host = "" # an authority without a host, e.g. "user@:8080" 

2983 netloc = make_netloc(user, password, host, self.explicit_port) 

2984 if empty := self._str_empty: 

2985 return unsplit_result_empty( 

2986 self._scheme, netloc, path, query_string, fragment, empty 

2987 ) 

2988 return unsplit_result(self._scheme, netloc, path, query_string, fragment) 

2989 

2990 if HAS_PYDANTIC: 

2991 # Borrowed from https://docs.pydantic.dev/latest/concepts/types/#handling-third-party-types 

2992 @classmethod 

2993 def __get_pydantic_json_schema__( 

2994 cls, 

2995 core_schema: "CoreSchema", 

2996 handler: "GetJsonSchemaHandler", 

2997 ) -> "JsonSchemaValue": 

2998 field_schema: dict[str, Any] = {} 

2999 field_schema.update(type="string", format="uri") 

3000 return field_schema 

3001 

3002 @classmethod 

3003 def __get_pydantic_core_schema__( 

3004 cls, 

3005 source_type: type[Self] | type[str], 

3006 handler: "GetCoreSchemaHandler", 

3007 ) -> "CoreSchema": 

3008 # Lazy import: pulling in pydantic_core at module load time 

3009 # increases yarl's import cost 3-7x for users who don't use 

3010 # pydantic. Keep this import function-scoped. 

3011 from pydantic_core import core_schema # noqa: PLC0415 

3012 

3013 from_str_schema = core_schema.chain_schema( 

3014 [ 

3015 core_schema.str_schema(), 

3016 core_schema.no_info_plain_validator_function(URL), 

3017 ] 

3018 ) 

3019 

3020 return core_schema.json_or_python_schema( 

3021 json_schema=from_str_schema, 

3022 python_schema=core_schema.union_schema( 

3023 [ 

3024 # check if it's an instance first before doing any further work 

3025 core_schema.is_instance_schema(URL), 

3026 from_str_schema, 

3027 ] 

3028 ), 

3029 serialization=core_schema.plain_serializer_function_ser_schema(str), 

3030 ) 

3031 

3032 

3033_DEFAULT_IDNA_SIZE = 256 

3034_DEFAULT_ENCODE_SIZE = 512 

3035 

3036 

3037@lru_cache(_DEFAULT_IDNA_SIZE) 

3038def _idna_decode(raw: str) -> str: 

3039 try: 

3040 return idna.decode(raw.encode("ascii")) 

3041 except UnicodeError: # e.g. '::1' 

3042 if _WHATWG_ONLY_HOST_RE.search(raw): 

3043 # IDNA2008 rejects the whole host for one label with "{", see 

3044 # _idna_encode_labels(). 

3045 return ".".join(map(_idna_decode_label, raw.split("."))) 

3046 return raw.encode("ascii").decode("idna") 

3047 

3048 

3049def _idna_decode_label(label: str) -> str: 

3050 """Decode a label of a host that has '"', "`", "{" or "}". 

3051 

3052 A label with them is decoded from Punycode without the IDNA checks, as 

3053 _idna_encode_labels() encodes it; IDNA2003 would map "faß{b}" to 

3054 "fass{b}" and fail to round-trip. 

3055 """ 

3056 if not _WHATWG_ONLY_HOST_RE.search(label): 

3057 return _idna_decode(label) 

3058 if label[:4] == "xn--": 

3059 try: 

3060 return label[4:].encode("ascii").decode("punycode") 

3061 except UnicodeError: 

3062 pass 

3063 return label 

3064 

3065 

3066@lru_cache(_DEFAULT_IDNA_SIZE) 

3067def _idna_encode(host: str) -> tuple[str, bool]: 

3068 """Encode a host with IDNA2008 and UTS #46, or else with IDNA2003. 

3069 

3070 The flag tells if the IDNA2003 fallback was needed, which RFC 3986 mode 

3071 rejects. 

3072 """ 

3073 try: 

3074 return idna.encode(host, uts46=True).decode("ascii"), False 

3075 except UnicodeError: 

3076 try: 

3077 mapped = idna.uts46_remap(host, std3_rules=False, transitional=False) 

3078 except UnicodeError: 

3079 mapped = "" 

3080 if _WHATWG_ONLY_HOST_RE.search(mapped): 

3081 return _idna_encode_labels(mapped), True 

3082 return host.encode("idna").decode("ascii"), True 

3083 

3084 

3085def _idna_encode_labels(mapped: str) -> str: 

3086 """Encode a host mapped with UTS #46 that has '"', "`", "{" or "}". 

3087 

3088 IDNA2008 rejects a label with them, which UTS #46 accepts without 

3089 UseSTD3ASCIIRules, as WHATWG uses it. Such a label is encoded with 

3090 Punycode, the other labels one by one; IDNA2003, the fallback for the 

3091 whole host, would turn "faß" into "fass" where WHATWG has "xn--fa-hia". 

3092 """ 

3093 return ".".join( 

3094 label 

3095 if label.isascii() 

3096 else _idna_encode(label)[0] 

3097 if not _WHATWG_ONLY_HOST_RE.search(label) 

3098 else _punycode_label(label) 

3099 for label in mapped.split(".") 

3100 ) 

3101 

3102 

3103def _punycode_label(label: str) -> str: 

3104 """Encode a label with Punycode after the checks UTS #46 makes. 

3105 

3106 The label must not start with a combining mark and must follow the 

3107 bidi rule of RFC 5893, as WHATWG has CheckBidi set: "x{א}" fails. 

3108 """ 

3109 idna.check_initial_combiner(label) 

3110 idna.check_bidi(label) 

3111 return "xn--" + label.encode("punycode").decode("ascii") 

3112 

3113 

3114_HEX_DIGITS = frozenset("0123456789abcdefABCDEF") 

3115# A host that ends in a number ends in one of these; most hosts do not. 

3116_NUMERIC_HOST_TAIL = frozenset("0123456789abcdefxABCDEFX.") 

3117 

3118 

3119def _moved_url(url: "URL", scheme: str, mode: Mode) -> "URL": 

3120 """Return url moved under scheme and mode, see _moved_parts().""" 

3121 netloc, path, empty = _moved_parts(url, scheme, mode, url._empty) 

3122 if path and not url._path and not (url._netloc or url._empty & EMPTY_AUTHORITY): 

3123 # "file:" and "file:?q" became "file:///" and "file:///?q" in WHATWG 

3124 # mode, but as a reference they keep the path of a file base, as 

3125 # the parsed ones do, see _encode_url(). The cache of a from_parts() 

3126 # URL is shared, so this one gets its own. 

3127 moved = from_parts_uncached( 

3128 scheme, netloc, path, url._query, url._fragment, mode, empty 

3129 ) 

3130 moved._cache["_join_path"] = "" 

3131 return moved 

3132 return from_parts(scheme, netloc, path, url._query, url._fragment, mode, empty) 

3133 

3134 

3135def _moved_parts( 

3136 url: "URL", scheme: str, mode: Mode, empty: int 

3137) -> tuple[str, str, int]: 

3138 """Return the netloc, path and empty mask of url moved under scheme and mode. 

3139 

3140 with_scheme(), join() and a change of mode can move a host that was 

3141 parsed under another scheme or mode into a special-scheme WHATWG URL, 

3142 where the host is percent-decoded and lowercased and one that ends in a 

3143 number is parsed as an IPv4 address. The opaque host of a non-special 

3144 WHATWG URL is kept under another non-special scheme, and validated as a 

3145 domain under a special one. An RFC 3986 mode URL moved into WHATWG mode 

3146 also loses an empty password and an empty userinfo, see 

3147 _drop_empty_password(). A file URL also loses "localhost" and must not 

3148 have userinfo or a port, see _check_whatwg_file_authority(), and reads a 

3149 drive letter in the authority as the start of the path. 

3150 """ 

3151 netloc = url._netloc 

3152 if mode is not _WHATWG: 

3153 return netloc, url._path, empty 

3154 if not netloc: 

3155 if not url._path and scheme == "file": 

3156 # A file URL has an authority and a path, "/" at least. 

3157 return "", "/", empty | EMPTY_AUTHORITY 

3158 return netloc, url._path, empty 

3159 host = url.raw_host 

3160 if (scheme in SPECIAL_SCHEMES or "[" in netloc) and host: 

3161 # The WHATWG host is cached with the encoding of the host. An opaque 

3162 # host was not checked as a domain, see _encode_opaque_host(). 

3163 encoded, whatwg_host, _ = _encode_host( 

3164 host, 

3165 url._mode is _WHATWG 

3166 and url._scheme not in SPECIAL_SCHEMES 

3167 and _is_opaque_host(url._scheme, host), 

3168 ) 

3169 if whatwg_host is not encoded: 

3170 host = _whatwg_special_host(encoded, whatwg_host) 

3171 netloc = make_netloc( 

3172 url.raw_user, url.raw_password, host, url.explicit_port 

3173 ) 

3174 elif encoded != host and "[" not in netloc: 

3175 # An opaque host is not lowercased, see _encode_opaque_host(). 

3176 host = encoded 

3177 netloc = make_netloc( 

3178 url.raw_user, url.raw_password, host, url.explicit_port 

3179 ) 

3180 if url._mode is not _WHATWG and "@" in netloc: 

3181 # The userinfo of RFC 3986 mode can have ";" and "=", which WHATWG 

3182 # mode encodes, see _whatwg_requote_userinfo(). 

3183 userinfo, _, hostport = netloc.rpartition("@") 

3184 user, colon, password = userinfo.partition(":") 

3185 user = _whatwg_requote_userinfo(user) 

3186 password = _whatwg_requote_userinfo(password) 

3187 netloc = f"{user}{colon}{password}@{hostport}" 

3188 if ":@" in netloc or netloc[0] == "@": 

3189 netloc = _drop_empty_password(netloc) 

3190 if scheme == "file": 

3191 # The netloc as written: the host above is lowercased. 

3192 if is_drive_letter(url._netloc): 

3193 path = _whatwg_file_drive(url._netloc, url._path) 

3194 if "." in path: 

3195 path = normalize_file_path(path) 

3196 return "", path, empty | EMPTY_AUTHORITY 

3197 _check_whatwg_file_authority(netloc) 

3198 if host == "localhost": 

3199 return "", url._path or "/", empty | EMPTY_AUTHORITY 

3200 return netloc, url._path or "/", empty 

3201 return netloc, url._path, empty 

3202 

3203 

3204def _drop_empty_password(netloc: str) -> str: 

3205 """Drop an empty password from an authority, as WHATWG writes it. 

3206 

3207 WHATWG writes ":" only before a password that is not empty, and "@" only 

3208 after a username or a password that is not empty: "a:@h" is "a@h" and 

3209 ":@h" and "@h" are "h". 

3210 """ 

3211 userinfo, _, hostinfo = netloc.rpartition("@") 

3212 user, _, password = userinfo.partition(":") 

3213 if password: 

3214 return netloc 

3215 return f"{user}@{hostinfo}" if user else hostinfo 

3216 

3217 

3218def _whatwg_special_host(host: str, whatwg_host: str | None) -> str: 

3219 """Return the host WHATWG parses a special-scheme host as. 

3220 

3221 The WHATWG URL Standard percent-decodes the host and parses a host whose 

3222 last label is a number, like "0x7f.1" or "example.123", as an IPv4 

3223 address. It fails when the decoded host is not a valid domain, a label 

3224 is not a number or a number is out of range. RFC 3986 mode accepts all 

3225 of them as a reg-name. 

3226 """ 

3227 if whatwg_host is None: 

3228 if host[0] == "[": 

3229 raise ValueError( 

3230 f"Host {host!r} is an IPvFuture address, which the WHATWG URL" 

3231 " Standard does not have" 

3232 ) 

3233 if "%" in host: 

3234 raise ValueError(f"Host {host!r} is not a valid host once percent-decoded") 

3235 raise ValueError(f"Host {host!r} ends in a number but is not an IPv4 address") 

3236 return whatwg_host 

3237 

3238 

3239# The forbidden host code points of the WHATWG URL Standard, which the 

3240# opaque-host parser rejects. 

3241_FORBIDDEN_HOST_RE = re.compile(r"[\x00\t\n\r #/:<>?@\[\\\]^|]") 

3242# The C0 control percent-encode set, which the opaque-host parser encodes. 

3243_C0_CONTROL_PERCENT_ENCODE_RE = re.compile(r"[^\x20-\x7e]+") 

3244# The percent-encoded octets of a non-ASCII host, see _decode_opaque_host(). 

3245_NON_ASCII_ESCAPES_RE = re.compile(r"(?:%[89A-Fa-f][0-9A-Fa-f])+") 

3246 

3247 

3248def _is_opaque_host(scheme: str, host: str) -> bool: 

3249 """Tell if the WHATWG mode host of a URL whose scheme is not special is 

3250 an opaque host. 

3251 

3252 The WHATWG URL Standard parses the host of a non-special URL as an opaque 

3253 host, unless it is an IPv6 address in brackets. A relative reference has 

3254 no scheme yet, its host is encoded as a domain. The callers check that 

3255 the scheme is not special first, which keeps the call off the hot path; 

3256 URL.build() keeps the scheme as given, so "HTTP" is checked here. 

3257 """ 

3258 return ( 

3259 scheme != "" 

3260 and ":" not in host 

3261 and host[:1] != "[" 

3262 and scheme.lower() not in SPECIAL_SCHEMES 

3263 ) 

3264 

3265 

3266def _encode_opaque_host(host: str) -> str: 

3267 """Encode a host with the WHATWG opaque-host parser. 

3268 

3269 The host is neither lowercased nor encoded with IDNA: it is rejected if 

3270 it has a forbidden host code point and its C0 controls and non-ASCII 

3271 code points are percent-encoded, so "sc://Ñ" is "sc://%C3%91". A "%" 

3272 that does not start a percent-encoded octet is kept. RFC 3986 mode and 

3273 special schemes encode a domain instead, see _encode_host(). 

3274 """ 

3275 if invalid := _FORBIDDEN_HOST_RE.search(host): 

3276 raise ValueError( 

3277 f"Host {host!r} cannot contain {invalid.group()!r}" 

3278 f" (at position {invalid.start()})" 

3279 ) 

3280 if host.isascii() and host.isprintable(): 

3281 return host 

3282 return _C0_CONTROL_PERCENT_ENCODE_RE.sub( 

3283 lambda match: quote(match.group(), safe=""), host 

3284 ) 

3285 

3286 

3287def _decode_opaque_host(raw: str) -> str: 

3288 """Decode the non-ASCII code points of an opaque host. 

3289 

3290 The percent-encoded ASCII characters stay encoded, as does an octet that 

3291 is not UTF-8, so that passing the result to with_host() gives the same 

3292 host back. 

3293 """ 

3294 if "%" not in raw: 

3295 return raw 

3296 return _NON_ASCII_ESCAPES_RE.sub(lambda match: UNQUOTER(match.group()), raw) 

3297 

3298 

3299# A number with more significant digits than this in its base is at least 

3300# 2**32, out of range for any part of an IPv4 address. Checking the length 

3301# first keeps int() away from huge labels, which Python refuses to convert. 

3302_IPV4_MAX_DIGITS = {16: 8, 8: 11, 10: 10} 

3303_IPV4_OUT_OF_RANGE = 2**32 

3304 

3305 

3306def _ipv4_number(label: str) -> int | None: 

3307 """Parse a WHATWG IPv4 number: decimal, 0x hex or 0 octal.""" 

3308 if label[:2] in ("0x", "0X"): 

3309 digits, base = label[2:], 16 

3310 if not all(c in _HEX_DIGITS for c in digits): 

3311 return None 

3312 elif label == "" or not label.isascii() or not label.isdigit(): 

3313 return None 

3314 elif label[0] == "0" and len(label) > 1: 

3315 digits, base = label[1:], 8 

3316 if "8" in digits or "9" in digits: 

3317 return None 

3318 else: 

3319 digits, base = label, 10 

3320 digits = digits.lstrip("0") 

3321 if len(digits) > _IPV4_MAX_DIGITS[base]: 

3322 return _IPV4_OUT_OF_RANGE 

3323 return int(digits, base) if digits else 0 

3324 

3325 

3326def _whatwg_numeric_host(host: str) -> str | None: 

3327 """Return host as WHATWG sees it for a special scheme. 

3328 

3329 A host that does not end in a number is returned as is. One that does 

3330 is run through the WHATWG IPv4 parser: the result is the dotted-quad 

3331 serialization, or None when parsing fails. The host must not be 

3332 percent-encoded; see _whatwg_decoded_host. 

3333 """ 

3334 if host[-1] not in _NUMERIC_HOST_TAIL: 

3335 return host 

3336 labels = host.split(".") 

3337 if labels[-1] == "" and len(labels) > 1: 

3338 labels.pop() 

3339 last = labels[-1] 

3340 if not (last.isascii() and last.isdigit()) and _ipv4_number(last) is None: 

3341 return host 

3342 if len(labels) > 4: 

3343 return None 

3344 numbers = [_ipv4_number(label) for label in labels] 

3345 *head, tail = numbers 

3346 if tail is None or tail >= 256 ** (5 - len(numbers)): 

3347 return None 

3348 address = tail 

3349 for i, number in enumerate(head): 

3350 if number is None or number > 255: 

3351 return None 

3352 address += number << (8 * (3 - i)) 

3353 return ".".join(str(address >> shift & 255) for shift in (24, 16, 8, 0)) 

3354 

3355 

3356def _whatwg_decoded_host(host: str) -> str | None: 

3357 """Return a percent-encoded host as WHATWG sees it for a special scheme. 

3358 

3359 The WHATWG host parser percent-decodes the host, decodes the bytes as 

3360 UTF-8 with U+FFFD for invalid sequences and runs domain to ASCII, so 

3361 "%e2%98%83" is "xn--n3h" and "ho%00st" fails. The result goes on to 

3362 the IPv4 parser like any other host. None means WHATWG fails, or gives 

3363 a host that yarl rejects anyway, like one with a default-ignorable code 

3364 point. 

3365 """ 

3366 decoded = unquote_to_bytes(host).decode("utf-8", "replace") 

3367 if decoded.isascii(): 

3368 decoded = decoded.lower() 

3369 elif _DEFAULT_IGNORABLE_RE.search(decoded): 

3370 return None 

3371 else: 

3372 try: 

3373 decoded = _idna_encode(decoded)[0] 

3374 except UnicodeError: 

3375 return None 

3376 if "%" in decoded or _NOT_WHATWG_DOMAIN_RE.search(decoded): 

3377 return None 

3378 return _whatwg_numeric_host(decoded) 

3379 

3380 

3381@lru_cache(_DEFAULT_ENCODE_SIZE) 

3382def _encode_host( 

3383 host: str, validate_host: bool, *, reject_empty_zone: bool = True 

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

3385 """Encode host part of URL. 

3386 

3387 Next to the encoded host, return the host WHATWG sees for a special 

3388 scheme, which is the encoded host itself unless the host is 

3389 percent-encoded (see _whatwg_decoded_host), ends in a number (see 

3390 _whatwg_numeric_host) or is an IPv6 address not written compressed, and 

3391 a flag that tells if RFC 3986 mode rejects the host: IDNA2008 could not 

3392 encode it, or it has a character that a WHATWG domain can have and an 

3393 RFC 3986 reg-name cannot, like "{". Both are computed here so that the 

3394 checks are cached with the encoding. 

3395 """ 

3396 # If the host ends with a digit or contains a colon, its likely 

3397 # an IP address. 

3398 if host and (host[-1].isdigit() or ":" in host): 

3399 # RFC 6874 spells the IPv6 zone separator as the percent-encoded 

3400 # ``%25``; the builders also take a bare ``%`` followed by the 

3401 # decoded zone (e.g. ``with_host("fe80::1%1")``). 

3402 part = "%25" if host.startswith("%25", host.find("%")) else "%" 

3403 raw_ip, sep, zone = host.partition(part) 

3404 # If it looks like an IP, we check with _ip_compressed_version 

3405 # and fall-through if its not an IP address. This is a performance 

3406 # optimization to avoid parsing IP addresses as much as possible 

3407 # because it is orders of magnitude slower than almost any other 

3408 # operation this library does. 

3409 # Might be an IP address, check it 

3410 # 

3411 # IP Addresses can look like: 

3412 # https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2 

3413 # - 127.0.0.1 (last character is a digit) 

3414 # - 2001:db8::ff00:42:8329 (contains a colon) 

3415 # - 2001:db8::ff00:42:8329%eth0 (contains a colon) 

3416 # - [2001:db8::ff00:42:8329] (contains a colon -- brackets should 

3417 # have been removed before it gets here) 

3418 # Rare IP Address formats are not supported per: 

3419 # https://datatracker.ietf.org/doc/html/rfc3986#section-7.4 

3420 # 

3421 # IP parsing is slow, so its wrapped in an LRU 

3422 try: 

3423 ip = ip_address(raw_ip) 

3424 except ValueError: 

3425 pass 

3426 else: 

3427 if sep and validate_host: 

3428 if ip.version == 6: 

3429 if sep == "%" and not _ZONE_ID_UNSAFE_RE.search(zone): 

3430 # A zone identifier given after a bare "%" is 

3431 # decoded, as host returns it; RFC 6874 writes it 

3432 # as "%25" followed by the percent-encoded zone. 

3433 # Percent-encoded octets are kept as they are. 

3434 zone = _ZONE_ID_REQUOTE_RE.sub(_quote_match, zone) 

3435 sep = "%25" 

3436 bad_zone = ZONE_ID_RE.fullmatch(zone) is None 

3437 else: 

3438 bad_zone = _ZONE_ID_UNSAFE_RE.search(zone) is not None 

3439 if bad_zone or (reject_empty_zone and not zone): 

3440 raise ValueError("Invalid characters in zone identifier") 

3441 # These checks should not happen in the 

3442 # LRU to keep the cache size small 

3443 host = ip.compressed 

3444 if ip.version == 6: 

3445 # RFC 3986 keeps an IPv6 address as written, lowercased per 

3446 # section 6.2.2.1; WHATWG serializes it compressed, which is 

3447 # the WHATWG host here, see _whatwg_special_host(). 

3448 host = f"[{host}{sep}{zone}]" if sep else f"[{host}]" 

3449 if (written := raw_ip.lower()) != ip.compressed: 

3450 return f"[{written}{sep}{zone}]", host, False 

3451 elif sep: 

3452 # WHATWG has no zone identifiers: "%" after an IPv4 address 

3453 # is percent-encoding in the host. 

3454 host = f"{host}{sep}{zone}" 

3455 return host, _whatwg_decoded_host(host), False 

3456 return host, host, False 

3457 

3458 # IDNA encoding is slow, skip it for ASCII-only strings 

3459 if host.isascii(): 

3460 # Check for invalid characters explicitly; _idna_encode() does this 

3461 # for non-ascii host names. 

3462 host = host.lower() 

3463 if host[:2] == "[v": 

3464 # Only an IPvFuture address keeps its brackets up to here, see 

3465 # split_netloc(). WHATWG has no IPvFuture, hence None. 

3466 end = len(host) - 1 

3467 if host[end] != "]" or IP_FUTURE_RE.fullmatch(host, 1, end) is None: 

3468 raise ValueError(f"IPvFuture address is invalid: {host!r}") 

3469 return host, None, False 

3470 not_rfc = False 

3471 if validate_host and (invalid := NOT_REG_NAME.search(host)): 

3472 if invalid := _NOT_WHATWG_DOMAIN_RE.search(host, invalid.start()): 

3473 value, pos, extra = invalid.group(), invalid.start(), "" 

3474 if value == "@" or (value == ":" and "@" in host[pos:]): 

3475 # this looks like an authority string 

3476 extra = ( 

3477 ", if the value includes a username or password, " 

3478 "use 'authority' instead of 'host'" 

3479 ) 

3480 raise ValueError( 

3481 f"Host {host!r} cannot contain {value!r} (at position {pos}){extra}" 

3482 ) from None 

3483 # The WHATWG URL Standard keeps '"', "`", "{" and "}" in a 

3484 # domain, RFC 3986 rejects them, see _rfc_host_error(). 

3485 not_rfc = True 

3486 if "%" in host: 

3487 return host, _whatwg_decoded_host(host), not_rfc 

3488 return host, host and _whatwg_numeric_host(host), not_rfc 

3489 

3490 # IDNA/UTS-46 mapping silently deletes default-ignorable code points, which 

3491 # would turn e.g. ``e<ZWSP>vil.com`` into ``evil.com``, a different host 

3492 # than the string the caller supplied. Reject them on every path (this runs 

3493 # regardless of ``validate_host`` since the plain ``URL(str)`` constructor 

3494 # encodes with ``validate_host=False``) so the parsed host cannot diverge 

3495 # from the input, matching idna/httpx/urllib3. 

3496 if invalid := _DEFAULT_IGNORABLE_RE.search(host): 

3497 raise ValueError( 

3498 f"Host {host!r} cannot contain {invalid.group()!r} " 

3499 f"(at position {invalid.start()})" 

3500 ) from None 

3501 encoded, not_rfc = _idna_encode(host) 

3502 # IDNA uses NFKC equivalence, so normalization can expand a non-ascii 

3503 # character into an ASCII delimiter (e.g. the fullwidth solidus U+FF0F 

3504 # becomes '/'). split_url's _check_netloc screens '/?#@:%' but not '[', 

3505 # ']' or '\\', so a host like ``exa[mple`` (fullwidth '[') slips 

3506 # through the parser and _idna_encode turns it into ``exa[mple``, a netloc 

3507 # that str(url) then renders but yarl itself rejects on re-parse. Run the 

3508 # check on every path (like the default-ignorable check above, which also 

3509 # ignores validate_host) so the parsed host cannot diverge from what the 

3510 # serialized URL means. 

3511 if invalid := NOT_REG_NAME.search(encoded): 

3512 if invalid := _NOT_WHATWG_DOMAIN_RE.search(encoded, invalid.start()): 

3513 raise ValueError( 

3514 f"Host {host!r} cannot contain {invalid.group()!r} " 

3515 f"after IDNA normalization to {encoded!r}" 

3516 ) from None 

3517 not_rfc = True 

3518 # The host can be percent-encoded next to the non-ASCII code points, or 

3519 # IDNA can map a code point to "%", like the fullwidth percent sign 

3520 # U+FF05, which WHATWG then rejects. 

3521 if "%" in encoded: 

3522 return encoded, _whatwg_decoded_host(host), not_rfc 

3523 return encoded, _whatwg_numeric_host(encoded), not_rfc 

3524 

3525 

3526@rewrite_module 

3527def cache_clear() -> None: 

3528 """Clear all LRU caches.""" 

3529 _idna_encode.cache_clear() 

3530 _idna_decode.cache_clear() 

3531 _encode_host.cache_clear() 

3532 

3533 

3534@rewrite_module 

3535def cache_info() -> CacheInfo: 

3536 """Report cache statistics.""" 

3537 return { 

3538 "idna_encode": _idna_encode.cache_info(), 

3539 "idna_decode": _idna_decode.cache_info(), 

3540 "ip_address": _encode_host.cache_info(), 

3541 "host_validate": _encode_host.cache_info(), 

3542 "encode_host": _encode_host.cache_info(), 

3543 } 

3544 

3545 

3546@rewrite_module 

3547def cache_configure( 

3548 *, 

3549 idna_encode_size: int | None = _DEFAULT_IDNA_SIZE, 

3550 idna_decode_size: int | None = _DEFAULT_IDNA_SIZE, 

3551 ip_address_size: int | None | UndefinedType = UNDEFINED, 

3552 host_validate_size: int | None | UndefinedType = UNDEFINED, 

3553 encode_host_size: int | None | UndefinedType = UNDEFINED, 

3554) -> None: 

3555 """Configure LRU cache sizes.""" 

3556 global _idna_decode, _idna_encode, _encode_host 

3557 # ip_address_size, host_validate_size are no longer 

3558 # used, but are kept for backwards compatibility. 

3559 if ip_address_size is not UNDEFINED or host_validate_size is not UNDEFINED: 

3560 warnings.warn( 

3561 "cache_configure() no longer accepts the " 

3562 "ip_address_size or host_validate_size arguments, " 

3563 "they are used to set the encode_host_size instead " 

3564 "and will be removed in the future", 

3565 DeprecationWarning, 

3566 stacklevel=2, 

3567 ) 

3568 

3569 if encode_host_size is not None: 

3570 for size in (ip_address_size, host_validate_size): 

3571 if size is None: 

3572 encode_host_size = None 

3573 elif encode_host_size is UNDEFINED: 

3574 if size is not UNDEFINED: 

3575 encode_host_size = size 

3576 elif size is not UNDEFINED: 

3577 if TYPE_CHECKING: 

3578 assert isinstance(size, int) 

3579 assert isinstance(encode_host_size, int) 

3580 encode_host_size = max(size, encode_host_size) 

3581 if encode_host_size is UNDEFINED: 

3582 encode_host_size = _DEFAULT_ENCODE_SIZE 

3583 

3584 _encode_host = lru_cache(encode_host_size)(_encode_host.__wrapped__) 

3585 _idna_decode = lru_cache(idna_decode_size)(_idna_decode.__wrapped__) 

3586 _idna_encode = lru_cache(idna_encode_size)(_idna_encode.__wrapped__)