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

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

819 statements  

1import re 

2import sys 

3import warnings 

4from collections.abc import Mapping, Sequence 

5from enum import Enum 

6from functools import _CacheInfo, lru_cache 

7from importlib.util import find_spec 

8from ipaddress import ip_address 

9from typing import ( 

10 TYPE_CHECKING, 

11 Any, 

12 NoReturn, 

13 TypedDict, 

14 TypeVar, 

15 Union, 

16 cast, 

17 overload, 

18) 

19from urllib.parse import SplitResult, scheme_chars, uses_relative 

20 

21import idna 

22from multidict import MultiDict, MultiDictProxy, istr 

23from propcache.api import under_cached_property as cached_property 

24 

25from ._parse import ( 

26 USES_AUTHORITY, 

27 SplitURLType, 

28 make_netloc, 

29 query_to_pairs, 

30 split_netloc, 

31 split_url, 

32 unsplit_result, 

33) 

34from ._path import normalize_path, normalize_path_segments 

35from ._query import ( 

36 Query, 

37 QueryVariable, 

38 SimpleQuery, 

39 get_str_query, 

40 get_str_query_from_iterable, 

41 get_str_query_from_sequence_iterable, 

42) 

43from ._quoters import ( 

44 FRAGMENT_QUOTER, 

45 FRAGMENT_REQUOTER, 

46 PATH_QUOTER, 

47 PATH_REQUOTER, 

48 PATH_SAFE_UNQUOTER, 

49 PATH_UNQUOTER, 

50 QS_UNQUOTER, 

51 QUERY_QUOTER, 

52 QUERY_REQUOTER, 

53 QUOTER, 

54 REQUOTER, 

55 UNQUOTER, 

56 human_quote, 

57) 

58 

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

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

61if TYPE_CHECKING: 

62 from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler 

63 from pydantic.json_schema import JsonSchemaValue 

64 from pydantic_core import CoreSchema 

65 

66 

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

68USES_RELATIVE = frozenset(uses_relative) 

69_SCHEME_CHARS = frozenset(scheme_chars) 

70 

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

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

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

74 

75 

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

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

78# on lower-cased ASCII values. 

79NOT_REG_NAME = re.compile( 

80 r""" 

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

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

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

84 | 

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

86 # sequence of 2 hex digits. 

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

88 """, 

89 re.VERBOSE, 

90) 

91 

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

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

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

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

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

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

98# matching the two encoders _idna_encode dispatches to: 

99# 

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

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

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

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

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

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

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

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

108# 

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

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

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

112# silently deletes is not matched here. 

113_DEFAULT_IGNORABLE_RE = re.compile( 

114 "[" 

115 "\u00ad" # SOFT HYPHEN 

116 "\u034f" # COMBINING GRAPHEME JOINER 

117 "\u061c" # ARABIC LETTER MARK 

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

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

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

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

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

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

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

125 "\u3164" # HANGUL FILLER 

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

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

128 "\uffa0" # HALFWIDTH HANGUL FILLER 

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

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

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

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

133 "]" 

134) 

135 

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

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

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

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

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

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

142 

143_T = TypeVar("_T") 

144 

145if sys.version_info >= (3, 11): 

146 from typing import Self 

147else: 

148 Self = Any 

149 

150 

151class UndefinedType(Enum): 

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

153 

154 _singleton = 0 

155 

156 

157UNDEFINED = UndefinedType._singleton 

158 

159 

160class CacheInfo(TypedDict): 

161 """Host encoding cache.""" 

162 

163 idna_encode: _CacheInfo 

164 idna_decode: _CacheInfo 

165 ip_address: _CacheInfo 

166 host_validate: _CacheInfo 

167 encode_host: _CacheInfo 

168 

169 

170class _InternalURLCache(TypedDict, total=False): 

171 _val: SplitURLType 

172 _origin: "URL" 

173 absolute: bool 

174 hash: int 

175 scheme: str 

176 raw_authority: str 

177 authority: str 

178 raw_user: str | None 

179 user: str | None 

180 raw_password: str | None 

181 password: str | None 

182 raw_host: str | None 

183 host: str | None 

184 host_subcomponent: str | None 

185 host_port_subcomponent: str | None 

186 port: int | None 

187 explicit_port: int | None 

188 raw_path: str 

189 path: str 

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

191 query: "MultiDictProxy[str]" 

192 raw_query_string: str 

193 query_string: str 

194 path_qs: str 

195 raw_path_qs: str 

196 raw_fragment: str 

197 fragment: str 

198 raw_parts: tuple[str, ...] 

199 parts: tuple[str, ...] 

200 parent: "URL" 

201 raw_name: str 

202 name: str 

203 raw_suffix: str 

204 suffix: str 

205 raw_suffixes: tuple[str, ...] 

206 suffixes: tuple[str, ...] 

207 

208 

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

210 obj.__module__ = "yarl" 

211 return obj 

212 

213 

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

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

216 colon_pos = path.find(":") 

217 if colon_pos <= 0: 

218 return path 

219 for c in path[:colon_pos]: 

220 if c not in _SCHEME_CHARS: 

221 return path 

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

223 

224 

225@lru_cache 

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

227 """Parse unencoded URL.""" 

228 cache: _InternalURLCache = {} 

229 host: str | None 

230 scheme, netloc, path, query, fragment = split_url(url_str) 

231 if not netloc: # netloc 

232 host = "" 

233 else: 

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

235 # Complex netloc 

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

237 else: 

238 username = password = port = None 

239 host = netloc 

240 if host is None: 

241 if scheme in SCHEME_REQUIRES_HOST: 

242 msg = ( 

243 "Invalid URL: host is required for " 

244 f"absolute urls with the {scheme} scheme" 

245 ) 

246 raise ValueError(msg) 

247 else: 

248 host = "" 

249 host = _encode_host(host, validate_host=False) 

250 # Remove brackets as host encoder adds back brackets for IPv6 addresses 

251 cache["raw_host"] = host[1:-1] if "[" in host else host 

252 cache["explicit_port"] = port 

253 if password is None and username is None: 

254 # Fast path for URLs without user, password 

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

