Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpx/_urls.py: 31%

252 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:12 +0000

1import typing 

2from urllib.parse import parse_qs, quote, unquote, urlencode 

3 

4import idna 

5import rfc3986 

6import rfc3986.exceptions 

7 

8from ._exceptions import InvalidURL 

9from ._types import PrimitiveData, QueryParamTypes, RawURL, URLTypes 

10from ._utils import primitive_value_to_str 

11 

12 

13class URL: 

14 """ 

15 url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink") 

16 

17 assert url.scheme == "https" 

18 assert url.username == "jo@email.com" 

19 assert url.password == "a secret" 

20 assert url.userinfo == b"jo%40email.com:a%20secret" 

21 assert url.host == "müller.de" 

22 assert url.raw_host == b"xn--mller-kva.de" 

23 assert url.port == 1234 

24 assert url.netloc == b"xn--mller-kva.de:1234" 

25 assert url.path == "/pa th" 

26 assert url.query == b"?search=ab" 

27 assert url.raw_path == b"/pa%20th?search=ab" 

28 assert url.fragment == "anchorlink" 

29 

30 The components of a URL are broken down like this: 

31 

32 https://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink 

33 [scheme] [ username ] [password] [ host ][port][ path ] [ query ] [fragment] 

34 [ userinfo ] [ netloc ][ raw_path ] 

35 

36 Note that: 

37 

38 * `url.scheme` is normalized to always be lowercased. 

39 

40 * `url.host` is normalized to always be lowercased. Internationalized domain 

41 names are represented in unicode, without IDNA encoding applied. For instance: 

42 

43 url = httpx.URL("http://中国.icom.museum") 

44 assert url.host == "中国.icom.museum" 

45 url = httpx.URL("http://xn--fiqs8s.icom.museum") 

46 assert url.host == "中国.icom.museum" 

47 

48 * `url.raw_host` is normalized to always be lowercased, and is IDNA encoded. 

49 

50 url = httpx.URL("http://中国.icom.museum") 

51 assert url.raw_host == b"xn--fiqs8s.icom.museum" 

52 url = httpx.URL("http://xn--fiqs8s.icom.museum") 

53 assert url.raw_host == b"xn--fiqs8s.icom.museum" 

54 

55 * `url.port` is either None or an integer. URLs that include the default port for 

56 "http", "https", "ws", "wss", and "ftp" schemes have their port normalized to `None`. 

57 

58 assert httpx.URL("http://example.com") == httpx.URL("http://example.com:80") 

59 assert httpx.URL("http://example.com").port is None 

60 assert httpx.URL("http://example.com:80").port is None 

61 

62 * `url.userinfo` is raw bytes, without URL escaping. Usually you'll want to work with 

63 `url.username` and `url.password` instead, which handle the URL escaping. 

64 

65 * `url.raw_path` is raw bytes of both the path and query, without URL escaping. 

66 This portion is used as the target when constructing HTTP requests. Usually you'll 

67 want to work with `url.path` instead. 

68 

69 * `url.query` is raw bytes, without URL escaping. A URL query string portion can only 

70 be properly URL escaped when decoding the parameter names and values themselves. 

71 """ 

72 

73 _uri_reference: rfc3986.URIReference 

74 

75 def __init__( 

76 self, url: typing.Union["URL", str] = "", **kwargs: typing.Any 

77 ) -> None: 

78 if isinstance(url, str): 

79 try: 

80 self._uri_reference = rfc3986.iri_reference(url).encode() 

81 except rfc3986.exceptions.InvalidAuthority as exc: 

82 raise InvalidURL(message=str(exc)) from None 

83 

84 if self.is_absolute_url: 

85 # We don't want to normalize relative URLs, since doing so 

86 # removes any leading `../` portion. 

87 self._uri_reference = self._uri_reference.normalize() 

88 elif isinstance(url, URL): 

89 self._uri_reference = url._uri_reference 

90 else: 

91 raise TypeError( 

92 f"Invalid type for url. Expected str or httpx.URL, got {type(url)}: {url!r}" 

93 ) 

94 

95 # Perform port normalization, following the WHATWG spec for default ports. 

96 # 

97 # See: 

