Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/httpx/_urls.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

195 statements  

1from __future__ import annotations 

2 

3import typing 

4from urllib.parse import parse_qs, unquote 

5 

6import idna 

7 

8from ._types import QueryParamTypes, RawURL, URLTypes 

9from ._urlparse import urlencode, urlparse 

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 

57 normalized to `None`. 

58 

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

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

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

62 

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

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

65 

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

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

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

69 

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

71 only be properly URL escaped when decoding the parameter names and values 

72 themselves. 

73 """ 

74 

75 def __init__(self, url: URL | str = "", **kwargs: typing.Any) -> None: 

76 if kwargs: 

77 allowed = { 

78 "scheme": str, 

79 "username": str, 

80 "password": str, 

81 "userinfo": bytes, 

82 "host": str, 

83 "port": int, 

84 "netloc": bytes, 

85 "path": str, 

86 "query": bytes, 

87 "raw_path": bytes, 

88 "fragment": str, 

89 "params": object, 

90 } 

91 

92 # Perform type checking for all supported keyword arguments. 

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

94 if key not in allowed: 

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

96 raise TypeError(message) 

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

98 expected = allowed[key].__name__ 

99 seen = type(value).__name__ 

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

101 raise TypeError(message) 

102 if isinstance(value, bytes): 

103 kwargs[key] = value.decode("ascii") 

104 

105 if "params" in kwargs: 

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

107 # 

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

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

110 # include an empty trailing "?". 

111 params = kwargs.pop("params") 

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

113 

114 if isinstance(url, str): 

115 self._uri_reference = urlparse(url, **kwargs) 

116 elif isinstance(url, URL): 

117 self._uri_reference = url._uri_reference.copy_with(**kwargs) 

118 else: 

119 raise TypeError( 

120 "Invalid type for url. Expected str or httpx.URL," 

121 f" got {type(url)}: {url!r}" 

122 ) 

123 

124 @property 

125 def scheme(self) -> str: 

126 """ 

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

128 Always normalised to lowercase. 

129 """ 

130 return self._uri_reference.scheme 

131 

132 @property 

133 def raw_scheme(self) -> bytes: 

134 """ 

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

136 Always normalised to lowercase. 

137 """ 

138 return self._uri_reference.scheme.encode("ascii") 

139 

140 @property 

141 def userinfo(self) -> bytes: 

142 """ 

143 The URL userinfo as a raw bytestring. 

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

145 """ 

146 return self._uri_reference.userinfo.encode("ascii") 

147 

148 @property 

149 def username(self) -> str: 

150 """ 

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

152 For example: "jo@email.com" 

153 """ 

154 userinfo = self._uri_reference.userinfo 

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

156 

157 @property 

158 def password(self) -> str: 

159 """ 

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

161 For example: "a secret" 

162 """ 

163 userinfo = self._uri_reference.userinfo 

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

165 

166 @property 

167 def host(self) -> str: 

168 """ 

169 The URL host as a string. 

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

171 

172 Examples: 

173 

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

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

176 

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

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

179 

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

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

182 

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

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

185 """ 

186 host: str = self._uri_reference.host 

187 

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

189 host = idna.decode(host) 

190 

191 return host 

192 

193 @property 

194 def raw_host(self) -> bytes: 

195 """ 

196 The raw bytes representation of the URL host. 

197 Always normalized to lowercase, and IDNA encoded. 

198 

199 Examples: 

200 

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

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

203 

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

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

206 

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

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

209 

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

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

212 """ 

213 return self._uri_reference.host.encode("ascii") 

214 

215 @property 

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

217 """ 

218 The URL port as an integer. 

219 

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

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

222 treated as `None`. 

223 

224 For example: 

225 

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

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

228 """ 

229 return self._uri_reference.port 

230 

231 @property 

232 def netloc(self) -> bytes: 

233 """ 

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

235 Always normalized to lowercase, and IDNA encoded. 

236 

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

238 "Host" header. 

239 """ 

240 return self._uri_reference.netloc.encode("ascii") 

241 

242 @property 

243 def path(self) -> str: 

244 """ 

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

246 

247 For example: 

248 

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

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

251 """ 

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

253 return unquote(path) 

254 

255 @property 

256 def query(self) -> bytes: 

257 """ 

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

259 

260 This is necessarily a bytewise interface, because we cannot 

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

262 the keys and values into a QueryParams instance. 

263 

264 For example: 

265 

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

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

268 """ 

269 query = self._uri_reference.query or "" 

270 return query.encode("ascii") 

271 

272 @property 

273 def params(self) -> QueryParams: 

274 """ 

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

276 multidict representation. 

277 """ 

278 return QueryParams(self._uri_reference.query) 

279 

280 @property 

281 def raw_path(self) -> bytes: 

282 """ 

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

284 Used as the target when constructing HTTP requests. 

285 

286 For example: 

287 

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

289 Host: www.example.org 

290 Connection: close 

291 """ 

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

293 if self._uri_reference.query is not None: 

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

295 return path.encode("ascii") 

296 

297 @property 

298 def fragment(self) -> str: 

299 """ 

300 The URL fragments, as used in HTML anchors. 

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

302 """ 

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

304 

305 @property 

306 def raw(self) -> RawURL: 

307 """ 

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

309 

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

311 We no longer use `RawURL`, and this property will be deprecated 

312 in a future release. 

313 """ 

314 return RawURL( 

315 self.raw_scheme, 

316 self.raw_host, 

317 self.port, 

318 self.raw_path, 

319 ) 

320 

321 @property 

322 def is_absolute_url(self) -> bool: 

323 """ 

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

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