256 cache["raw_user"] = None 

257 cache["raw_password"] = None 

258 else: 

259 raw_user = REQUOTER(username) if username else username 

260 raw_password = REQUOTER(password) if password else password 

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

262 cache["raw_user"] = raw_user 

263 cache["raw_password"] = raw_password 

264 

265 if path: 

266 path = PATH_REQUOTER(path) 

267 if netloc and "." in path: 

268 path = normalize_path(path) 

269 elif not scheme and not netloc: 

270 path = _encode_relative_scheme_colon(path) 

271 if query: 

272 query = QUERY_REQUOTER(query) 

273 if fragment: 

274 fragment = FRAGMENT_REQUOTER(fragment) 

275 

276 cache["scheme"] = scheme 

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

278 cache["raw_query_string"] = query 

279 cache["raw_fragment"] = fragment 

280 

281 self = object.__new__(URL) 

282 self._scheme = scheme 

283 self._netloc = netloc 

284 self._path = path 

285 self._query = query 

286 self._fragment = fragment 

287 self._cache = cache 

288 return self 

289 

290 

291@lru_cache 

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

293 """Parse pre-encoded URL.""" 

294 self = object.__new__(URL) 

295 val = split_url(url_str) 

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

297 self._cache = {} 

298 return self 

299 

300 

301@lru_cache 

302def build_pre_encoded_url( 

303 scheme: str, 

304 authority: str, 

305 user: str | None, 

306 password: str | None, 

307 host: str, 

308 port: int | None, 

309 path: str, 

310 query_string: str, 

311 fragment: str, 

312) -> "URL": 

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

314 self = object.__new__(URL) 

315 self._scheme = scheme 

316 if authority: 

317 self._netloc = authority 

318 elif host: 

319 if port is not None: 

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

321 if user is None and password is None: 

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

323 else: 

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

325 else: 

326 self._netloc = "" 

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

328 path = _encode_relative_scheme_colon(path) 

329 self._path = path 

330 self._query = query_string 

331 self._fragment = fragment 

332 self._cache = {} 

333 return self 

334 

335 

336def from_parts_uncached( 

337 scheme: str, netloc: str, path: str, query: str, fragment: str 

338) -> "URL": 

339 """Create a new URL from parts.""" 

340 self = object.__new__(URL) 

341 self._scheme = scheme 

342 self._netloc = netloc 

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

344 path = _encode_relative_scheme_colon(path) 

345 self._path = path 

346 self._query = query 

347 self._fragment = fragment 

348 self._cache = {} 

349 return self 

350 

351 

352from_parts = lru_cache(from_parts_uncached) 

353 

354 

355@rewrite_module 

356class URL: 

357 # Don't derive from str 

358 # follow pathlib.Path design 

359 # probably URL will not suffer from pathlib problems: 

360 # it's intended for libraries like aiohttp, 

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

362 

363 # URL grammar (RFC 3986) 

364 # pct-encoded = "%" HEXDIG HEXDIG 

365 # reserved = gen-delims / sub-delims 

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

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

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

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

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

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

372 # / path-absolute 

373 # / path-rootless 

374 # / path-empty 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

391 # ; least-significant 32 bits of address 

392 # h16 = 1*4HEXDIG 

393 # ; 16 bits of address represented in hexadecimal 

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

395 # dec-octet = DIGIT ; 0-9 

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

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

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

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

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

401 # port = *DIGIT 

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

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

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

405 # / path-rootless ; begins with a segment 

406 # / path-empty ; zero characters 

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

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

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

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

411 # path-empty = 0<pchar> 

412 # segment = *pchar 

413 # segment-nz = 1*pchar 

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

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

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

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

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

419 # URI-reference = URI / relative-ref 

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

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

422 # / path-absolute 

423 # / path-noscheme 

424 # / path-empty 

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

426 __slots__ = ("_cache", "_scheme", "_netloc", "_path", "_query", "_fragment") 

427 

428 _cache: _InternalURLCache 

429 _scheme: str 

430 _netloc: str 

431 _path: str 

432 _query: str 

433 _fragment: str 

434 

435 def __new__( 

436 cls, 

437 val: Union[str, SplitResult, "URL", UndefinedType] = UNDEFINED, 

438 *, 

439 encoded: bool = False, 

440 strict: bool | None = None, 

441 ) -> "URL": 

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

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

444 if type(val) is str: 

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

446 if type(val) is cls: 

447 return val 

448 if type(val) is SplitResult: 

449 if not encoded: 

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

451 return from_parts(*val) 

452 if isinstance(val, str): 

453 return pre_encoded_url(str(val)) if encoded else encode_url(str(val)) 

454 if val is UNDEFINED: 

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

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

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

458 self = object.__new__(URL) 

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

460 self._cache = {} 

461 return self 

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

463 

464 @classmethod 

465 def build( 

466 cls, 

467 *, 

468 scheme: str = "", 

469 authority: str = "", 

470 user: str | None = None, 

471 password: str | None = None, 

472 host: str = "", 

473 port: int | None = None, 

474 path: str = "", 

475 query: Query | None = None, 

476 query_string: str = "", 

477 fragment: str = "", 

478 encoded: bool = False, 

479 ) -> "URL": 

480 """Creates and returns a new URL""" 

481 

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

483 raise ValueError( 

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

485 ) 

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

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

488 if port and not host: 

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

490 if query and query_string: 

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

492 if ( 

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

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

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

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

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

498 or fragment is None 

499 ): 

500 raise TypeError( 

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

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

503 ) 

504 

505 if query: 

506 query_string = get_str_query(query) or "" 

507 

508 if encoded: 

509 return build_pre_encoded_url( 

510 scheme, 

511 authority, 

512 user, 

513 password, 

514 host, 

515 port, 

516 path, 

517 query_string, 

518 fragment, 

519 ) 

520 

521 self = object.__new__(URL) 

522 self._scheme = scheme 

523 _host: str | None = None 

524 if authority: 

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

526 _host = _encode_host(_host, validate_host=False) if _host else "" 

527 elif host: 

528 _host = _encode_host(host, validate_host=True) 

529 else: 