98 # * https://tools.ietf.org/html/rfc3986#section-3.2.3 

99 # * https://url.spec.whatwg.org/#url-miscellaneous 

100 # * https://url.spec.whatwg.org/#scheme-state 

101 default_port = { 

102 "ftp": ":21", 

103 "http": ":80", 

104 "https": ":443", 

105 "ws": ":80", 

106 "wss": ":443", 

107 }.get(self._uri_reference.scheme, "") 

108 authority = self._uri_reference.authority or "" 

109 if default_port and authority.endswith(default_port): 

110 authority = authority[: -len(default_port)] 

111 self._uri_reference = self._uri_reference.copy_with(authority=authority) 

112 

113 if kwargs: 

114 self._uri_reference = self.copy_with(**kwargs)._uri_reference 

115 

116 @property 

117 def scheme(self) -> str: 

118 """ 

119 The URL scheme, such as "http", "https". 

120 Always normalised to lowercase. 

121 """ 

122 return self._uri_reference.scheme or "" 

123 

124 @property 

125 def raw_scheme(self) -> bytes: 

126 """ 

127 The raw bytes representation of the URL scheme, such as b"http", b"https". 

128 Always normalised to lowercase. 

129 """ 

130 return self.scheme.encode("ascii") 

131 

132 @property 

133 def userinfo(self) -> bytes: 

134 """ 

135 The URL userinfo as a raw bytestring. 

136 For example: b"jo%40email.com:a%20secret". 

137 """ 

138 userinfo = self._uri_reference.userinfo or "" 

139 return userinfo.encode("ascii") 

140 

141 @property 

142 def username(self) -> str: 

143 """ 

144 The URL username as a string, with URL decoding applied. 

145 For example: "jo@email.com" 

146 """ 

147 userinfo = self._uri_reference.userinfo or "" 

148 return unquote(userinfo.partition(":")[0]) 

149 

150 @property 

151 def password(self) -> str: 

152 """ 

153 The URL password as a string, with URL decoding applied. 

154 For example: "a secret" 

155 """ 

156 userinfo = self._uri_reference.userinfo or "" 

157 return unquote(userinfo.partition(":")[2]) 

158 

159 @property 

160 def host(self) -> str: 

161 """ 

162 The URL host as a string. 

163 Always normalized to lowercase, with IDNA hosts decoded into unicode. 

164 

165 Examples: 

166 

167 url = httpx.URL("http://www.EXAMPLE.org") 

168 assert url.host == "www.example.org" 

169 

170 url = httpx.URL("http://中国.icom.museum") 

171 assert url.host == "中国.icom.museum" 

172 

173 url = httpx.URL("http://xn--fiqs8s.icom.museum") 

174 assert url.host == "中国.icom.museum" 

175 

176 url = httpx.URL("https://[::ffff:192.168.0.1]") 

177 assert url.host == "::ffff:192.168.0.1" 

178 """ 

179 host: str = self._uri_reference.host or "" 

180 

181 if host and ":" in host and host[0] == "[": 

182 # it's an IPv6 address 

183 host = host.lstrip("[").rstrip("]") 

184 

185 if host.startswith("xn--"): 

186 host = idna.decode(host) 

187 

188 return host 

189 

190 @property 

191 def raw_host(self) -> bytes: 

192 """ 

193 The raw bytes representation of the URL host. 

194 Always normalized to lowercase, and IDNA encoded. 

195 

196 Examples: 

197 

198 url = httpx.URL("http://www.EXAMPLE.org") 

199 assert url.raw_host == b"www.example.org" 

200 

201 url = httpx.URL("http://中国.icom.museum") 

202 assert url.raw_host == b"xn--fiqs8s.icom.museum" 

203 

204 url = httpx.URL("http://xn--fiqs8s.icom.museum") 

205 assert url.raw_host == b"xn--fiqs8s.icom.museum" 

206 

207 url = httpx.URL("https://[::ffff:192.168.0.1]") 

208 assert url.raw_host == b"::ffff:192.168.0.1" 

209 """ 

210 host: str = self._uri_reference.host or "" 

211 

212 if host and ":" in host and host[0] == "[": 

213 # it's an IPv6 address 