326 """ 

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

328 # URLs with a fragment portion as not absolute. 

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

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

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

332 

333 @property 

334 def is_relative_url(self) -> bool: 

335 """ 

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

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

338 """ 

339 return not self.is_absolute_url 

340 

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

342 """ 

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

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

345 available via properties on the `URL` class. 

346 

347 For example: 

348 

349 url = httpx.URL("https://www.example.com").copy_with( 

350 username="jo@gmail.com", password="a secret" 

351 ) 

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

353 """ 

354 return URL(self, **kwargs) 

355 

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

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

358 

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

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

361 

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

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

364 

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

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

367 

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

369 """ 

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

371 

372 Eg. 

373 

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

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

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

377 """ 

378 from urllib.parse import urljoin 

379 

380 return URL(urljoin(str(self), str(URL(url)))) 

381 

382 def __hash__(self) -> int: 

383 return hash(str(self)) 

384 

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

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

387 

388 def __str__(self) -> str: 

389 return str(self._uri_reference) 

390 

391 def __repr__(self) -> str: 

392 scheme, userinfo, host, port, path, query, fragment = self._uri_reference 

393 

394 if ":" in userinfo: 

395 # Mask any password component. 

396 userinfo = f'{userinfo.split(":")[0]}:[secure]' 

397 

398 authority = "".join( 

399 [ 

400 f"{userinfo}@" if userinfo else "", 

401 f"[{host}]" if ":" in host else host, 

402 f":{port}" if port is not None else "", 

403 ] 

404 ) 

405 url = "".join( 

406 [ 

407 f"{self.scheme}:" if scheme else "", 

408 f"//{authority}" if authority else "", 

409 path, 

410 f"?{query}" if query is not None else "", 

411 f"#{fragment}" if fragment is not None else "", 

412 ] 

413 ) 

414 

415 return f"{self.__class__.__name__}({url!r})" 

416 

417 

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

419 """ 

420 URL query parameters, as a multi-dict. 

421 """ 

422 

423 def __init__(self, *args: QueryParamTypes | None, **kwargs: typing.Any) -> None: 

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

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

426 

427 value = args[0] if args else kwargs 

428 

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

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

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

432 elif isinstance(value, QueryParams): 

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

434 else: 

435 dict_value: dict[typing.Any, list[typing.Any]] = {} 

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

437 # Convert list inputs like: 

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

439 # To a dict representation, like: 

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

441 for item in value: 

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

443 else: 

444 # Convert dict inputs like: 

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

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

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

448 dict_value = { 

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

450 for k, v in value.items() 

451 } 

452 

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

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

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

456 self._dict = { 

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

458 for k, v in dict_value.items() 

459 } 

460 

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

462 """ 

463 Return all the keys in the query params. 

464 

465 Usage: 

466 

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

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

469 """ 

470 return self._dict.keys() 

471 

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

473 """ 

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

475 only the first item for that key is returned. 

476 

477 Usage: 

478 

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

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

481 """ 

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

483 

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

485 """ 

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

487 only the first item for that key is returned. 

488 

489 Usage: 

490 

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

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

493 """ 

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

495 

496 def multi_items(self) -> list[tuple[str, str]]: 

497 """ 

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

499 

500 Usage: 

501 

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

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

504 """ 

505 multi_items: list[tuple[str, str]] = [] 

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

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

508 return multi_items 

509 

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

511 """ 

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

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

514 

515 Usage: 

516 

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

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

519 """ 

520 if key in self._dict: 

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

522 return default 

523 

524 def get_list(self, key: str) -> list[str]: 

525 """ 

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

527 

528 Usage: 

529 

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

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

532 """ 

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

534 

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

536 """ 

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

538 

539 Usage: 

540 

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

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

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

544 """ 

545 q = QueryParams() 

546 q._dict = dict(self._dict) 

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

548 return q 

549 

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

551 """ 

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

553 

554 Usage: 

555 

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

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

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

559 """ 

560 q = QueryParams() 

561 q._dict = dict(self._dict) 

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

563 return q 

564 

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

566 """ 

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

568 

569 Usage: 

570 

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

572 q = q.remove("a") 

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

574 """ 

575 q = QueryParams() 

576 q._dict = dict(self._dict) 

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

578 return q 

579 

580 def merge(self, params: QueryParamTypes | None = None) -> QueryParams: 

581 """ 

582 Return a new QueryParams instance, updated with. 

583 

584 Usage: 

585 

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

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

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

589 

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

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

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

593 """ 

594 q = QueryParams(params) 

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

596 return q 

597 

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

599 return self._dict[key][0] 

600 

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

602 return key in self._dict 

603 

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

605 return iter(self.keys()) 

606 

607 def __len__(self) -> int: 

608 return len(self._dict) 

609 

610 def __bool__(self) -> bool: 

611 return bool(self._dict) 

612 

613 def __hash__(self) -> int: 

614 return hash(str(self)) 

615 

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

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

618 return False 

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

620 

621 def __str__(self) -> str: 

622 """ 

623 Note that we use '%20' encoding for spaces, and treat '/' as a safe 

624 character. 

625 

626 See https://github.com/encode/httpx/issues/2536 and 

627 https://docs.python.org/3/library/urllib.parse.html#urllib.parse.urlencode 

628 """ 

629 return urlencode(self.multi_items()) 

630 

631 def __repr__(self) -> str: 

632 class_name = self.__class__.__name__ 

633 query_string = str(self) 

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

635 

636 def update(self, params: QueryParamTypes | None = None) -> None: 

637 raise RuntimeError( 

638 "QueryParams are immutable since 0.18.0. " 

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

640 ) 

641 

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

643 raise RuntimeError( 

644 "QueryParams are immutable since 0.18.0. " 

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

646 )