530 self._netloc = "" 

531 

532 if _host is not None: 

533 if port is not None: 

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

535 if user is None and password is None: 

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

537 else: 

538 self._netloc = make_netloc(user, password, _host, port, True) 

539 

540 path = PATH_QUOTER(path) if path else path 

541 if path and self._netloc: 

542 if "." in path: 

543 path = normalize_path(path) 

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

545 msg = ( 

546 "Path in a URL with authority should " 

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

548 ) 

549 raise ValueError(msg) 

550 

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

552 path = _encode_relative_scheme_colon(path) 

553 self._path = path 

554 if not query and query_string: 

555 query_string = QUERY_QUOTER(query_string) 

556 self._query = query_string 

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

558 self._cache = {} 

559 return self 

560 

561 def __init_subclass__(cls) -> NoReturn: 

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

563 

564 def __str__(self) -> str: 

565 if not self._path and self._netloc and (self._query or self._fragment): 

566 path = "/" 

567 else: 

568 path = self._path 

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

570 self._scheme 

571 ): 

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

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

574 host = self.host_subcomponent 

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

576 else: 

577 netloc = self._netloc 

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

579 

580 def __repr__(self) -> str: 

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

582 

583 def __bytes__(self) -> bytes: 

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

585 

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

587 if type(other) is not URL: 

588 return NotImplemented 

589 

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

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

592 return ( 

593 self._scheme == other._scheme 

594 and self._netloc == other._netloc 

595 and path1 == path2 

596 and self._query == other._query 

597 and self._fragment == other._fragment 

598 ) 

599 

600 def __hash__(self) -> int: 

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

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

603 ret = self._cache["hash"] = hash( 

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

605 ) 

606 return ret 

607 

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

609 if type(other) is not URL: 

610 return NotImplemented 

611 return self._val <= other._val 

612 

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

614 if type(other) is not URL: 

615 return NotImplemented 

616 return self._val < other._val 

617 

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

619 if type(other) is not URL: 

620 return NotImplemented 

621 return self._val >= other._val 

622 

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

624 if type(other) is not URL: 

625 return NotImplemented 

626 return self._val > other._val 

627 

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

629 if not isinstance(name, str): 

630 return NotImplemented 

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

632 

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

634 return self.update_query(query) 

635 

636 def __bool__(self) -> bool: 

637 return bool(self._netloc or self._path or self._query or self._fragment) 

638 

639 def __getstate__(self) -> tuple[SplitURLType]: 

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

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

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

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

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

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

646 # ``SplitResult``) still load correctly. 

647 return (self._val,) 

648 

649 def __setstate__( 

650 self, state: tuple[SplitURLType] | tuple[None, _InternalURLCache] 

651 ) -> None: 

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

653 # default style pickle 

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

655 else: 

656 unused: list[object] 

657 val, *unused = state 

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

659 self._cache = {} 

660 

661 def _cache_netloc(self) -> None: 

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

663 c = self._cache 

664 split_loc = split_netloc(self._netloc) 

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

666 

667 def is_absolute(self) -> bool: 

668 """A check for absolute URLs. 

669 

670 Return True for absolute ones (having scheme or starting 

671 with //), False otherwise. 

672 

673 Is is preferred to call the .absolute property instead 

674 as it is cached. 

675 """ 

676 return self.absolute 

677 

678 def is_default_port(self) -> bool: 

679 """A check for default port. 

680 

681 Return True if port is default for specified scheme, 

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

683 otherwise. 

684 

685 Return False for relative URLs. 

686 

687 """ 

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

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

690 # using the default port unless its a relative URL 

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

692 return self._netloc != "" 

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

694 

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

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

697 

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

699 

700 """ 

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

702 return self._origin 

703 

704 @cached_property 

705 def _val(self) -> SplitURLType: 

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

707 

708 @cached_property 

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

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

711 

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

713 """ 

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

715 raise ValueError("URL should be absolute") 

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

717 raise ValueError("URL should have scheme") 

718 if "@" in netloc: 

719 encoded_host = self.host_subcomponent 

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

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

722 return self 

723 return from_parts(scheme, netloc, "", "", "") 

724 

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

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

727 

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

729 

730 """ 

731 if not self._netloc: 

732 raise ValueError("URL should be absolute") 

733 return from_parts("", "", self._path, self._query, self._fragment) 

734 

735 @cached_property 

736 def absolute(self) -> bool: 

737 """A check for absolute URLs. 

738 