214 host = host.lstrip("[").rstrip("]") 

215 

216 return host.encode("ascii") 

217 

218 @property 

219 def port(self) -> typing.Optional[int]: 

220 """ 

221 The URL port as an integer. 

222 

223 Note that the URL class performs port normalization as per the WHATWG spec. 

224 Default ports for "http", "https", "ws", "wss", and "ftp" schemes are always 

225 treated as `None`. 

226 

227 For example: 

228 

229 assert httpx.URL("http://www.example.com") == httpx.URL("http://www.example.com:80") 

230 assert httpx.URL("http://www.example.com:80").port is None 

231 """ 

232 port = self._uri_reference.port 

233 return int(port) if port else None 

234 

235 @property 

236 def netloc(self) -> bytes: 

237 """ 

238 Either `<host>` or `<host>:<port>` as bytes. 

239 Always normalized to lowercase, and IDNA encoded. 

240 

241 This property may be used for generating the value of a request 

242 "Host" header. 

243 """ 

244 host = self._uri_reference.host or "" 

245 port = self._uri_reference.port 

246 netloc = host.encode("ascii") 

247 if port: 

248 netloc = netloc + b":" + port.encode("ascii") 

249 return netloc 

250 

251 @property 

252 def path(self) -> str: 

253 """ 

254 The URL path as a string. Excluding the query string, and URL decoded. 

255 

256 For example: 

257 

258 url = httpx.URL("https://example.com/pa%20th") 

259 assert url.path == "/pa th" 

260 """ 

261 path = self._uri_reference.path or "/" 

262 return unquote(path) 

263 

264 @property 

265 def query(self) -> bytes: 

266 """ 

267 The URL query string, as raw bytes, excluding the leading b"?". 

268 

269 This is necessarily a bytewise interface, because we cannot 

270 perform URL decoding of this representation until we've parsed 

271 the keys and values into a QueryParams instance. 

272 

273 For example: 

274 

275 url = httpx.URL("https://example.com/?filter=some%20search%20terms") 

276 assert url.query == b"filter=some%20search%20terms" 

277 """ 

278 query = self._uri_reference.query or "" 

279 return query.encode("ascii") 

280 

281 @property 

282 def params(self) -> "QueryParams": 

283 """ 

284 The URL query parameters, neatly parsed and packaged into an immutable 

285 multidict representation. 

286 """ 

287 return QueryParams(self._uri_reference.query) 

288 

289 @property 

290 def raw_path(self) -> bytes: 

291 """ 

292 The complete URL path and query string as raw bytes. 

293 Used as the target when constructing HTTP requests. 

294 

295 For example: 

296 

297 GET /users?search=some%20text HTTP/1.1 

298 Host: www.example.org 

299 Connection: close 

300 """ 

301 path = self._uri_reference.path or "/" 

302 if self._uri_reference.query is not None: 

303 path += "?" + self._uri_reference.query 

304 return path.encode("ascii") 

305 

306 @property 

307 def fragment(self) -> str: 

308 """ 

309 The URL fragments, as used in HTML anchors. 

310 As a string, without the leading '#'. 

311 """ 

312 return unquote(self._uri_reference.fragment or "") 

313 

314 @property 

315 def raw(self) -> RawURL: 

316 """ 

317 Provides the (scheme, host, port, target) for the outgoing request. 

318 

319 In older versions of `httpx` this was used in the low-level transport API. 

320 We no longer use `RawURL`, and this property will be deprecated in a future release. 

321 """ 

322 return RawURL( 

323 self.raw_scheme, 

324 self.raw_host, 

325 self.port, 

326 self.raw_path, 

327 ) 

328 

329 @property 

330 def is_absolute_url(self) -> bool: 

331 """ 

332 Return `True` for absolute URLs such as 'http://example.com/path', 

333 and `False` for relative URLs such as '/path'. 

334 """ 

335 # We don't use `.is_absolute` from `rfc3986` because it treats 

336 # URLs with a fragment portion as not absolute. 

337 # What we actually care about is if the URL provides 

338 # a scheme and hostname to which connections should be made. 

339 return bool(self._uri_reference.scheme and self._uri_reference.host) 

340 

341 @property 

