Voice et bot modif

This commit is contained in:
pi 2026-06-16 17:09:34 +00:00
parent 189d56026b
commit 7333a22bcd
10774 changed files with 634644 additions and 933308 deletions

View file

@ -1,7 +1,7 @@
from ._query import Query, QueryVariable, SimpleQuery
from ._url import URL, cache_clear, cache_configure, cache_info
__version__ = "1.22.0"
__version__ = "1.24.2"
__all__ = (
"URL",

View file

@ -3,7 +3,6 @@
import re
import unicodedata
from functools import lru_cache
from typing import Union
from urllib.parse import scheme_chars, uses_netloc
from ._quoters import QUOTER, UNQUOTER_PLUS
@ -58,6 +57,14 @@ def split_url(url: str) -> SplitURLType:
delim = wdelim # use earliest delim position
netloc = url[2:delim]
url = url[delim:]
# Backslash is not valid in the authority component per RFC 3986.
# WHATWG parsers treat \ as a path separator for special schemes, so
# accepting it in the authority can cause host parsing ambiguity.
if "\\" in netloc:
raise ValueError(
"Invalid URL: backslash ('\\') is not allowed in the authority "
"component per RFC 3986."
)
has_left_bracket = "[" in netloc
has_right_bracket = "]" in netloc
if (has_left_bracket and not has_right_bracket) or (
@ -65,7 +72,22 @@ def split_url(url: str) -> SplitURLType:
):
raise ValueError("Invalid IPv6 URL")
if has_left_bracket:
bracketed_host = netloc.partition("[")[2].partition("]")[0]
# Per RFC 3986, brackets are only valid at the START of the host
# for IP-literal addresses. Text before '[' (e.g. '127.0.0.1[::1]')
# is invalid and must be rejected to prevent SSRF bypasses. The
# count checks reject URLs with more than one bracket pair in the
# host subcomponent (e.g. 'http://[:localhost[]].google:80'),
# which would otherwise resolve to an unintended host.
hostinfo = netloc.rpartition("@")[2]
if hostinfo[0] != "[" or hostinfo.count("[") > 1 or hostinfo.count("]") > 1:
raise ValueError("Invalid IPv6 URL")
bracketed_host, _, after_bracket = hostinfo[1:].partition("]")
# Per RFC 3986 §3.2.2, after the closing ']' of an IP-literal
# only ":" <port> or end-of-authority is valid. Any other text
# (e.g. '[::1]allowed.example:1') must be rejected to prevent
# host-confusion where the suffix is silently dropped.
if after_bracket and after_bracket[0] != ":":
raise ValueError("Invalid IPv6 URL")
# Valid bracketed hosts are defined in
# https://www.rfc-editor.org/rfc/rfc3986#page-49
# https://url.spec.whatwg.org/
@ -108,11 +130,11 @@ def _check_netloc(netloc: str) -> None:
@lru_cache # match the same size as urlsplit
def split_netloc(
netloc: str,
) -> tuple[Union[str, None], Union[str, None], Union[str, None], Union[int, None]]:
) -> tuple[str | None, str | None, str | None, int | None]:
"""Split netloc into username, password, host and port."""
if "@" not in netloc:
username: Union[str, None] = None
password: Union[str, None] = None
username: str | None = None
password: str | None = None
hostinfo = netloc
else:
userinfo, _, hostinfo = netloc.rpartition("@")
@ -121,8 +143,15 @@ def split_netloc(
password = None
if "[" in hostinfo:
if hostinfo[0] != "[" or hostinfo.count("[") > 1 or hostinfo.count("]") > 1:
raise ValueError("Invalid IPv6 URL")
_, _, bracketed = hostinfo.partition("[")
hostname, _, port_str = bracketed.partition("]")
# Defense-in-depth: after ']' only ':port' or empty is valid.
# split_url() should have already rejected invalid suffixes,
# but guard here too for callers that use split_netloc() directly.
if port_str and port_str[0] != ":":
raise ValueError("Invalid IPv6 URL")
_, _, port_str = port_str.partition(":")
else:
hostname, _, port_str = hostinfo.partition(":")
@ -157,10 +186,10 @@ def unsplit_result(
@lru_cache # match the same size as urlsplit
def make_netloc(
user: Union[str, None],
password: Union[str, None],
host: Union[str, None],
port: Union[int, None],
user: str | None,
password: str | None,
host: str | None,
port: int | None,
encode: bool = False,
) -> str:
"""Make netloc from parts.

View file

@ -16,7 +16,13 @@ Query = Union[
def query_var(v: SimpleQuery) -> str:
"""Convert a query variable to a string."""
"""Convert a query variable to a string.
Note: Objects implementing the ``__int__`` data model method (typed as
``SupportsInt``; e.g. ``uuid.UUID``) are converted via ``int()`` first.
Callers should convert such values to ``str`` explicitly if the string
representation is desired.
"""
cls = type(v)
if cls is int: # Fast path for non-subclassed int
return str(v)
@ -38,7 +44,7 @@ def query_var(v: SimpleQuery) -> str:
def get_str_query_from_sequence_iterable(
items: Iterable[tuple[Union[str, istr], QueryVariable]],
items: Iterable[tuple[str | istr, QueryVariable]],
) -> str:
"""Return a query string from a sequence of (key, value) pairs.
@ -58,7 +64,7 @@ def get_str_query_from_sequence_iterable(
def get_str_query_from_iterable(
items: Iterable[tuple[Union[str, istr], SimpleQuery]],
items: Iterable[tuple[str | istr, SimpleQuery]],
) -> str:
"""Return a query string from an iterable.
@ -76,14 +82,14 @@ def get_str_query_from_iterable(
return "&".join(pairs)
def get_str_query(*args: Any, **kwargs: Any) -> Union[str, None]:
def get_str_query(*args: Any, **kwargs: Any) -> str | None:
"""Return a query string from supported args."""
query: Union[
str,
Mapping[str, QueryVariable],
Sequence[tuple[Union[str, istr], SimpleQuery]],
None,
]
query: (
str
| Mapping[str, QueryVariable]
| Sequence[tuple[str | istr, SimpleQuery]]
| None
)
if kwargs:
if args:
msg = "Either kwargs or single query parameter must be present"

View file

@ -1,6 +1,5 @@
"""Quoting and unquoting utilities for URL parts."""
from typing import Union
from urllib.parse import quote
from ._quoting import _Quoter, _Unquoter
@ -22,7 +21,7 @@ QS_UNQUOTER = _Unquoter(qs=True)
UNQUOTER_PLUS = _Unquoter(plus=True) # to match urllib.parse.unquote_plus
def human_quote(s: Union[str, None], unsafe: str) -> Union[str, None]:
def human_quote(s: str | None, unsafe: str) -> str | None:
if not s:
return s
for c in "%" + unsafe:

View file

@ -1,7 +1,7 @@
import codecs
import re
from string import ascii_letters, ascii_lowercase, digits
from typing import Union, overload
from typing import overload
BASCII_LOWERCASE = ascii_lowercase.encode("ascii")
BPCT_ALLOWED = {f"%{i:02X}".encode("ascii") for i in range(256)}
@ -37,7 +37,7 @@ class _Quoter:
def __call__(self, val: str) -> str: ...
@overload
def __call__(self, val: None) -> None: ...
def __call__(self, val: Union[str, None]) -> Union[str, None]:
def __call__(self, val: str | None) -> str | None:
if val is None:
return None
if not isinstance(val, str):
@ -138,7 +138,7 @@ class _Unquoter:
def __call__(self, val: str) -> str: ...
@overload
def __call__(self, val: None) -> None: ...
def __call__(self, val: Union[str, None]) -> Union[str, None]:
def __call__(self, val: str | None) -> str | None:
if val is None:
return None
if not isinstance(val, str):

View file

@ -4,6 +4,7 @@ import warnings
from collections.abc import Mapping, Sequence
from enum import Enum
from functools import _CacheInfo, lru_cache
from importlib.util import find_spec
from ipaddress import ip_address
from typing import (
TYPE_CHECKING,
@ -15,7 +16,7 @@ from typing import (
cast,
overload,
)
from urllib.parse import SplitResult, uses_relative
from urllib.parse import SplitResult, scheme_chars, uses_relative
import idna
from multidict import MultiDict, MultiDictProxy, istr
@ -55,8 +56,17 @@ from ._quoters import (
human_quote,
)
# Avoid Pydantic import if not used (increases yarl's import time by 3-7x).
HAS_PYDANTIC = find_spec("pydantic_core") is not None
if TYPE_CHECKING:
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema
DEFAULT_PORTS = {"http": 80, "https": 443, "ws": 80, "wss": 443, "ftp": 21}
USES_RELATIVE = frozenset(uses_relative)
_SCHEME_CHARS = frozenset(scheme_chars)
# Special schemes https://url.spec.whatwg.org/#special-scheme
# are not allowed to have an empty host https://url.spec.whatwg.org/#url-representation
@ -114,16 +124,16 @@ class _InternalURLCache(TypedDict, total=False):
scheme: str
raw_authority: str
authority: str
raw_user: Union[str, None]
user: Union[str, None]
raw_password: Union[str, None]
password: Union[str, None]
raw_host: Union[str, None]
host: Union[str, None]
host_subcomponent: Union[str, None]
host_port_subcomponent: Union[str, None]
port: Union[int, None]
explicit_port: Union[int, None]
raw_user: str | None
user: str | None
raw_password: str | None
password: str | None
raw_host: str | None
host: str | None
host_subcomponent: str | None
host_port_subcomponent: str | None
port: int | None
explicit_port: int | None
raw_path: str
path: str
_parsed_query: list[tuple[str, str]]
@ -150,11 +160,22 @@ def rewrite_module(obj: _T) -> _T:
return obj
def _encode_relative_scheme_colon(path: str) -> str:
"""Re-encode a scheme-shaped leading ``:`` in a relative path to ``%3A``."""
colon_pos = path.find(":")
if colon_pos <= 0:
return path
for c in path[:colon_pos]:
if c not in _SCHEME_CHARS:
return path
return path[:colon_pos] + "%3A" + path[colon_pos + 1 :]
@lru_cache
def encode_url(url_str: str) -> "URL":
"""Parse unencoded URL."""
cache: _InternalURLCache = {}
host: Union[str, None]
host: str | None
scheme, netloc, path, query, fragment = split_url(url_str)
if not netloc: # netloc
host = ""
@ -194,6 +215,8 @@ def encode_url(url_str: str) -> "URL":
path = PATH_REQUOTER(path)
if netloc and "." in path:
path = normalize_path(path)
elif not scheme and not netloc:
path = _encode_relative_scheme_colon(path)
if query:
query = QUERY_REQUOTER(query)
if fragment:
@ -228,10 +251,10 @@ def pre_encoded_url(url_str: str) -> "URL":
def build_pre_encoded_url(
scheme: str,
authority: str,
user: Union[str, None],
password: Union[str, None],
user: str | None,
password: str | None,
host: str,
port: Union[int, None],
port: int | None,
path: str,
query_string: str,
fragment: str,
@ -359,7 +382,7 @@ class URL:
val: Union[str, SplitResult, "URL", UndefinedType] = UNDEFINED,
*,
encoded: bool = False,
strict: Union[bool, None] = None,
strict: bool | None = None,
) -> "URL":
if strict is not None: # pragma: no cover
warnings.warn("strict parameter is ignored")
@ -389,12 +412,12 @@ class URL:
*,
scheme: str = "",
authority: str = "",
user: Union[str, None] = None,
password: Union[str, None] = None,
user: str | None = None,
password: str | None = None,
host: str = "",
port: Union[int, None] = None,
port: int | None = None,
path: str = "",
query: Union[Query, None] = None,
query: Query | None = None,
query_string: str = "",
fragment: str = "",
encoded: bool = False,
@ -442,7 +465,7 @@ class URL:
self = object.__new__(URL)
self._scheme = scheme
_host: Union[str, None] = None
_host: str | None = None
if authority:
user, password, _host, port = split_netloc(authority)
_host = _encode_host(_host, validate_host=False) if _host else ""
@ -547,7 +570,7 @@ class URL:
def __truediv__(self, name: str) -> "URL":
if not isinstance(name, str):
return NotImplemented # type: ignore[unreachable]
return NotImplemented
return self._make_child((str(name),))
def __mod__(self, query: Query) -> "URL":
@ -556,11 +579,18 @@ class URL:
def __bool__(self) -> bool:
return bool(self._netloc or self._path or self._query or self._fragment)
def __getstate__(self) -> tuple[SplitResult]:
return (tuple.__new__(SplitResult, self._val),)
def __getstate__(self) -> tuple[SplitURLType]:
# Return a plain tuple rather than a ``SplitResult``. Constructing a
# ``SplitResult`` via ``tuple.__new__`` skips its ``__init__`` and on
# Python 3.15+ leaves ``_keep_empty`` unset, which breaks pickling: the
# new ``SplitResult.__getstate__`` indexes a state that ends up as
# ``None`` (gh-1632). ``__setstate__`` already unpacks both shapes, so
# pickles produced by older yarl releases (which embed a real
# ``SplitResult``) still load correctly.
return (self._val,)
def __setstate__(
self, state: Union[tuple[SplitURLType], tuple[None, _InternalURLCache]]
self, state: tuple[SplitURLType] | tuple[None, _InternalURLCache]
) -> None:
if state[0] is None and isinstance(state[1], dict):
# default style pickle
@ -687,7 +717,7 @@ class URL:
return make_netloc(self.user, self.password, self.host, self.port)
@cached_property
def raw_user(self) -> Union[str, None]:
def raw_user(self) -> str | None:
"""Encoded user part of URL.
None if user is missing.
@ -698,7 +728,7 @@ class URL:
return self._cache["raw_user"]
@cached_property
def user(self) -> Union[str, None]:
def user(self) -> str | None:
"""Decoded user part of URL.
None if user is missing.
@ -709,7 +739,7 @@ class URL:
return UNQUOTER(raw_user)
@cached_property
def raw_password(self) -> Union[str, None]:
def raw_password(self) -> str | None:
"""Encoded password part of URL.
None if password is missing.
@ -719,7 +749,7 @@ class URL:
return self._cache["raw_password"]
@cached_property
def password(self) -> Union[str, None]:
def password(self) -> str | None:
"""Decoded password part of URL.
None if password is missing.
@ -730,7 +760,7 @@ class URL:
return UNQUOTER(raw_password)
@cached_property
def raw_host(self) -> Union[str, None]:
def raw_host(self) -> str | None:
"""Encoded host part of URL.
None for relative URLs.
@ -744,7 +774,7 @@ class URL:
return self._cache["raw_host"]
@cached_property
def host(self) -> Union[str, None]:
def host(self) -> str | None:
"""Decoded host part of URL.
None for relative URLs.
@ -758,7 +788,7 @@ class URL:
return _idna_decode(raw)
@cached_property
def host_subcomponent(self) -> Union[str, None]:
def host_subcomponent(self) -> str | None:
"""Return the host subcomponent part of URL.
None for relative URLs.
@ -780,7 +810,7 @@ class URL:
return f"[{raw}]" if ":" in raw else raw
@cached_property
def host_port_subcomponent(self) -> Union[str, None]:
def host_port_subcomponent(self) -> str | None:
"""Return the host and port subcomponent part of URL.
Trailing dots are removed from the host part.
@ -818,7 +848,7 @@ class URL:
return f"[{raw}]:{port}" if ":" in raw else f"{raw}:{port}"
@cached_property
def port(self) -> Union[int, None]:
def port(self) -> int | None:
"""Port part of URL, with scheme-based fallback.
None for relative URLs or URLs without explicit port and
@ -830,7 +860,7 @@ class URL:
return DEFAULT_PORTS.get(self._scheme)
@cached_property
def explicit_port(self) -> Union[int, None]:
def explicit_port(self) -> int | None:
"""Port part of URL, without scheme-based fallback.
None for relative URLs or URLs without explicit port.
@ -1068,7 +1098,7 @@ class URL:
raise ValueError(msg)
return from_parts(lower_scheme, netloc, self._path, self._query, self._fragment)
def with_user(self, user: Union[str, None]) -> "URL":
def with_user(self, user: str | None) -> "URL":
"""Return a new URL with user replaced.
Autoencode user if needed.
@ -1090,7 +1120,7 @@ class URL:
netloc = make_netloc(user, password, encoded_host, self.explicit_port)
return from_parts(self._scheme, netloc, self._path, self._query, self._fragment)
def with_password(self, password: Union[str, None]) -> "URL":
def with_password(self, password: str | None) -> "URL":
"""Return a new URL with password replaced.
Autoencode password if needed.
@ -1133,7 +1163,7 @@ class URL:
netloc = make_netloc(self.raw_user, self.raw_password, encoded_host, port)
return from_parts(self._scheme, netloc, self._path, self._query, self._fragment)
def with_port(self, port: Union[int, None]) -> "URL":
def with_port(self, port: int | None) -> "URL":
"""Return a new URL with port replaced.
Clear port to default if None is passed.
@ -1240,12 +1270,12 @@ class URL:
>>> url.update_query(a=3, c=4)
URL('http://example.com/?a=3&b=2&c=4')
"""
in_query: Union[
str,
Mapping[str, QueryVariable],
Sequence[tuple[Union[str, istr], SimpleQuery]],
None,
]
in_query: (
str
| Mapping[str, QueryVariable]
| Sequence[tuple[str | istr, SimpleQuery]]
| None
)
if kwargs:
if args:
msg = "Either kwargs or single query parameter must be present"
@ -1305,7 +1335,7 @@ class URL:
)
)
def with_fragment(self, fragment: Union[str, None]) -> "URL":
def with_fragment(self, fragment: str | None) -> "URL":
"""Return a new URL with fragment replaced.
Autoencode fragment if needed.
@ -1463,13 +1493,15 @@ class URL:
def human_repr(self) -> str:
"""Return decoded human readable string for URL representation."""
user = human_quote(self.user, "#/:?@[]")
password = human_quote(self.password, "#/:?@[]")
user = human_quote(self.user, "#/:?@[]\\")
password = human_quote(self.password, "#/:?@[]\\")
if (host := self.host) and ":" in host:
host = f"[{host}]"
path = human_quote(self.path, "#?")
if TYPE_CHECKING:
assert path is not None
if not self._scheme and not self._netloc:
path = _encode_relative_scheme_colon(path)
query_string = "&".join(
"{}={}".format(human_quote(k, "#&+;="), human_quote(v, "#&+;="))
for k, v in self.query.items()
@ -1480,6 +1512,48 @@ class URL:
netloc = make_netloc(user, password, host, self.explicit_port)
return unsplit_result(self._scheme, netloc, path, query_string, fragment)
if HAS_PYDANTIC:
# Borrowed from https://docs.pydantic.dev/latest/concepts/types/#handling-third-party-types
@classmethod
def __get_pydantic_json_schema__(
cls,
core_schema: "CoreSchema",
handler: "GetJsonSchemaHandler",
) -> "JsonSchemaValue":
field_schema: dict[str, Any] = {}
field_schema.update(type="string", format="uri")
return field_schema
@classmethod
def __get_pydantic_core_schema__(
cls,
source_type: type[Self] | type[str],
handler: "GetCoreSchemaHandler",
) -> "CoreSchema":
# Lazy import: pulling in pydantic_core at module load time
# increases yarl's import cost 3-7x for users who don't use
# pydantic. Keep this import function-scoped.
from pydantic_core import core_schema # noqa: PLC0415
from_str_schema = core_schema.chain_schema(
[
core_schema.str_schema(),
core_schema.no_info_plain_validator_function(URL),
]
)
return core_schema.json_or_python_schema(
json_schema=from_str_schema,
python_schema=core_schema.union_schema(
[
# check if it's an instance first before doing any further work
core_schema.is_instance_schema(URL),
from_str_schema,
]
),
serialization=core_schema.plain_serializer_function_ser_schema(str),
)
_DEFAULT_IDNA_SIZE = 256
_DEFAULT_ENCODE_SIZE = 512
@ -1582,11 +1656,11 @@ def cache_info() -> CacheInfo:
@rewrite_module
def cache_configure(
*,
idna_encode_size: Union[int, None] = _DEFAULT_IDNA_SIZE,
idna_decode_size: Union[int, None] = _DEFAULT_IDNA_SIZE,
ip_address_size: Union[int, None, UndefinedType] = UNDEFINED,
host_validate_size: Union[int, None, UndefinedType] = UNDEFINED,
encode_host_size: Union[int, None, UndefinedType] = UNDEFINED,
idna_encode_size: int | None = _DEFAULT_IDNA_SIZE,
idna_decode_size: int | None = _DEFAULT_IDNA_SIZE,
ip_address_size: int | None | UndefinedType = UNDEFINED,
host_validate_size: int | None | UndefinedType = UNDEFINED,
encode_host_size: int | None | UndefinedType = UNDEFINED,
) -> None:
"""Configure LRU cache sizes."""
global _idna_decode, _idna_encode, _encode_host