739 Return True for absolute ones (having scheme or starting 

740 with //), False otherwise. 

741 

742 """ 

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

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

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

746 # to parse the host from the `netloc` 

747 return self._netloc != "" 

748 

749 @cached_property 

750 def scheme(self) -> str: 

751 """Scheme for absolute URLs. 

752 

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

754 

755 """ 

756 return self._scheme 

757 

758 @cached_property 

759 def raw_authority(self) -> str: 

760 """Encoded authority part of URL. 

761 

762 Empty string for relative URLs. 

763 

764 """ 

765 return self._netloc 

766 

767 @cached_property 

768 def authority(self) -> str: 

769 """Decoded authority part of URL. 

770 

771 Empty string for relative URLs. 

772 

773 """ 

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

775 

776 @cached_property 

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

778 """Encoded user part of URL. 

779 

780 None if user is missing. 

781 

782 """ 

783 # not .username 

784 self._cache_netloc() 

785 return self._cache["raw_user"] 

786 

787 @cached_property 

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

789 """Decoded user part of URL. 

790 

791 None if user is missing. 

792 

793 """ 

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

795 return None 

796 return UNQUOTER(raw_user) 

797 

798 @cached_property 

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

800 """Encoded password part of URL. 

801 

802 None if password is missing. 

803 

804 """ 

805 self._cache_netloc() 

806 return self._cache["raw_password"] 

807 

808 @cached_property 

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

810 """Decoded password part of URL. 

811 

812 None if password is missing. 

813 

814 """ 

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

816 return None 

817 return UNQUOTER(raw_password) 

818 

819 @cached_property 

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

821 """Encoded host part of URL. 

822 

823 None for relative URLs. 

824 

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

826 as it will return the host subcomponent with brackets. 

827 """ 

828 # Use host instead of hostname for sake of shortness 

829 # May add .hostname prop later 

830 self._cache_netloc() 

831 return self._cache["raw_host"] 

832 

833 @cached_property 

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

835 """Decoded host part of URL. 

836 

837 None for relative URLs. 

838 

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

840 ``%25`` zone separator is decoded back to ``%``; the encoded 

841 form is still available via :attr:`raw_host` and 

842 :attr:`host_subcomponent`. 

843 

844 """ 

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

846 return None 

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

848 # IP addresses are never IDNA encoded. The replace decodes 

849 # every %25 in the raw host, i.e. the RFC 6874 zone 

850 # separator and any %25 that percent-encodes a literal % 

851 # inside the zone identifier. 

852 if "%25" in raw: 

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

854 return raw 

855 return _idna_decode(raw) 

856 

857 @cached_property 

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

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

860 

861 None for relative URLs. 

862 

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

864 

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

866 

867 Examples: 

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

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

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

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

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

873 

874 """ 

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

876 return None 

877 return f"[{raw}]" if ":" in raw else raw 

878 

879 @cached_property 

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

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

882 

883 Trailing dots are removed from the host part. 

884 

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

886 

887 None for relative URLs. 

888 

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

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

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

892 port = *DIGIT 

893 

894 Examples: 

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

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

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

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

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

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

901 

902 """ 

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

904 return None 

905 if raw[-1] == ".": 

906 # Remove all trailing dots from the netloc as while 

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

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

909 # To avoid string manipulation we only call rstrip if 

910 # the last character is a dot. 

911 raw = raw.rstrip(".") 

912 port = self.explicit_port 

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

914 return f"[{raw}]" if ":" in raw else raw 

915 return f"[{raw}]:{port}" if ":" in raw else f"{raw}:{port}" 

916 

917 @cached_property 

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

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

920 

921 None for relative URLs or URLs without explicit port and 

922 scheme without default port substitution. 

923 

924 """ 

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

926 return explicit_port 

927 return DEFAULT_PORTS.get(self._scheme) 

928 

929 @cached_property 

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

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

932 

933 None for relative URLs or URLs without explicit port. 

934 

935 """ 

936 self._cache_netloc() 

937 return self._cache["explicit_port"] 

938 

939 @cached_property 

940 def raw_path(self) -> str: 

941 """Encoded path of URL. 

942 

943 / for absolute URLs without path part. 

944 

945 """ 

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

947 

948 @cached_property 

949 def path(self) -> str: 

950 """Decoded path of URL. 

951 

952 / for absolute URLs without path part. 

953 

954 """ 

955 return PATH_UNQUOTER(self._path) if self._path else "/" if self._netloc else "" 

956 

957 @cached_property 

958 def path_safe(self) -> str: 

959 """Decoded path of URL. 

960 

961 / for absolute URLs without path part. 

962 

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

964 

965 """ 

966 if self._path: 

967 return PATH_SAFE_UNQUOTER(self._path) 

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

969 

970 @cached_property 

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

972 """Parse query part of URL.""" 

973 return query_to_pairs(self._query) 

974 

975 @cached_property 

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

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

978 representation. 

979 

980 Empty value if URL has no query part. 

981 

982 """ 

983 return MultiDictProxy(MultiDict(self._parsed_query)) 

984 

985 @cached_property 

986 def raw_query_string(self) -> str: 

987 """Encoded query part of URL. 

988 

989 Empty string if query is missing. 

990 

991 """ 

992 return self._query 

993 

994 @cached_property 

995 def query_string(self) -> str: 

996 """Decoded query part of URL. 

997 

998 Empty string if query is missing. 

999 

1000 """ 

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

1002 

1003 @cached_property 

1004 def path_qs(self) -> str: 

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

1006 return self.path if not (q := self.query_string) else f"{self.path}?{q}" 

1007 

1008 @cached_property 

1009 def raw_path_qs(self) -> str: 

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

1011 if q := self._query: 

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

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

1014 

1015 @cached_property 

1016 def raw_fragment(self) -> str: 

1017 """Encoded fragment part of URL. 

1018 

1019 Empty string if fragment is missing. 

1020 

1021 """ 

1022 return self._fragment 

1023 

1024 @cached_property 

1025 def fragment(self) -> str: 

1026 """Decoded fragment part of URL. 

1027 

1028 Empty string if fragment is missing. 

1029 

1030 """ 

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

1032 

1033 @cached_property 

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

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

1036 

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

1038 

1039 """ 

1040 path = self._path 

1041 if self._netloc: 

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

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

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

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

1046 

1047 @cached_property 

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

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

1050 

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

1052 

1053 """ 

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

1055 

1056 @cached_property 

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

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

1059 fragment. 

1060 

1061 """ 

1062 path = self._path 

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

1064 if self._fragment or self._query: 

1065 return from_parts(self._scheme, self._netloc, path, "", "") 

1066 return self 

1067 parts = path.split("/") 

1068 return from_parts(self._scheme, self._netloc, "/".join(parts[:-1]), "", "") 

1069 

1070 @cached_property 

1071 def raw_name(self) -> str: 

1072 """The last part of raw_parts.""" 

1073 parts = self.raw_parts 

1074 if not self._netloc: 

1075 return parts[-1] 

1076 parts = parts[1:] 

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

1078 

1079 @cached_property 

1080 def name(self) -> str: 

1081 """The last part of parts.""" 

1082 return UNQUOTER(self.raw_name) 

1083 

1084 @cached_property 

1085 def raw_suffix(self) -> str: 

1086 name = self.raw_name 

1087 i = name.rfind(".") 

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

1089 

1090 @cached_property 

1091 def suffix(self) -> str: 

1092 return UNQUOTER(self.raw_suffix) 

1093 

1094 @cached_property 

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

1096 name = self.raw_name 

1097 if name.endswith("."): 

1098 return () 

1099 name = name.lstrip(".") 

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

1101 

1102 @cached_property 

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

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

1105 

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

1107 """ 

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

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

1110 """ 

1111 parsed: list[str] = [] 

1112 needs_normalize: bool = False 

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

1114 # empty segment of last is not removed 

1115 last = idx == 0 

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

1117 raise ValueError( 

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

1119 ) 

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

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

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

1123 # the existing path. 

1124 path = path if encoded else PATH_QUOTER(path) 

1125 needs_normalize |= "." in path 

1126 segments = path.split("/") 

1127 segments.reverse() 

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

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

1130 

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

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

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

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

1135 old.reverse() 

1136 parsed += old 

1137 

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

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

1140 if (netloc := self._netloc) and parsed and parsed[-1] != "": 

1141 parsed.append("") 

1142 

1143 parsed.reverse() 

1144 if not netloc or not needs_normalize: 

1145 return from_parts(self._scheme, netloc, "/".join(parsed), "", "") 

1146 

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

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

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

1150 path = f"/{path}" 

1151 return from_parts(self._scheme, netloc, path, "", "") 

1152 

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

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

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

1156 if not isinstance(scheme, str): 

1157 raise TypeError("Invalid scheme type") 

1158 lower_scheme = scheme.lower() 

1159 netloc = self._netloc 

1160 if not netloc and lower_scheme in SCHEME_REQUIRES_HOST: 

1161 msg = ( 

1162 "scheme replacement is not allowed for " 

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

1164 ) 

1165 raise ValueError(msg) 

1166 return from_parts(lower_scheme, netloc, self._path, self._query, self._fragment) 

1167 

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

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

1170 

1171 Autoencode user if needed. 

1172 

1173 Clear user/password if user is None. 

1174 

1175 """ 

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

1177 if user is None: 

1178 password = None 

1179 elif isinstance(user, str): 

1180 user = QUOTER(user) 

1181 password = self.raw_password 

1182 else: 

1183 raise TypeError("Invalid user type") 

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

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

1186 encoded_host = self.host_subcomponent or "" 

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

1188 return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) 