342 def is_relative_url(self) -> bool: 

343 """ 

344 Return `False` for absolute URLs such as 'http://example.com/path', 

345 and `True` for relative URLs such as '/path'. 

346 """ 

347 return not self.is_absolute_url 

348 

349 def copy_with(self, **kwargs: typing.Any) -> "URL": 

350 """ 

351 Copy this URL, returning a new URL with some components altered. 

352 Accepts the same set of parameters as the components that are made 

353 available via properties on the `URL` class. 

354 

355 For example: 

356 

357 url = httpx.URL("https://www.example.com").copy_with(username="jo@gmail.com", password="a secret") 

358 assert url == "https://jo%40email.com:a%20secret@www.example.com" 

359 """ 

360 allowed = { 

361 "scheme": str, 

362 "username": str, 

363 "password": str, 

364 "userinfo": bytes, 

365 "host": str, 

366 "port": int, 

367 "netloc": bytes, 

368 "path": str, 

369 "query": bytes, 

370 "raw_path": bytes, 

371 "fragment": str, 

372 "params": object, 

373 } 

374 

375 # Step 1 

376 # ====== 

377 # 

378 # Perform type checking for all supported keyword arguments. 

379 for key, value in kwargs.items(): 

380 if key not in allowed: 

381 message = f"{key!r} is an invalid keyword argument for copy_with()" 

382 raise TypeError(message) 

383 if value is not None and not isinstance(value, allowed[key]): 

384 expected = allowed[key].__name__ 

385 seen = type(value).__name__ 

386 message = f"Argument {key!r} must be {expected} but got {seen}" 

387 raise TypeError(message) 

388 

389 # Step 2 

390 # ====== 

391 # 

392 # Consolidate "username", "password", "userinfo", "host", "port" and "netloc" 

393 # into a single "authority" keyword, for `rfc3986`. 

394 if "username" in kwargs or "password" in kwargs: 

395 # Consolidate "username" and "password" into "userinfo". 

396 username = quote(kwargs.pop("username", self.username) or "") 

397 password = quote(kwargs.pop("password", self.password) or "") 

398 userinfo = f"{username}:{password}" if password else username 

399 kwargs["userinfo"] = userinfo.encode("ascii") 

400 

401 if "host" in kwargs or "port" in kwargs: 

402 # Consolidate "host" and "port" into "netloc". 

403 host = kwargs.pop("host", self.host) or "" 

404 port = kwargs.pop("port", self.port) 

405 

406 if host and ":" in host and host[0] != "[": 

407 # IPv6 addresses need to be escaped within square brackets. 

408 host = f"[{host}]" 

409 

410 kwargs["netloc"] = ( 

411 f"{host}:{port}".encode("ascii") 

412 if port is not None 

413 else host.encode("ascii") 

414 ) 

415 

416 if "userinfo" in kwargs or "netloc" in kwargs: 

417 # Consolidate "userinfo" and "netloc" into authority. 

418 userinfo = (kwargs.pop("userinfo", self.userinfo) or b"").decode("ascii") 

419 netloc = (kwargs.pop("netloc", self.netloc) or b"").decode("ascii") 

420 authority = f"{userinfo}@{netloc}" if userinfo else netloc 

421 kwargs["authority"] = authority 

422 

423 # Step 3 

424 # ====== 

425 # 

426 # Wrangle any "path", "query", "raw_path" and "params" keywords into 

427 # "query" and "path" keywords for `rfc3986`. 

428 if "raw_path" in kwargs: 

429 # If "raw_path" is included, then split it into "path" and "query" components. 

430 raw_path = kwargs.pop("raw_path") or b"" 

431 path, has_query, query = raw_path.decode("ascii").partition("?") 

432 kwargs["path"] = path 

433 kwargs["query"] = query if has_query else None 

434 

435 else: 

436 if kwargs.get("path") is not None: 

437 # Ensure `kwargs["path"] = <url quoted str>` for `rfc3986`. 

438 kwargs["path"] = quote(kwargs["path"]) 

439 

440 if kwargs.get("query") is not None: 

441 # Ensure `kwargs["query"] = <str>` for `rfc3986`. 

442 # 

