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
« 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
4import idna
5import rfc3986
6import rfc3986.exceptions
8from ._exceptions import InvalidURL
9from ._types import PrimitiveData, QueryParamTypes, RawURL, URLTypes
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 normalized to `None`.
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
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.
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.
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 """
73 _uri_reference: rfc3986.URIReference
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
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 )
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)
113 if kwargs:
114 self._uri_reference = self.copy_with(**kwargs)._uri_reference
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 ""
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")
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")
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])
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])
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.
165 Examples:
167 url = httpx.URL("http://www.EXAMPLE.org")
168 assert url.host == "www.example.org"
170 url = httpx.URL("http://中国.icom.museum")
171 assert url.host == "中国.icom.museum"
173 url = httpx.URL("http://xn--fiqs8s.icom.museum")
174 assert url.host == "中国.icom.museum"
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 ""
181 if host and ":" in host and host[0] == "[":
182 # it's an IPv6 address
183 host = host.lstrip("[").rstrip("]")
185 if host.startswith("xn--"):
186 host = idna.decode(host)
188 return host
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.
196 Examples:
198 url = httpx.URL("http://www.EXAMPLE.org")
199 assert url.raw_host == b"www.example.org"
201 url = httpx.URL("http://中国.icom.museum")
202 assert url.raw_host == b"xn--fiqs8s.icom.museum"
204 url = httpx.URL("http://xn--fiqs8s.icom.museum")
205 assert url.raw_host == b"xn--fiqs8s.icom.museum"
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 ""
212 if host and ":" in host and host[0] == "[":
213 # it's an IPv6 address
214 host = host.lstrip("[").rstrip("]")
216 return host.encode("ascii")
218 @property
219 def port(self) -> typing.Optional[int]:
220 """
221 The URL port as an integer.
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`.
227 For example:
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
235 @property
236 def netloc(self) -> bytes:
237 """
238 Either `<host>` or `<host>:<port>` as bytes.
239 Always normalized to lowercase, and IDNA encoded.
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
251 @property
252 def path(self) -> str:
253 """
254 The URL path as a string. Excluding the query string, and URL decoded.
256 For example:
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)
264 @property
265 def query(self) -> bytes:
266 """
267 The URL query string, as raw bytes, excluding the leading b"?".
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.
273 For example:
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")
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)
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.
295 For example:
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")
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 "")
314 @property
315 def raw(self) -> RawURL:
316 """
317 Provides the (scheme, host, port, target) for the outgoing request.
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 )
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)
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
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.
355 For example:
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 }
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)
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")
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)
406 if host and ":" in host and host[0] != "[":
407 # IPv6 addresses need to be escaped within square brackets.
408 host = f"[{host}]"
410 kwargs["netloc"] = (
411 f"{host}:{port}".encode("ascii")
412 if port is not None
413 else host.encode("ascii")
414 )
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
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
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"])
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")
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))
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"])
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)
482 def copy_set_param(self, key: str, value: typing.Any = None) -> "URL":
483 return self.copy_with(params=self.params.set(key, value))
485 def copy_add_param(self, key: str, value: typing.Any = None) -> "URL":
486 return self.copy_with(params=self.params.add(key, value))
488 def copy_remove_param(self, key: str) -> "URL":
489 return self.copy_with(params=self.params.remove(key))
491 def copy_merge_params(self, params: QueryParamTypes) -> "URL":
492 return self.copy_with(params=self.params.merge(params))
494 def join(self, url: URLTypes) -> "URL":
495 """
496 Return an absolute URL, using this URL as the base.
498 Eg.
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 )
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())
520 def __hash__(self) -> int:
521 return hash(str(self))
523 def __eq__(self, other: typing.Any) -> bool:
524 return isinstance(other, (URL, str)) and str(self) == str(URL(other))
526 def __str__(self) -> str:
527 return typing.cast(str, self._uri_reference.unsplit())
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})"
544class QueryParams(typing.Mapping[str, str]):
545 """
546 URL query parameters, as a multi-dict.
547 """
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."
555 value = args[0] if args else kwargs
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 }
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 }
590 def keys(self) -> typing.KeysView[str]:
591 """
592 Return all the keys in the query params.
594 Usage:
596 q = httpx.QueryParams("a=123&a=456&b=789")
597 assert list(q.keys()) == ["a", "b"]
598 """
599 return self._dict.keys()
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.
606 Usage:
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()
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.
618 Usage:
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()
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.
629 Usage:
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
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.
644 Usage:
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
653 def get_list(self, key: str) -> typing.List[str]:
654 """
655 Get all values from the query param for a given key.
657 Usage:
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), []))
664 def set(self, key: str, value: typing.Any = None) -> "QueryParams":
665 """
666 Return a new QueryParams instance, setting the value of a key.
668 Usage:
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
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.
683 Usage:
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
694 def remove(self, key: str) -> "QueryParams":
695 """
696 Return a new QueryParams instance, removing the value of a key.
698 Usage:
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
709 def merge(self, params: typing.Optional[QueryParamTypes] = None) -> "QueryParams":
710 """
711 Return a new QueryParams instance, updated with.
713 Usage:
715 q = httpx.QueryParams("a=123")
716 q = q.merge({"b": "456"})
717 assert q == httpx.QueryParams("a=123&b=456")
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
727 def __getitem__(self, key: typing.Any) -> str:
728 return self._dict[key][0]
730 def __contains__(self, key: typing.Any) -> bool:
731 return key in self._dict
733 def __iter__(self) -> typing.Iterator[typing.Any]:
734 return iter(self.keys())
736 def __len__(self) -> int:
737 return len(self._dict)
739 def __bool__(self) -> bool:
740 return bool(self._dict)
742 def __hash__(self) -> int:
743 return hash(str(self))
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())
750 def __str__(self) -> str:
751 return urlencode(self.multi_items())
753 def __repr__(self) -> str:
754 class_name = self.__class__.__name__
755 query_string = str(self)
756 return f"{class_name}({query_string!r})"
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 )
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 )