1189 

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

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

1192 

1193 Autoencode password if needed. 

1194 

1195 Clear password if argument is None. 

1196 

1197 """ 

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

1199 if password is None: 

1200 pass 

1201 elif isinstance(password, str): 

1202 password = QUOTER(password) 

1203 else: 

1204 raise TypeError("Invalid password type") 

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

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

1207 encoded_host = self.host_subcomponent or "" 

1208 port = self.explicit_port 

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

1210 return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) 

1211 

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

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

1214 

1215 Autoencode host if needed. 

1216 

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

1218 instead. 

1219 

1220 """ 

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

1222 if not isinstance(host, str): 

1223 raise TypeError("Invalid host type") 

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

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

1226 if not host: 

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

1228 encoded_host = _encode_host(host, validate_host=True) if host else "" 

1229 port = self.explicit_port 

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

1231 return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) 

1232 

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

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

1235 

1236 Clear port to default if None is passed. 

1237 

1238 """ 

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

1240 if port is not None: 

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

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

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

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

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

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

1247 encoded_host = self.host_subcomponent or "" 

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

1249 return from_parts(self._scheme, netloc, self._path, self._query, self._fragment) 

1250 

1251 def with_path( 

1252 self, 

1253 path: str, 

1254 *, 

1255 encoded: bool = False, 

1256 keep_query: bool = False, 

1257 keep_fragment: bool = False, 

1258 ) -> "URL": 

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

1260 netloc = self._netloc 

1261 if not encoded: 

1262 path = PATH_QUOTER(path) 

1263 if netloc: 

1264 path = normalize_path(path) if "." in path else path 

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

1266 path = f"/{path}" 

1267 query = self._query if keep_query else "" 

1268 fragment = self._fragment if keep_fragment else "" 

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

1270 

1271 @overload 

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

1273 

1274 @overload 

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

1276 

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

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

1279 

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

1281 or str, autoencode the argument if needed. 

1282 

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

1284 

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

1286 

1287 Clear query if None is passed. 

1288 

1289 """ 

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

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

1292 return from_parts_uncached( 

1293 self._scheme, self._netloc, self._path, query, self._fragment 

1294 ) 

1295 

1296 @overload 

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

1298 

1299 @overload 

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

1301 

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

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

1304 

1305 This method will not remove existing query parameters. 

1306 

1307 Example: 

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

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

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

1311 """ 

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

1313 return self 

1314 if query := self._query: 

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

1316 # string join 

1317 query += new_query if query[-1] == "&" else f"&{new_query}" 

1318 else: 

1319 query = new_query 

1320 return from_parts_uncached( 

1321 self._scheme, self._netloc, self._path, query, self._fragment 

1322 ) 

1323 

1324 @overload 

1325 def update_query(self, query: Query) -> "URL": ... 

1326 

1327 @overload 

1328 def update_query(self, **kwargs: QueryVariable) -> "URL": ... 

1329 

1330 def update_query(self, *args: Any, **kwargs: Any) -> "URL": 

1331 """Return a new URL with query part updated. 

1332 

1333 This method will overwrite existing query parameters. 

1334 

1335 Example: 

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

1337 >>> url.update_query(a=3, c=4) 

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