443 # Note that `.copy_with(query=None)` and `.copy_with(query=b"")` 

444 # are subtly different. The `None` style will not include an empty 

445 # trailing "?" character. 

446 kwargs["query"] = kwargs["query"].decode("ascii") 

447 

448 if "params" in kwargs: 

449 # Replace any "params" keyword with the raw "query" instead. 

450 # 

451 # Ensure that empty params use `kwargs["query"] = None` rather 

452 # than `kwargs["query"] = ""`, so that generated URLs do not 

453 # include an empty trailing "?". 

454 params = kwargs.pop("params") 

455 kwargs["query"] = None if not params else str(QueryParams(params)) 

456 

457 # Step 4 

458 # ====== 

459 # 

460 # Ensure any fragment component is quoted. 

461 if kwargs.get("fragment") is not None: 

462 kwargs["fragment"] = quote(kwargs["fragment"]) 

463 

464 # Step 5 

465 # ====== 

466 # 

467 # At this point kwargs may include keys for "scheme", "authority", "path", 

468 # "query" and "fragment". Together these constitute the entire URL. 

469 # 

470 # See https://tools.ietf.org/html/rfc3986#section-3 

471 # 

472 # foo://example.com:8042/over/there?name=ferret#nose 

473 # \_/ \______________/\_________/ \_________/ \__/ 

474 # | | | | | 

475 # scheme authority path query fragment 

476 new_url = URL(self) 

477 new_url._uri_reference = self._uri_reference.copy_with(**kwargs) 

478 if new_url.is_absolute_url: 

479 new_url._uri_reference = new_url._uri_reference.normalize() 

480 return URL(new_url) 

481 

482 def copy_set_param(self, key: str, value: typing.Any = None) -> "URL": 

483 return self.copy_with(params=self.params.set(key, value)) 

484 

485 def copy_add_param(self, key: str, value: typing.Any = None) -> "URL": 

486 return self.copy_with(params=self.params.add(key, value)) 

487 

488 def copy_remove_param(self, key: str) -> "URL": 

489 return self.copy_with(params=self.params.remove(key)) 

490 

491 def copy_merge_params(self, params: QueryParamTypes) -> "URL": 

492 return self.copy_with(params=self.params.merge(params)) 

493 

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

495 """ 

496 Return an absolute URL, using this URL as the base. 

497 

498 Eg. 

499 

500 url = httpx.URL("https://www.example.com/test") 

501 url = url.join("/new/path") 

502 assert url == "https://www.example.com/new/path" 

503 """ 

504 if self.is_relative_url: 

505 # Workaround to handle relative URLs, which otherwise raise 

506 # rfc3986.exceptions.ResolutionError when used as an argument 

507 # in `.resolve_with`. 

508 return ( 

509 self.copy_with(scheme="http", host="example.com") 

510 .join(url) 

511 .copy_with(scheme=None, host=None) 

512 ) 

513 

514 # We drop any fragment portion, because RFC 3986 strictly 

515 # treats URLs with a fragment portion as not being absolute URLs. 

516 base_uri = self._uri_reference.copy_with(fragment=None) 

517 relative_url = URL(url) 

518 return URL(relative_url._uri_reference.resolve_with(base_uri).unsplit()) 

519 

520 def __hash__(self) -> int: 

521 return hash(str(self)) 

522 

523 def __eq__(self, other: typing.Any) -> bool: 

524 return isinstance(other, (URL, str)) and str(self) == str(URL(other)) 

525 

526 def __str__(self) -> str: 

527 return typing.cast(str, self._uri_reference.unsplit()) 

528 

529 def __repr__(self) -> str: 

530 class_name = self.__class__.__name__ 

531 url_str = str(self) 

532 if self._uri_reference.userinfo: 

533 # Mask any password component in the URL representation, to lower the 

534 # risk of unintended leakage, such as in debug information and logging. 

535 username = quote(self.username) 

536 url_str = ( 

537 rfc3986.urlparse(url_str) 

538 .copy_with(userinfo=f"{username}:[secure]") 

539 .unsplit() 

540 ) 

541 return f"{class_name}({url_str!r})" 

542 

543 

544class QueryParams(typing.Mapping[str, str]): 

