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
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
1from __future__ import annotations
3import typing
4from urllib.parse import parse_qs, unquote
6import idna
8from ._types import QueryParamTypes, RawURL, URLTypes
9from ._urlparse import urlencode, urlparse
10from ._utils import primitive_value_to_str
13class URL:
14 """
15 url = httpx.URL("HTTPS://jo%40email.com:a%20secret@müller.de:1234/pa%20th?search=ab#anchorlink")
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"
30 The components of a URL are broken down like this:
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 ]
36 Note that:
38 * `url.scheme` is normalized to always be lowercased.
40 * `url.host` is normalized to always be lowercased. Internationalized domain
41 names are represented in unicode, without IDNA encoding applied. For instance:
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"
48 * `url.raw_host` is normalized to always be lowercased, and is IDNA encoded.
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"
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`.
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
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.
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.
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 """
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 }
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")
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))
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 )
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
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")
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")
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])
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])
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.
172 Examples:
174 url = httpx.URL("http://www.EXAMPLE.org")
175 assert url.host == "www.example.org"
177 url = httpx.URL("http://中国.icom.museum")
178 assert url.host == "中国.icom.museum"
180 url = httpx.URL("http://xn--fiqs8s.icom.museum")
181 assert url.host == "中国.icom.museum"
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
188 if host.startswith("xn--"):
189 host = idna.decode(host)
191 return host
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.
199 Examples:
201 url = httpx.URL("http://www.EXAMPLE.org")
202 assert url.raw_host == b"www.example.org"
204 url = httpx.URL("http://中国.icom.museum")
205 assert url.raw_host == b"xn--fiqs8s.icom.museum"
207 url = httpx.URL("http://xn--fiqs8s.icom.museum")
208 assert url.raw_host == b"xn--fiqs8s.icom.museum"
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")
215 @property
216 def port(self) -> int | None:
217 """
218 The URL port as an integer.
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`.
224 For example:
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
231 @property
232 def netloc(self) -> bytes:
233 """
234 Either `<host>` or `<host>:<port>` as bytes.
235 Always normalized to lowercase, and IDNA encoded.
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")
242 @property
243 def path(self) -> str:
244 """
245 The URL path as a string. Excluding the query string, and URL decoded.
247 For example:
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)
255 @property
256 def query(self) -> bytes:
257 """
258 The URL query string, as raw bytes, excluding the leading b"?".
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.
264 For example:
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")
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)
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.
286 For example:
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")
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 "")
305 @property
306 def raw(self) -> RawURL:
307 """
308 Provides the (scheme, host, port, target) for the outgoing request.
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 )
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)
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
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.
347 For example:
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)
356 def copy_set_param(self, key: str, value: typing.Any = None) -> URL:
357 return self.copy_with(params=self.params.set(key, value))
359 def copy_add_param(self, key: str, value: typing.Any = None) -> URL:
360 return self.copy_with(params=self.params.add(key, value))
362 def copy_remove_param(self, key: str) -> URL:
363 return self.copy_with(params=self.params.remove(key))
365 def copy_merge_params(self, params: QueryParamTypes) -> URL:
366 return self.copy_with(params=self.params.merge(params))
368 def join(self, url: URLTypes) -> URL:
369 """
370 Return an absolute URL, using this URL as the base.
372 Eg.
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
380 return URL(urljoin(str(self), str(URL(url))))
382 def __hash__(self) -> int:
383 return hash(str(self))
385 def __eq__(self, other: typing.Any) -> bool:
386 return isinstance(other, (URL, str)) and str(self) == str(URL(other))
388 def __str__(self) -> str:
389 return str(self._uri_reference)
391 def __repr__(self) -> str:
392 scheme, userinfo, host, port, path, query, fragment = self._uri_reference
394 if ":" in userinfo:
395 # Mask any password component.
396 userinfo = f'{userinfo.split(":")[0]}:[secure]'
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 )
415 return f"{self.__class__.__name__}({url!r})"
418class QueryParams(typing.Mapping[str, str]):
419 """
420 URL query parameters, as a multi-dict.
421 """
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."
427 value = args[0] if args else kwargs
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 }
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 }
461 def keys(self) -> typing.KeysView[str]:
462 """
463 Return all the keys in the query params.
465 Usage:
467 q = httpx.QueryParams("a=123&a=456&b=789")
468 assert list(q.keys()) == ["a", "b"]
469 """
470 return self._dict.keys()
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.
477 Usage:
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()
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.
489 Usage:
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()
496 def multi_items(self) -> list[tuple[str, str]]:
497 """
498 Return all items in the query params. Allow duplicate keys to occur.
500 Usage:
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
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.
515 Usage:
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
524 def get_list(self, key: str) -> list[str]:
525 """
526 Get all values from the query param for a given key.
528 Usage:
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), []))
535 def set(self, key: str, value: typing.Any = None) -> QueryParams:
536 """
537 Return a new QueryParams instance, setting the value of a key.
539 Usage:
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
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.
554 Usage:
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
565 def remove(self, key: str) -> QueryParams:
566 """
567 Return a new QueryParams instance, removing the value of a key.
569 Usage:
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
580 def merge(self, params: QueryParamTypes | None = None) -> QueryParams:
581 """
582 Return a new QueryParams instance, updated with.
584 Usage:
586 q = httpx.QueryParams("a=123")
587 q = q.merge({"b": "456"})
588 assert q == httpx.QueryParams("a=123&b=456")
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
598 def __getitem__(self, key: typing.Any) -> str:
599 return self._dict[key][0]
601 def __contains__(self, key: typing.Any) -> bool:
602 return key in self._dict
604 def __iter__(self) -> typing.Iterator[typing.Any]:
605 return iter(self.keys())
607 def __len__(self) -> int:
608 return len(self._dict)
610 def __bool__(self) -> bool:
611 return bool(self._dict)
613 def __hash__(self) -> int:
614 return hash(str(self))
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())
621 def __str__(self) -> str:
622 """
623 Note that we use '%20' encoding for spaces, and treat '/' as a safe
624 character.
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())
631 def __repr__(self) -> str:
632 class_name = self.__class__.__name__
633 query_string = str(self)
634 return f"{class_name}({query_string!r})"
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 )
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 )