1339 """ 

1340 in_query: ( 

1341 str 

1342 | Mapping[str, QueryVariable] 

1343 | Sequence[tuple[str | istr, SimpleQuery]] 

1344 | None 

1345 ) 

1346 if kwargs: 

1347 if args: 

1348 msg = "Either kwargs or single query parameter must be present" 

1349 raise ValueError(msg) 

1350 in_query = kwargs 

1351 elif len(args) == 1: 

1352 in_query = args[0] 

1353 else: 

1354 raise ValueError("Either kwargs or single query parameter must be present") 

1355 

1356 if in_query is None: 

1357 query = "" 

1358 elif not in_query: 

1359 query = self._query 

1360 elif isinstance(in_query, Mapping): 

1361 qm: MultiDict[QueryVariable] = MultiDict(self._parsed_query) 

1362 qm.update(in_query) 

1363 query = get_str_query_from_sequence_iterable(qm.items()) 

1364 elif isinstance(in_query, str): 

1365 qstr: MultiDict[str] = MultiDict(self._parsed_query) 

1366 qstr.update(query_to_pairs(in_query)) 

1367 query = get_str_query_from_iterable(qstr.items()) 

1368 elif isinstance(in_query, (bytes, bytearray, memoryview)): 

1369 msg = "Invalid query type: bytes, bytearray and memoryview are forbidden" 

1370 raise TypeError(msg) 

1371 elif isinstance(in_query, Sequence): 

1372 # We don't expect sequence values if we're given a list of pairs 

1373 # already; only mappings like builtin `dict` which can't have the 

1374 # same key pointing to multiple values are allowed to use 

1375 # `_query_seq_pairs`. 

1376 if TYPE_CHECKING: 

1377 in_query = cast( 

1378 Sequence[tuple[Union[str, istr], SimpleQuery]], in_query 

1379 ) 

1380 qs: MultiDict[SimpleQuery] = MultiDict(self._parsed_query) 

1381 qs.update(in_query) 

1382 query = get_str_query_from_iterable(qs.items()) 

1383 else: 

1384 raise TypeError( 

1385 "Invalid query type: only str, mapping or " 

1386 "sequence of (key, value) pairs is allowed" 

1387 ) 

1388 return from_parts_uncached( 

1389 self._scheme, self._netloc, self._path, query, self._fragment 

1390 ) 

1391 

1392 def without_query_params(self, *query_params: str) -> "URL": 

1393 """Remove some keys from query part and return new URL.""" 

1394 params_to_remove = set(query_params) & self.query.keys() 

1395 if not params_to_remove: 

1396 return self 

1397 return self.with_query( 

1398 tuple( 

1399 (name, value) 

1400 for name, value in self.query.items() 

1401 if name not in params_to_remove 

1402 ) 

1403 ) 

1404 

1405 def with_fragment(self, fragment: str | None) -> "URL": 

1406 """Return a new URL with fragment replaced. 

1407 

1408 Autoencode fragment if needed. 

1409 

1410 Clear fragment to default if None is passed. 

1411 

1412 """ 

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

1414 if fragment is None: 

1415 raw_fragment = "" 

1416 elif not isinstance(fragment, str): 

1417 raise TypeError("Invalid fragment type") 

1418 else: 

1419 raw_fragment = FRAGMENT_QUOTER(fragment) 

1420 if self._fragment == raw_fragment: 

1421 return self 

1422 return from_parts( 

1423 self._scheme, self._netloc, self._path, self._query, raw_fragment 

1424 ) 

1425 

1426 def with_name( 

1427 self, 

1428 name: str, 

1429 *, 

1430 keep_query: bool = False, 

1431 keep_fragment: bool = False, 

1432 ) -> "URL": 

1433 """Return a new URL with name (last part of path) replaced. 

1434 

1435 Query and fragment parts are cleaned up. 

1436 

1437 Name is encoded if needed. 

1438 

1439 """ 

1440 # N.B. DOES cleanup query/fragment 

1441 if not isinstance(name, str): 

1442 raise TypeError("Invalid name type") 

1443 if "/" in name: 

1444 raise ValueError("Slash in name is not allowed") 

1445 name = PATH_QUOTER(name) 

1446 if name in (".", ".."): 

1447 raise ValueError(". and .. values are forbidden") 

1448 parts = list(self.raw_parts) 

1449 if netloc := self._netloc: 

1450 if len(parts) == 1: 

1451 parts.append(name) 

1452 else: 

1453 parts[-1] = name 

1454 parts[0] = "" # replace leading '/' 

1455 else: 

1456 parts[-1] = name 

1457 if parts[0] == "/": 

1458 parts[0] = "" # replace leading '/' 

1459 

1460 query = self._query if keep_query else "" 

1461 fragment = self._fragment if keep_fragment else "" 

1462 return from_parts(self._scheme, netloc, "/".join(parts), query, fragment) 

1463 

1464 def with_suffix( 

1465 self, 

1466 suffix: str, 

1467 *, 

1468 keep_query: bool = False, 

1469 keep_fragment: bool = False, 

1470 ) -> "URL": 

1471 """Return a new URL with suffix (file extension of name) replaced. 

1472 

1473 Query and fragment parts are cleaned up. 

1474 

1475 suffix is encoded if needed. 

1476 """ 

1477 if not isinstance(suffix, str): 

1478 raise TypeError("Invalid suffix type") 

1479 if suffix and not suffix[0] == "." or suffix == "." or "/" in suffix: 

1480 raise ValueError(f"Invalid suffix {suffix!r}") 

1481 name = self.raw_name 

1482 if not name: 

1483 raise ValueError(f"{self!r} has an empty name") 

1484 old_suffix = self.raw_suffix 

1485 suffix = PATH_QUOTER(suffix) 

1486 name = name + suffix if not old_suffix else name[: -len(old_suffix)] + suffix 

1487 if name in (".", ".."): 

1488 raise ValueError(". and .. values are forbidden") 

1489 parts = list(self.raw_parts) 

1490 if netloc := self._netloc: 

1491 if len(parts) == 1: 

1492 parts.append(name) 

1493 else: 

1494 parts[-1] = name 

1495 parts[0] = "" # replace leading '/' 

1496 else: 

1497 parts[-1] = name 

1498 if parts[0] == "/": 

1499 parts[0] = "" # replace leading '/' 

1500 

1501 query = self._query if keep_query else "" 

1502 fragment = self._fragment if keep_fragment else "" 

1503 return from_parts(self._scheme, netloc, "/".join(parts), query, fragment) 

1504 

1505 def join(self, url: "URL") -> "URL": 

1506 """Join URLs 

1507 

1508 Construct a full (“absolute”) URL by combining a “base URL” 

1509 (self) with another URL (url). 

1510 

1511 Informally, this uses components of the base URL, in 

1512 particular the addressing scheme, the network location and 

1513 (part of) the path, to provide missing components in the 

1514 relative URL. 

1515 