545 """ 

546 URL query parameters, as a multi-dict. 

547 """ 

548 

549 def __init__( 

550 self, *args: typing.Optional[QueryParamTypes], **kwargs: typing.Any 

551 ) -> None: 

552 assert len(args) < 2, "Too many arguments." 

553 assert not (args and kwargs), "Cannot mix named and unnamed arguments." 

554 

555 value = args[0] if args else kwargs 

556 

557 items: typing.Sequence[typing.Tuple[str, PrimitiveData]] 

558 if value is None or isinstance(value, (str, bytes)): 

559 value = value.decode("ascii") if isinstance(value, bytes) else value 

560 self._dict = parse_qs(value, keep_blank_values=True) 

561 elif isinstance(value, QueryParams): 

562 self._dict = {k: list(v) for k, v in value._dict.items()} 

563 else: 

564 dict_value: typing.Dict[typing.Any, typing.List[typing.Any]] = {} 

565 if isinstance(value, (list, tuple)): 

566 # Convert list inputs like: 

567 # [("a", "123"), ("a", "456"), ("b", "789")] 

568 # To a dict representation, like: 

569 # {"a": ["123", "456"], "b": ["789"]} 

570 for item in value: 

571 dict_value.setdefault(item[0], []).append(item[1]) 

572 else: 

573 # Convert dict inputs like: 

574 # {"a": "123", "b": ["456", "789"]} 

575 # To dict inputs where values are always lists, like: 

576 # {"a": ["123"], "b": ["456", "789"]} 

577 dict_value = { 

578 k: list(v) if isinstance(v, (list, tuple)) else [v] 

579 for k, v in value.items() 

580 } 

581 

582 # Ensure that keys and values are neatly coerced to strings. 

583 # We coerce values `True` and `False` to JSON-like "true" and "false" 

584 # representations, and coerce `None` values to the empty string. 

585 self._dict = { 

586 str(k): [primitive_value_to_str(item) for item in v] 

587 for k, v in dict_value.items() 

588 } 

589 

590 def keys(self) -> typing.KeysView[str]: 

591 """ 

592 Return all the keys in the query params. 

593 

594 Usage: 

595 

596 q = httpx.QueryParams("a=123&a=456&b=789") 

597 assert list(q.keys()) == ["a", "b"] 

598 """ 

599 return self._dict.keys() 

600 

601 def values(self) -> typing.ValuesView[str]: 

602 """ 

603 Return all the values in the query params. If a key occurs more than once 

604 only the first item for that key is returned. 

605 

606 Usage: 

607 

608 q = httpx.QueryParams("a=123&a=456&b=789") 

609 assert list(q.values()) == ["123", "789"] 

610 """ 

611 return {k: v[0] for k, v in self._dict.items()}.values() 

612 

613 def items(self) -> typing.ItemsView[str, str]: 

614 """ 

615 Return all items in the query params. If a key occurs more than once 

616 only the first item for that key is returned. 

617 

618 Usage: 

619 

620 q = httpx.QueryParams("a=123&a=456&b=789") 

621 assert list(q.items()) == [("a", "123"), ("b", "789")] 

622 """ 

623 return {k: v[0] for k, v in self._dict.items()}.items() 

624 

625 def multi_items(self) -> typing.List[typing.Tuple[str, str]]: 

626 """ 

627 Return all items in the query params. Allow duplicate keys to occur. 

628 

629 Usage: 

630 

631 q = httpx.QueryParams("a=123&a=456&b=789") 

632 assert list(q.multi_items()) == [("a", "123"), ("a", "456"), ("b", "789")] 

633 """ 

634 multi_items: typing.List[typing.Tuple[str, str]] = [] 

635 for k, v in self._dict.items(): 

636 multi_items.extend([(k, i) for i in v]) 

637 return multi_items 

638 

639 def get(self, key: typing.Any, default: typing.Any = None) -> typing.Any: 

640 """ 

641 Get a value from the query param for a given key. If the key occurs 

642 more than once, then only the first value is returned. 

643 

644 Usage: 

645 

646 q = httpx.QueryParams("a=123&a=456&b=789") 

647 assert q.get("a") == "123" 

648 """ 

649 if key in self._dict: 

650 return self._dict[str(key)][0] 

651 return default 

652 

653 def get_list(self, key: str) -> typing.List[str]: 

654 """ 

655 Get all values from the query param for a given key. 

656 

657 Usage: 

658 

659 q = httpx.QueryParams("a=123&a=456&b=789") 

660 assert q.get_list("a") == ["123", "456"] 

661 """ 

662 return list(self._dict.get(str(key), [])) 

663 

664 def set(self, key: str, value: typing.Any = None) -> "QueryParams": 

665 """ 

666 Return a new QueryParams instance, setting the value of a key. 

667 

668 Usage: 

669 

670 q = httpx.QueryParams("a=123") 

671 q = q.set("a", "456") 

672 assert q == httpx.QueryParams("a=456") 

673 """ 

674 q = QueryParams() 

675 q._dict = dict(self._dict) 

676 q._dict[str(key)] = [primitive_value_to_str(value)] 

677 return q 

678 

679 def add(self, key: str, value: typing.Any = None) -> "QueryParams": 

680 """ 

681 Return a new QueryParams instance, setting or appending the value of a key. 

682 

683 Usage: 

684 

685 q = httpx.QueryParams("a=123") 

686 q = q.add("a", "456") 

687 assert q == httpx.QueryParams("a=123&a=456") 

688 """ 

689 q = QueryParams() 

690 q._dict = dict(self._dict) 

691 q._dict[str(key)] = q.get_list(key) + [primitive_value_to_str(value)] 

692 return q 

693 

694 def remove(self, key: str) -> "QueryParams": 

695 """ 

696 Return a new QueryParams instance, removing the value of a key. 

697 

698 Usage: 

699 

700 q = httpx.QueryParams("a=123") 

701 q = q.remove("a") 

702 assert q == httpx.QueryParams("") 

703 """ 

704 q = QueryParams() 

705 q._dict = dict(self._dict) 

706 q._dict.pop(str(key), None) 

707 return q 

708 

709 def merge(self, params: typing.Optional[QueryParamTypes] = None) -> "QueryParams": 

710 """ 

711 Return a new QueryParams instance, updated with. 

712 

713 Usage: 

714 

715 q = httpx.QueryParams("a=123") 

716 q = q.merge({"b": "456"}) 

717 assert q == httpx.QueryParams("a=123&b=456") 

718 

719 q = httpx.QueryParams("a=123") 

720 q = q.merge({"a": "456", "b": "789"}) 

721 assert q == httpx.QueryParams("a=456&b=789") 

722 """ 

723 q = QueryParams(params) 

724 q._dict = {**self._dict, **q._dict} 

725 return q 

726 

727 def __getitem__(self, key: typing.Any) -> str: 

728 return self._dict[key][0] 

729 

730 def __contains__(self, key: typing.Any) -> bool: 

731 return key in self._dict 

732 

733 def __iter__(self) -> typing.Iterator[typing.Any]: 

734 return iter(self.keys()) 

735 

736 def __len__(self) -> int: 

737 return len(self._dict) 

738 

739 def __bool__(self) -> bool: 

740 return bool(self._dict) 

741 

742 def __hash__(self) -> int: 

743 return hash(str(self)) 

744 

745 def __eq__(self, other: typing.Any) -> bool: 

746 if not isinstance(other, self.__class__): 

747 return False 

748 return sorted(self.multi_items()) == sorted(other.multi_items()) 

749 

750 def __str__(self) -> str: 

751 return urlencode(self.multi_items()) 

752 

753 def __repr__(self) -> str: 

754 class_name = self.__class__.__name__ 

755 query_string = str(self) 

756 return f"{class_name}({query_string!r})" 

757 

758 def update(self, params: typing.Optional[QueryParamTypes] = None) -> None: 

759 raise RuntimeError( 

760 "QueryParams are immutable since 0.18.0. " 

761 "Use `q = q.merge(...)` to create an updated copy." 

762 ) 

763 

764 def __setitem__(self, key: str, value: str) -> None: 

765 raise RuntimeError( 

766 "QueryParams are immutable since 0.18.0. " 

767 "Use `q = q.set(key, value)` to create an updated copy." 

768 )