1516 """ 

1517 if type(url) is not URL: 

1518 raise TypeError("url should be URL") 

1519 

1520 scheme = url._scheme or self._scheme 

1521 if scheme != self._scheme or scheme not in USES_RELATIVE: 

1522 return url 

1523 

1524 # scheme is in uses_authority as uses_authority is a superset of uses_relative 

1525 if (join_netloc := url._netloc) and scheme in USES_AUTHORITY: 

1526 return from_parts(scheme, join_netloc, url._path, url._query, url._fragment) 

1527 

1528 orig_path = self._path 

1529 if join_path := url._path: 

1530 if join_path[0] == "/": 

1531 path = join_path 

1532 elif not orig_path: 

1533 path = f"/{join_path}" 

1534 elif orig_path[-1] == "/": 

1535 path = f"{orig_path}{join_path}" 

1536 else: 

1537 # … 

1538 # and relativizing ".." 

1539 # parts[0] is / for absolute urls, 

1540 # this join will add a double slash there 

1541 path = "/".join([*self.parts[:-1], ""]) + join_path 

1542 # which has to be removed 

1543 if orig_path[0] == "/": 

1544 path = path[1:] 

1545 path = normalize_path(path) if "." in path else path 

1546 else: 

1547 path = orig_path 

1548 

1549 return from_parts( 

1550 scheme, 

1551 self._netloc, 

1552 path, 

1553 url._query if join_path or url._query else self._query, 

1554 url._fragment if join_path or url._fragment else self._fragment, 

1555 ) 

1556 

1557 def joinpath(self, *other: str, encoded: bool = False) -> "URL": 

1558 """Return a new URL with the elements in other appended to the path.""" 

1559 return self._make_child(other, encoded=encoded) 

1560 

1561 def human_repr(self) -> str: 

1562 """Return decoded human readable string for URL representation.""" 

1563 user = human_quote(self.user, "#/:?@[]\\") 

1564 password = human_quote(self.password, "#/:?@[]\\") 

1565 if (host := self.host) and ":" in host: 

1566 host = f"[{host}]" 

1567 path = human_quote(self.path, "#?") 

1568 if TYPE_CHECKING: 

1569 assert path is not None 

1570 if not self._scheme and not self._netloc: 

1571 path = _encode_relative_scheme_colon(path) 

1572 query_string = "&".join( 

1573 "{}={}".format(human_quote(k, "#&+;="), human_quote(v, "#&+;=")) 

1574 for k, v in self.query.items() 

1575 ) 

1576 fragment = human_quote(self.fragment, "") 

1577 if TYPE_CHECKING: 

1578 assert fragment is not None 

1579 netloc = make_netloc(user, password, host, self.explicit_port) 

1580 return unsplit_result(self._scheme, netloc, path, query_string, fragment) 

1581 

1582 if HAS_PYDANTIC: 

1583 # Borrowed from https://docs.pydantic.dev/latest/concepts/types/#handling-third-party-types 

1584 @classmethod 

1585 def __get_pydantic_json_schema__( 

1586 cls, 

1587 core_schema: "CoreSchema", 

1588 handler: "GetJsonSchemaHandler", 

1589 ) -> "JsonSchemaValue": 

1590 field_schema: dict[str, Any] = {} 

1591 field_schema.update(type="string", format="uri") 

1592 return field_schema 

1593 

1594 @classmethod 

1595 def __get_pydantic_core_schema__( 

1596 cls, 

1597 source_type: type[Self] | type[str], 

1598 handler: "GetCoreSchemaHandler", 

1599 ) -> "CoreSchema": 

1600 # Lazy import: pulling in pydantic_core at module load time 

1601 # increases yarl's import cost 3-7x for users who don't use 

1602 # pydantic. Keep this import function-scoped. 

1603 from pydantic_core import core_schema # noqa: PLC0415 

1604 

1605 from_str_schema = core_schema.chain_schema( 

1606 [ 

1607 core_schema.str_schema(), 

1608 core_schema.no_info_plain_validator_function(URL), 

1609 ] 

1610 ) 

1611 

1612 return core_schema.json_or_python_schema( 

1613 json_schema=from_str_schema, 

1614 python_schema=core_schema.union_schema( 

1615 [ 

1616 # check if it's an instance first before doing any further work 

1617 core_schema.is_instance_schema(URL), 

1618 from_str_schema, 

1619 ] 

1620 ), 

1621 serialization=core_schema.plain_serializer_function_ser_schema(str), 

1622 ) 

1623 

1624 

1625_DEFAULT_IDNA_SIZE = 256 

1626_DEFAULT_ENCODE_SIZE = 512 

1627 

1628 

1629@lru_cache(_DEFAULT_IDNA_SIZE) 

1630def _idna_decode(raw: str) -> str: 

1631 try: 

1632 return idna.decode(raw.encode("ascii")) 

1633 except UnicodeError: # e.g. '::1' 

1634 return raw.encode("ascii").decode("idna") 

1635 

1636 

1637@lru_cache(_DEFAULT_IDNA_SIZE) 

1638def _idna_encode(host: str) -> str: 

1639 try: 

1640 return idna.encode(host, uts46=True).decode("ascii") 

1641 except UnicodeError: 

1642 return host.encode("idna").decode("ascii") 

1643 

1644 

1645@lru_cache(_DEFAULT_ENCODE_SIZE) 

1646def _encode_host(host: str, validate_host: bool) -> str: 

1647 """Encode host part of URL.""" 

1648 # If the host ends with a digit or contains a colon, its likely 

1649 # an IP address. 

1650 if host and (host[-1].isdigit() or ":" in host): 

1651 # RFC 6874 spells the IPv6 zone separator as the percent-encoded 

1652 # ``%25``; bare ``%`` is still accepted so that hosts constructed 

1653 # programmatically (e.g. ``with_host("fe80::1%1")``) keep working. 

1654 part = "%25" if "%25" in host else "%" 

1655 raw_ip, sep, zone = host.partition(part) 

1656 # If it looks like an IP, we check with _ip_compressed_version 

1657 # and fall-through if its not an IP address. This is a performance 

1658 # optimization to avoid parsing IP addresses as much as possible 

1659 # because it is orders of magnitude slower than almost any other 

1660 # operation this library does. 

1661 # Might be an IP address, check it 

1662 # 

1663 # IP Addresses can look like: 

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

1665 # - 127.0.0.1 (last character is a digit) 

1666 # - 2001:db8::ff00:42:8329 (contains a colon) 

1667 # - 2001:db8::ff00:42:8329%eth0 (contains a colon) 

1668 # - [2001:db8::ff00:42:8329] (contains a colon -- brackets should 

1669 # have been removed before it gets here) 

1670 # Rare IP Address formats are not supported per: 

1671 # https://datatracker.ietf.org/doc/html/rfc3986#section-7.4 

1672 # 

1673 # IP parsing is slow, so its wrapped in an LRU 

1674 try: 

1675 ip = ip_address(raw_ip) 

1676 except ValueError: 

1677 pass 

1678 else: 

1679 if sep and validate_host and (not zone or _ZONE_ID_UNSAFE_RE.search(zone)): 

1680 raise ValueError("Invalid characters in zone identifier") 

1681 # These checks should not happen in the 

1682 # LRU to keep the cache size small 

1683 host = ip.compressed 

1684 if ip.version == 6: 

1685 return f"[{host}{sep}{zone}]" if sep else f"[{host}]" 

1686 return f"{host}{sep}{zone}" if sep else host 

1687 

1688 # IDNA encoding is slow, skip it for ASCII-only strings 

1689 if host.isascii(): 

1690 # Check for invalid characters explicitly; _idna_encode() does this 

1691 # for non-ascii host names. 

1692 host = host.lower() 

1693 if validate_host and (invalid := NOT_REG_NAME.search(host)): 

1694 value, pos, extra = invalid.group(), invalid.start(), "" 

1695 if value == "@" or (value == ":" and "@" in host[pos:]): 

1696 # this looks like an authority string 

1697 extra = ( 

1698 ", if the value includes a username or password, " 

1699 "use 'authority' instead of 'host'" 

1700 ) 

1701 raise ValueError( 

1702 f"Host {host!r} cannot contain {value!r} (at position {pos}){extra}" 

1703 ) from None 

1704 return host 

1705 

1706 # IDNA/UTS-46 mapping silently deletes default-ignorable code points, which 

1707 # would turn e.g. ``e<ZWSP>vil.com`` into ``evil.com``, a different host 

1708 # than the string the caller supplied. Reject them on every path (this runs 

1709 # regardless of ``validate_host`` since the plain ``URL(str)`` constructor 

1710 # encodes with ``validate_host=False``) so the parsed host cannot diverge 

1711 # from the input, matching idna/httpx/urllib3. 

1712 if invalid := _DEFAULT_IGNORABLE_RE.search(host): 

1713 raise ValueError( 

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

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

1716 ) from None 

1717 encoded = _idna_encode(host) 

1718 # IDNA uses NFKC equivalence, so normalization can expand a non-ascii 

1719 # character into an ASCII delimiter (e.g. the fullwidth solidus U+FF0F 

1720 # becomes '/'). The ascii branch above rejects such delimiters directly; 

1721 # apply the same check to the IDNA output so the builder APIs agree with 

1722 # the parser's _check_netloc. 

1723 if validate_host and (invalid := NOT_REG_NAME.search(encoded)): 

1724 raise ValueError( 

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

1726 f"after IDNA normalization to {encoded!r}" 

1727 ) from None 

1728 return encoded 

1729 

1730 

1731@rewrite_module 

1732def cache_clear() -> None: 

1733 """Clear all LRU caches.""" 

1734 _idna_encode.cache_clear() 

1735 _idna_decode.cache_clear() 

1736 _encode_host.cache_clear() 

1737 

1738 

1739@rewrite_module 

1740def cache_info() -> CacheInfo: 

1741 """Report cache statistics.""" 

1742 return { 

1743 "idna_encode": _idna_encode.cache_info(), 

1744 "idna_decode": _idna_decode.cache_info(), 

1745 "ip_address": _encode_host.cache_info(), 

1746 "host_validate": _encode_host.cache_info(), 

1747 "encode_host": _encode_host.cache_info(), 

1748 } 

1749 

1750 

1751@rewrite_module 

1752def cache_configure( 

1753 *, 

1754 idna_encode_size: int | None = _DEFAULT_IDNA_SIZE, 

1755 idna_decode_size: int | None = _DEFAULT_IDNA_SIZE, 

1756 ip_address_size: int | None | UndefinedType = UNDEFINED, 

1757 host_validate_size: int | None | UndefinedType = UNDEFINED, 

1758 encode_host_size: int | None | UndefinedType = UNDEFINED, 

1759) -> None: 

1760 """Configure LRU cache sizes.""" 

1761 global _idna_decode, _idna_encode, _encode_host 

1762 # ip_address_size, host_validate_size are no longer 

1763 # used, but are kept for backwards compatibility. 

1764 if ip_address_size is not UNDEFINED or host_validate_size is not UNDEFINED: 

1765 warnings.warn( 

1766 "cache_configure() no longer accepts the " 

1767 "ip_address_size or host_validate_size arguments, " 

1768 "they are used to set the encode_host_size instead " 

1769 "and will be removed in the future", 

1770 DeprecationWarning, 

1771 stacklevel=2, 

1772 ) 

1773 

1774 if encode_host_size is not None: 

1775 for size in (ip_address_size, host_validate_size): 

1776 if size is None: 

1777 encode_host_size = None 

1778 elif encode_host_size is UNDEFINED: 

1779 if size is not UNDEFINED: 

1780 encode_host_size = size 

1781 elif size is not UNDEFINED: 

1782 if TYPE_CHECKING: 

1783 assert isinstance(size, int) 

1784 assert isinstance(encode_host_size, int) 

1785 encode_host_size = max(size, encode_host_size) 

1786 if encode_host_size is UNDEFINED: 

1787 encode_host_size = _DEFAULT_ENCODE_SIZE 

1788 

1789 _encode_host = lru_cache(encode_host_size)(_encode_host.__wrapped__) 

1790 _idna_decode = lru_cache(idna_decode_size)(_idna_decode.__wrapped__) 

1791 _idna_encode = lru_cache(idna_encode_size)(_idna_encode.__wrapped__)