diff options
| author | robot-piglet <[email protected]> | 2026-06-01 20:43:11 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-06-01 21:11:51 +0300 |
| commit | 415294973da8f1bf43f9e855f7efb4e076f56d1d (patch) | |
| tree | c76cf49f28cab8053c80ef9eaa7c16696d5efc20 /contrib/python | |
| parent | 54fe0971bca36ca40446ad614bcc60282228a85d (diff) | |
Intermediate changes
commit_hash:860ee6528739b22789a3ab8c56a304e11ffe475e
Diffstat (limited to 'contrib/python')
66 files changed, 7882 insertions, 6859 deletions
diff --git a/contrib/python/Werkzeug/py3/.dist-info/METADATA b/contrib/python/Werkzeug/py3/.dist-info/METADATA index 647bfc8001d..8bdbf5c0009 100644 --- a/contrib/python/Werkzeug/py3/.dist-info/METADATA +++ b/contrib/python/Werkzeug/py3/.dist-info/METADATA @@ -1,20 +1,10 @@ Metadata-Version: 2.1 Name: Werkzeug -Version: 2.2.3 +Version: 2.3.8 Summary: The comprehensive WSGI web application library. -Home-page: https://palletsprojects.com/p/werkzeug/ -Author: Armin Ronacher -Author-email: [email protected] -Maintainer: Pallets -Maintainer-email: [email protected] -License: BSD-3-Clause -Project-URL: Donate, https://palletsprojects.com/donate -Project-URL: Documentation, https://werkzeug.palletsprojects.com/ -Project-URL: Changes, https://werkzeug.palletsprojects.com/changes/ -Project-URL: Source Code, https://github.com/pallets/werkzeug/ -Project-URL: Issue Tracker, https://github.com/pallets/werkzeug/issues/ -Project-URL: Twitter, https://twitter.com/PalletsTeam -Project-URL: Chat, https://discord.gg/pallets +Maintainer-email: Pallets <[email protected]> +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Web Environment Classifier: Intended Audience :: Developers @@ -26,12 +16,15 @@ Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Topic :: Software Development :: Libraries :: Application Frameworks -Requires-Python: >=3.7 -Description-Content-Type: text/x-rst -License-File: LICENSE.rst -Requires-Dist: MarkupSafe (>=2.1.1) +Requires-Dist: MarkupSafe>=2.1.1 +Requires-Dist: watchdog>=2.3 ; extra == "watchdog" +Project-URL: Changes, https://werkzeug.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://werkzeug.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Issue Tracker, https://github.com/pallets/werkzeug/issues/ +Project-URL: Source Code, https://github.com/pallets/werkzeug/ Provides-Extra: watchdog -Requires-Dist: watchdog ; extra == 'watchdog' Werkzeug ======== @@ -121,6 +114,5 @@ Links - PyPI Releases: https://pypi.org/project/Werkzeug/ - Source Code: https://github.com/pallets/werkzeug/ - Issue Tracker: https://github.com/pallets/werkzeug/issues/ -- Website: https://palletsprojects.com/p/werkzeug/ -- Twitter: https://twitter.com/PalletsTeam - Chat: https://discord.gg/pallets + diff --git a/contrib/python/Werkzeug/py3/README.rst b/contrib/python/Werkzeug/py3/README.rst index f1592a5699e..220c9979a7f 100644 --- a/contrib/python/Werkzeug/py3/README.rst +++ b/contrib/python/Werkzeug/py3/README.rst @@ -86,6 +86,4 @@ Links - PyPI Releases: https://pypi.org/project/Werkzeug/ - Source Code: https://github.com/pallets/werkzeug/ - Issue Tracker: https://github.com/pallets/werkzeug/issues/ -- Website: https://palletsprojects.com/p/werkzeug/ -- Twitter: https://twitter.com/PalletsTeam - Chat: https://discord.gg/pallets diff --git a/contrib/python/Werkzeug/py3/patches/03-fix-cookie.patch b/contrib/python/Werkzeug/py3/patches/03-fix-cookie.patch index a9aebf66b5d..359e346311b 100644 --- a/contrib/python/Werkzeug/py3/patches/03-fix-cookie.patch +++ b/contrib/python/Werkzeug/py3/patches/03-fix-cookie.patch @@ -1,13 +1,7 @@ --- contrib/python/Werkzeug/py3/werkzeug/test.py (index) +++ contrib/python/Werkzeug/py3/werkzeug/test.py (working tree) -@@ -201,8 +201,8 @@ class _TestCookieJar(CookieJar): - - if cvals: - environ["HTTP_COOKIE"] = "; ".join(cvals) +@@ -201,2 +201,2 @@ class _TestCookieJar(CookieJar): - else: - environ.pop("HTTP_COOKIE", None) + #else: + # environ.pop("HTTP_COOKIE", None) - - def extract_wsgi( - self, diff --git a/contrib/python/Werkzeug/py3/patches/05-safe-symbols.patch b/contrib/python/Werkzeug/py3/patches/05-safe-symbols.patch index 2034626b14a..e53d410fb60 100644 --- a/contrib/python/Werkzeug/py3/patches/05-safe-symbols.patch +++ b/contrib/python/Werkzeug/py3/patches/05-safe-symbols.patch @@ -1,10 +1,5 @@ # This patch cant be droped after d0f040a65bfc39c52930f0ea0ca0a9d465bc5043 commit --- contrib/python/Werkzeug/py3/werkzeug/urls.py +++ contrib/python/Werkzeug/py3/werkzeug/urls.py -@@ -27,7 +27,6 @@ _always_safe = frozenset( - b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" - b"0123456789" - b"-._~" -- b"$!'()*+,;" # RFC3986 sub-delims set, not including query string delimiters &= - ) - ) +@@ -27,1 +27,0 @@ _always_safe = frozenset( +- "$!'()*+,;" # RFC3986 sub-delims set, not including query string delimiters &= diff --git a/contrib/python/Werkzeug/py3/werkzeug/__init__.py b/contrib/python/Werkzeug/py3/werkzeug/__init__.py index c20ac29bcb4..0a472ae7d6c 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/__init__.py +++ b/contrib/python/Werkzeug/py3/werkzeug/__init__.py @@ -3,4 +3,4 @@ from .test import Client as Client from .wrappers import Request as Request from .wrappers import Response as Response -__version__ = "2.2.3" +__version__ = "2.3.8" diff --git a/contrib/python/Werkzeug/py3/werkzeug/_internal.py b/contrib/python/Werkzeug/py3/werkzeug/_internal.py index f95207ab210..6ed4d3024bf 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/_internal.py +++ b/contrib/python/Werkzeug/py3/werkzeug/_internal.py @@ -1,50 +1,18 @@ +from __future__ import annotations + import logging import operator import re -import string import sys -import typing import typing as t -from datetime import date from datetime import datetime from datetime import timezone -from itertools import chain -from weakref import WeakKeyDictionary if t.TYPE_CHECKING: - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment - from .wrappers.request import Request # noqa: F401 - -_logger: t.Optional[logging.Logger] = None -_signature_cache = WeakKeyDictionary() # type: ignore -_epoch_ord = date(1970, 1, 1).toordinal() -_legal_cookie_chars = frozenset( - c.encode("ascii") - for c in f"{string.ascii_letters}{string.digits}/=!#$%&'*+-.^_`|~:" -) - -_cookie_quoting_map = {b",": b"\\054", b";": b"\\073", b'"': b'\\"', b"\\": b"\\\\"} -for _i in chain(range(32), range(127, 256)): - _cookie_quoting_map[_i.to_bytes(1, sys.byteorder)] = f"\\{_i:03o}".encode("latin1") + from .wrappers.request import Request -_octal_re = re.compile(rb"\\[0-3][0-7][0-7]") -_quote_re = re.compile(rb"[\\].") -_legal_cookie_chars_re = rb"[\w\d!#%&\'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" -_cookie_re = re.compile( - rb""" - (?P<key>[^=;]*) - (?:\s*=\s* - (?P<val> - "(?:[^\\"]|\\.)*" | - (?:.*?) - ) - )? - \s*; -""", - flags=re.VERBOSE, -) +_logger: logging.Logger | None = None class _Missing: @@ -58,12 +26,12 @@ class _Missing: _missing = _Missing() def _make_encode_wrapper(reference: str) -> t.Callable[[str], str]: ... def _make_encode_wrapper(reference: bytes) -> t.Callable[[str], bytes]: ... @@ -78,7 +46,7 @@ def _make_encode_wrapper(reference: t.AnyStr) -> t.Callable[[str], t.AnyStr]: return operator.methodcaller("encode", "latin1") -def _check_str_tuple(value: t.Tuple[t.AnyStr, ...]) -> None: +def _check_str_tuple(value: tuple[t.AnyStr, ...]) -> None: """Ensure tuple items are all strings or all bytes.""" if not value: return @@ -93,7 +61,7 @@ _default_encoding = sys.getdefaultencoding() def _to_bytes( - x: t.Union[str, bytes], charset: str = _default_encoding, errors: str = "strict" + x: str | bytes, charset: str = _default_encoding, errors: str = "strict" ) -> bytes: if x is None or isinstance(x, bytes): return x @@ -107,20 +75,20 @@ def _to_bytes( raise TypeError("Expected bytes") def _to_str( # type: ignore x: None, - charset: t.Optional[str] = ..., + charset: str | None = ..., errors: str = ..., allow_none_charset: bool = ..., ) -> None: ... def _to_str( x: t.Any, - charset: t.Optional[str] = ..., + charset: str | None = ..., errors: str = ..., allow_none_charset: bool = ..., ) -> str: @@ -128,11 +96,11 @@ def _to_str( def _to_str( - x: t.Optional[t.Any], - charset: t.Optional[str] = _default_encoding, + x: t.Any | None, + charset: str | None = _default_encoding, errors: str = "strict", allow_none_charset: bool = False, -) -> t.Optional[t.Union[str, bytes]]: +) -> str | bytes | None: if x is None or isinstance(x, str): return x @@ -152,16 +120,11 @@ def _wsgi_decoding_dance( return s.encode("latin1").decode(charset, errors) -def _wsgi_encoding_dance( - s: str, charset: str = "utf-8", errors: str = "replace" -) -> str: - if isinstance(s, bytes): - return s.decode("latin1", errors) - +def _wsgi_encoding_dance(s: str, charset: str = "utf-8", errors: str = "strict") -> str: return s.encode(charset).decode("latin1", errors) -def _get_environ(obj: t.Union["WSGIEnvironment", "Request"]) -> "WSGIEnvironment": +def _get_environ(obj: WSGIEnvironment | Request) -> WSGIEnvironment: env = getattr(obj, "environ", obj) assert isinstance( env, dict @@ -224,17 +187,17 @@ def _log(type: str, message: str, *args: t.Any, **kwargs: t.Any) -> None: getattr(_logger, type)(message.rstrip(), *args, **kwargs) def _dt_as_utc(dt: None) -> None: ... def _dt_as_utc(dt: datetime) -> datetime: ... -def _dt_as_utc(dt: t.Optional[datetime]) -> t.Optional[datetime]: +def _dt_as_utc(dt: datetime | None) -> datetime | None: if dt is None: return dt @@ -257,11 +220,11 @@ class _DictAccessorProperty(t.Generic[_TAccessorValue]): def __init__( self, name: str, - default: t.Optional[_TAccessorValue] = None, - load_func: t.Optional[t.Callable[[str], _TAccessorValue]] = None, - dump_func: t.Optional[t.Callable[[_TAccessorValue], str]] = None, - read_only: t.Optional[bool] = None, - doc: t.Optional[str] = None, + default: _TAccessorValue | None = None, + load_func: t.Callable[[str], _TAccessorValue] | None = None, + dump_func: t.Callable[[_TAccessorValue], str] | None = None, + read_only: bool | None = None, + doc: str | None = None, ) -> None: self.name = name self.default = default @@ -274,19 +237,19 @@ class _DictAccessorProperty(t.Generic[_TAccessorValue]): def lookup(self, instance: t.Any) -> t.MutableMapping[str, t.Any]: raise NotImplementedError - @typing.overload + @t.overload def __get__( self, instance: None, owner: type - ) -> "_DictAccessorProperty[_TAccessorValue]": + ) -> _DictAccessorProperty[_TAccessorValue]: ... - @typing.overload + @t.overload def __get__(self, instance: t.Any, owner: type) -> _TAccessorValue: ... def __get__( - self, instance: t.Optional[t.Any], owner: type - ) -> t.Union[_TAccessorValue, "_DictAccessorProperty[_TAccessorValue]"]: + self, instance: t.Any | None, owner: type + ) -> _TAccessorValue | _DictAccessorProperty[_TAccessorValue]: if instance is None: return self @@ -324,230 +287,44 @@ class _DictAccessorProperty(t.Generic[_TAccessorValue]): return f"<{type(self).__name__} {self.name}>" -def _cookie_quote(b: bytes) -> bytes: - buf = bytearray() - all_legal = True - _lookup = _cookie_quoting_map.get - _push = buf.extend - - for char_int in b: - char = char_int.to_bytes(1, sys.byteorder) - if char not in _legal_cookie_chars: - all_legal = False - char = _lookup(char, char) - _push(char) - - if all_legal: - return bytes(buf) - return bytes(b'"' + buf + b'"') - - -def _cookie_unquote(b: bytes) -> bytes: - if len(b) < 2: - return b - if b[:1] != b'"' or b[-1:] != b'"': - return b - - b = b[1:-1] - - i = 0 - n = len(b) - rv = bytearray() - _push = rv.extend - - while 0 <= i < n: - o_match = _octal_re.search(b, i) - q_match = _quote_re.search(b, i) - if not o_match and not q_match: - rv.extend(b[i:]) - break - j = k = -1 - if o_match: - j = o_match.start(0) - if q_match: - k = q_match.start(0) - if q_match and (not o_match or k < j): - _push(b[i:k]) - _push(b[k + 1 : k + 2]) - i = k + 2 - else: - _push(b[i:j]) - rv.append(int(b[j + 1 : j + 4], 8)) - i = j + 4 - - return bytes(rv) - - -def _cookie_parse_impl(b: bytes) -> t.Iterator[t.Tuple[bytes, bytes]]: - """Lowlevel cookie parsing facility that operates on bytes.""" - i = 0 - n = len(b) - b += b";" - - while i < n: - match = _cookie_re.match(b, i) - - if not match: - break - - i = match.end(0) - key = match.group("key").strip() - - if not key: - continue - - value = match.group("val") or b"" - yield key, _cookie_unquote(value) - - -def _encode_idna(domain: str) -> bytes: - # If we're given bytes, make sure they fit into ASCII - if isinstance(domain, bytes): - domain.decode("ascii") +def _decode_idna(domain: str) -> str: + try: + data = domain.encode("ascii") + except UnicodeEncodeError: + # If the domain is not ASCII, it's decoded already. return domain - # Otherwise check if it's already ascii, then return try: - return domain.encode("ascii") - except UnicodeError: + # Try decoding in one shot. + return data.decode("idna") + except UnicodeDecodeError: pass - # Otherwise encode each part separately - return b".".join(p.encode("idna") for p in domain.split(".")) - + # Decode each part separately, leaving invalid parts as punycode. + parts = [] -def _decode_idna(domain: t.Union[str, bytes]) -> str: - # If the input is a string try to encode it to ascii to do the idna - # decoding. If that fails because of a unicode error, then we - # already have a decoded idna domain. - if isinstance(domain, str): + for part in data.split(b"."): try: - domain = domain.encode("ascii") - except UnicodeError: - return domain # type: ignore - - # Decode each part separately. If a part fails, try to decode it - # with ascii and silently ignore errors. This makes sense because - # the idna codec does not have error handling. - def decode_part(part: bytes) -> str: - try: - return part.decode("idna") - except UnicodeError: - return part.decode("ascii", "ignore") - - return ".".join(decode_part(p) for p in domain.split(b".")) + parts.append(part.decode("idna")) + except UnicodeDecodeError: + parts.append(part.decode("ascii")) - -def _make_cookie_domain(domain: None) -> None: - ... - - -def _make_cookie_domain(domain: str) -> bytes: - ... + return ".".join(parts) -def _make_cookie_domain(domain: t.Optional[str]) -> t.Optional[bytes]: - if domain is None: - return None - domain = _encode_idna(domain) - if b":" in domain: - domain = domain.split(b":", 1)[0] - if b"." in domain: - return domain - raise ValueError( - "Setting 'domain' for a cookie on a server running locally (ex: " - "localhost) is not supported by complying browsers. You should " - "have something like: '127.0.0.1 localhost dev.localhost' on " - "your hosts file and then point your server to run on " - "'dev.localhost' and also set 'domain' for 'dev.localhost'" - ) - - -def _easteregg(app: t.Optional["WSGIApplication"] = None) -> "WSGIApplication": - """Like the name says. But who knows how it works?""" +_plain_int_re = re.compile(r"-?\d+", re.ASCII) - def bzzzzzzz(gyver: bytes) -> str: - import base64 - import zlib - return zlib.decompress(base64.b64decode(gyver)).decode("ascii") +def _plain_int(value: str) -> int: + """Parse an int only if it is only ASCII digits and ``-``. - gyver = "\n".join( - [ - x + (77 - len(x)) * " " - for x in bzzzzzzz( - b""" -eJyFlzuOJDkMRP06xRjymKgDJCDQStBYT8BCgK4gTwfQ2fcFs2a2FzvZk+hvlcRvRJD148efHt9m -9Xz94dRY5hGt1nrYcXx7us9qlcP9HHNh28rz8dZj+q4rynVFFPdlY4zH873NKCexrDM6zxxRymzz -4QIxzK4bth1PV7+uHn6WXZ5C4ka/+prFzx3zWLMHAVZb8RRUxtFXI5DTQ2n3Hi2sNI+HK43AOWSY -jmEzE4naFp58PdzhPMdslLVWHTGUVpSxImw+pS/D+JhzLfdS1j7PzUMxij+mc2U0I9zcbZ/HcZxc -q1QjvvcThMYFnp93agEx392ZdLJWXbi/Ca4Oivl4h/Y1ErEqP+lrg7Xa4qnUKu5UE9UUA4xeqLJ5 -jWlPKJvR2yhRI7xFPdzPuc6adXu6ovwXwRPXXnZHxlPtkSkqWHilsOrGrvcVWXgGP3daXomCj317 -8P2UOw/NnA0OOikZyFf3zZ76eN9QXNwYdD8f8/LdBRFg0BO3bB+Pe/+G8er8tDJv83XTkj7WeMBJ -v/rnAfdO51d6sFglfi8U7zbnr0u9tyJHhFZNXYfH8Iafv2Oa+DT6l8u9UYlajV/hcEgk1x8E8L/r -XJXl2SK+GJCxtnyhVKv6GFCEB1OO3f9YWAIEbwcRWv/6RPpsEzOkXURMN37J0PoCSYeBnJQd9Giu -LxYQJNlYPSo/iTQwgaihbART7Fcyem2tTSCcwNCs85MOOpJtXhXDe0E7zgZJkcxWTar/zEjdIVCk -iXy87FW6j5aGZhttDBoAZ3vnmlkx4q4mMmCdLtnHkBXFMCReqthSGkQ+MDXLLCpXwBs0t+sIhsDI -tjBB8MwqYQpLygZ56rRHHpw+OAVyGgaGRHWy2QfXez+ZQQTTBkmRXdV/A9LwH6XGZpEAZU8rs4pE -1R4FQ3Uwt8RKEtRc0/CrANUoes3EzM6WYcFyskGZ6UTHJWenBDS7h163Eo2bpzqxNE9aVgEM2CqI -GAJe9Yra4P5qKmta27VjzYdR04Vc7KHeY4vs61C0nbywFmcSXYjzBHdiEjraS7PGG2jHHTpJUMxN -Jlxr3pUuFvlBWLJGE3GcA1/1xxLcHmlO+LAXbhrXah1tD6Ze+uqFGdZa5FM+3eHcKNaEarutAQ0A -QMAZHV+ve6LxAwWnXbbSXEG2DmCX5ijeLCKj5lhVFBrMm+ryOttCAeFpUdZyQLAQkA06RLs56rzG -8MID55vqr/g64Qr/wqwlE0TVxgoiZhHrbY2h1iuuyUVg1nlkpDrQ7Vm1xIkI5XRKLedN9EjzVchu -jQhXcVkjVdgP2O99QShpdvXWoSwkp5uMwyjt3jiWCqWGSiaaPAzohjPanXVLbM3x0dNskJsaCEyz -DTKIs+7WKJD4ZcJGfMhLFBf6hlbnNkLEePF8Cx2o2kwmYF4+MzAxa6i+6xIQkswOqGO+3x9NaZX8 -MrZRaFZpLeVTYI9F/djY6DDVVs340nZGmwrDqTCiiqD5luj3OzwpmQCiQhdRYowUYEA3i1WWGwL4 -GCtSoO4XbIPFeKGU13XPkDf5IdimLpAvi2kVDVQbzOOa4KAXMFlpi/hV8F6IDe0Y2reg3PuNKT3i -RYhZqtkQZqSB2Qm0SGtjAw7RDwaM1roESC8HWiPxkoOy0lLTRFG39kvbLZbU9gFKFRvixDZBJmpi -Xyq3RE5lW00EJjaqwp/v3EByMSpVZYsEIJ4APaHmVtpGSieV5CALOtNUAzTBiw81GLgC0quyzf6c -NlWknzJeCsJ5fup2R4d8CYGN77mu5vnO1UqbfElZ9E6cR6zbHjgsr9ly18fXjZoPeDjPuzlWbFwS -pdvPkhntFvkc13qb9094LL5NrA3NIq3r9eNnop9DizWOqCEbyRBFJTHn6Tt3CG1o8a4HevYh0XiJ -sR0AVVHuGuMOIfbuQ/OKBkGRC6NJ4u7sbPX8bG/n5sNIOQ6/Y/BX3IwRlTSabtZpYLB85lYtkkgm -p1qXK3Du2mnr5INXmT/78KI12n11EFBkJHHp0wJyLe9MvPNUGYsf+170maayRoy2lURGHAIapSpQ -krEDuNoJCHNlZYhKpvw4mspVWxqo415n8cD62N9+EfHrAvqQnINStetek7RY2Urv8nxsnGaZfRr/ -nhXbJ6m/yl1LzYqscDZA9QHLNbdaSTTr+kFg3bC0iYbX/eQy0Bv3h4B50/SGYzKAXkCeOLI3bcAt -mj2Z/FM1vQWgDynsRwNvrWnJHlespkrp8+vO1jNaibm+PhqXPPv30YwDZ6jApe3wUjFQobghvW9p -7f2zLkGNv8b191cD/3vs9Q833z8t""" - ).splitlines() - ] - ) + This disallows ``+``, ``_``, and non-ASCII digits, which are accepted by ``int`` but + are not allowed in HTTP header values. - def easteregged( - environ: "WSGIEnvironment", start_response: "StartResponse" - ) -> t.Iterable[bytes]: - def injecting_start_response( - status: str, headers: t.List[t.Tuple[str, str]], exc_info: t.Any = None - ) -> t.Callable[[bytes], t.Any]: - headers.append(("X-Powered-By", "Werkzeug")) - return start_response(status, headers, exc_info) - - if app is not None and environ.get("QUERY_STRING") != "macgybarchakku": - return app(environ, injecting_start_response) - injecting_start_response("200 OK", [("Content-Type", "text/html")]) - return [ - f"""\ -<!doctype html> -<html lang=en> -<head> -<title>About Werkzeug</title> -<style type="text/css"> - body {{ font: 15px Georgia, serif; text-align: center; }} - a {{ color: #333; text-decoration: none; }} - h1 {{ font-size: 30px; margin: 20px 0 10px 0; }} - p {{ margin: 0 0 30px 0; }} - pre {{ font: 11px 'Consolas', 'Monaco', monospace; line-height: 0.95; }} -</style> -</head> -<body> -<h1><a href="http://werkzeug.pocoo.org/">Werkzeug</a></h1> -<p>the Swiss Army knife of Python web development.</p> -<pre>{gyver}\n\n\n</pre> -</body> -</html>""".encode( - "latin1" - ) - ] + Any leading or trailing whitespace is stripped + """ + value = value.strip() + if _plain_int_re.fullmatch(value) is None: + raise ValueError - return easteregged + return int(value) diff --git a/contrib/python/Werkzeug/py3/werkzeug/_reloader.py b/contrib/python/Werkzeug/py3/werkzeug/_reloader.py index 67614a78a71..9e69997f4a0 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/_reloader.py +++ b/contrib/python/Werkzeug/py3/werkzeug/_reloader.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import fnmatch import os import subprocess @@ -55,13 +57,13 @@ def _iter_module_paths() -> t.Iterator[str]: yield name -def _remove_by_pattern(paths: t.Set[str], exclude_patterns: t.Set[str]) -> None: +def _remove_by_pattern(paths: set[str], exclude_patterns: set[str]) -> None: for pattern in exclude_patterns: paths.difference_update(fnmatch.filter(paths, pattern)) def _find_stat_paths( - extra_files: t.Set[str], exclude_patterns: t.Set[str] + extra_files: set[str], exclude_patterns: set[str] ) -> t.Iterable[str]: """Find paths for the stat reloader to watch. Returns imported module files, Python files under non-system paths. Extra files and @@ -115,7 +117,7 @@ def _find_stat_paths( def _find_watchdog_paths( - extra_files: t.Set[str], exclude_patterns: t.Set[str] + extra_files: set[str], exclude_patterns: set[str] ) -> t.Iterable[str]: """Find paths for the stat reloader to watch. Looks at the same sources as the stat reloader, but watches everything under @@ -139,7 +141,7 @@ def _find_watchdog_paths( def _find_common_roots(paths: t.Iterable[str]) -> t.Iterable[str]: - root: t.Dict[str, dict] = {} + root: dict[str, dict] = {} for chunks in sorted((PurePath(x).parts for x in paths), key=len, reverse=True): node = root @@ -151,7 +153,7 @@ def _find_common_roots(paths: t.Iterable[str]) -> t.Iterable[str]: rv = set() - def _walk(node: t.Mapping[str, dict], path: t.Tuple[str, ...]) -> None: + def _walk(node: t.Mapping[str, dict], path: tuple[str, ...]) -> None: for prefix, child in node.items(): _walk(child, path + (prefix,)) @@ -162,10 +164,15 @@ def _find_common_roots(paths: t.Iterable[str]) -> t.Iterable[str]: return rv -def _get_args_for_reloading() -> t.List[str]: +def _get_args_for_reloading() -> list[str]: """Determine how the script was executed, and return the args needed to execute it again in a new process. """ + if sys.version_info >= (3, 10): + # sys.orig_argv, added in Python 3.10, contains the exact args used to invoke + # Python. Still replace argv[0] with sys.executable for accuracy. + return [sys.executable, *sys.orig_argv[1:]] + rv = [sys.executable] py_script = sys.argv[0] args = sys.argv[1:] @@ -221,15 +228,15 @@ class ReloaderLoop: def __init__( self, - extra_files: t.Optional[t.Iterable[str]] = None, - exclude_patterns: t.Optional[t.Iterable[str]] = None, - interval: t.Union[int, float] = 1, + extra_files: t.Iterable[str] | None = None, + exclude_patterns: t.Iterable[str] | None = None, + interval: int | float = 1, ) -> None: - self.extra_files: t.Set[str] = {os.path.abspath(x) for x in extra_files or ()} - self.exclude_patterns: t.Set[str] = set(exclude_patterns or ()) + self.extra_files: set[str] = {os.path.abspath(x) for x in extra_files or ()} + self.exclude_patterns: set[str] = set(exclude_patterns or ()) self.interval = interval - def __enter__(self) -> "ReloaderLoop": + def __enter__(self) -> ReloaderLoop: """Do any setup, then run one step of the watch to populate the initial filesystem state. """ @@ -281,7 +288,7 @@ class StatReloaderLoop(ReloaderLoop): name = "stat" def __enter__(self) -> ReloaderLoop: - self.mtimes: t.Dict[str, float] = {} + self.mtimes: dict[str, float] = {} return super().__enter__() def run_step(self) -> None: @@ -305,15 +312,20 @@ class WatchdogReloaderLoop(ReloaderLoop): def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: from watchdog.observers import Observer from watchdog.events import PatternMatchingEventHandler + from watchdog.events import EVENT_TYPE_OPENED + from watchdog.events import FileModifiedEvent super().__init__(*args, **kwargs) trigger_reload = self.trigger_reload class EventHandler(PatternMatchingEventHandler): - def on_any_event(self, event): # type: ignore + def on_any_event(self, event: FileModifiedEvent): # type: ignore + if event.event_type == EVENT_TYPE_OPENED: + return + trigger_reload(event.src_path) - reloader_name = Observer.__name__.lower() + reloader_name = Observer.__name__.lower() # type: ignore[attr-defined] if reloader_name.endswith("observer"): reloader_name = reloader_name[:-8] @@ -343,7 +355,7 @@ class WatchdogReloaderLoop(ReloaderLoop): self.log_reload(filename) def __enter__(self) -> ReloaderLoop: - self.watches: t.Dict[str, t.Any] = {} + self.watches: dict[str, t.Any] = {} self.observer.start() return super().__enter__() @@ -382,7 +394,7 @@ class WatchdogReloaderLoop(ReloaderLoop): self.observer.unschedule(watch) -reloader_loops: t.Dict[str, t.Type[ReloaderLoop]] = { +reloader_loops: dict[str, type[ReloaderLoop]] = { "stat": StatReloaderLoop, "watchdog": WatchdogReloaderLoop, } @@ -416,9 +428,9 @@ def ensure_echo_on() -> None: def run_with_reloader( main_func: t.Callable[[], None], - extra_files: t.Optional[t.Iterable[str]] = None, - exclude_patterns: t.Optional[t.Iterable[str]] = None, - interval: t.Union[int, float] = 1, + extra_files: t.Iterable[str] | None = None, + exclude_patterns: t.Iterable[str] | None = None, + interval: int | float = 1, reloader_type: str = "auto", ) -> None: """Run the given function in an independent Python interpreter.""" diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures.py deleted file mode 100644 index a293dfddb56..00000000000 --- a/contrib/python/Werkzeug/py3/werkzeug/datastructures.py +++ /dev/null @@ -1,3040 +0,0 @@ -import base64 -import codecs -import mimetypes -import os -import re -from collections.abc import Collection -from collections.abc import MutableSet -from copy import deepcopy -from io import BytesIO -from itertools import repeat -from os import fspath - -from . import exceptions -from ._internal import _missing - - -def is_immutable(self): - raise TypeError(f"{type(self).__name__!r} objects are immutable") - - -def iter_multi_items(mapping): - """Iterates over the items of a mapping yielding keys and values - without dropping any from more complex structures. - """ - if isinstance(mapping, MultiDict): - yield from mapping.items(multi=True) - elif isinstance(mapping, dict): - for key, value in mapping.items(): - if isinstance(value, (tuple, list)): - for v in value: - yield key, v - else: - yield key, value - else: - yield from mapping - - -class ImmutableListMixin: - """Makes a :class:`list` immutable. - - .. versionadded:: 0.5 - - :private: - """ - - _hash_cache = None - - def __hash__(self): - if self._hash_cache is not None: - return self._hash_cache - rv = self._hash_cache = hash(tuple(self)) - return rv - - def __reduce_ex__(self, protocol): - return type(self), (list(self),) - - def __delitem__(self, key): - is_immutable(self) - - def __iadd__(self, other): - is_immutable(self) - - def __imul__(self, other): - is_immutable(self) - - def __setitem__(self, key, value): - is_immutable(self) - - def append(self, item): - is_immutable(self) - - def remove(self, item): - is_immutable(self) - - def extend(self, iterable): - is_immutable(self) - - def insert(self, pos, value): - is_immutable(self) - - def pop(self, index=-1): - is_immutable(self) - - def reverse(self): - is_immutable(self) - - def sort(self, key=None, reverse=False): - is_immutable(self) - - -class ImmutableList(ImmutableListMixin, list): - """An immutable :class:`list`. - - .. versionadded:: 0.5 - - :private: - """ - - def __repr__(self): - return f"{type(self).__name__}({list.__repr__(self)})" - - -class ImmutableDictMixin: - """Makes a :class:`dict` immutable. - - .. versionadded:: 0.5 - - :private: - """ - - _hash_cache = None - - @classmethod - def fromkeys(cls, keys, value=None): - instance = super().__new__(cls) - instance.__init__(zip(keys, repeat(value))) - return instance - - def __reduce_ex__(self, protocol): - return type(self), (dict(self),) - - def _iter_hashitems(self): - return self.items() - - def __hash__(self): - if self._hash_cache is not None: - return self._hash_cache - rv = self._hash_cache = hash(frozenset(self._iter_hashitems())) - return rv - - def setdefault(self, key, default=None): - is_immutable(self) - - def update(self, *args, **kwargs): - is_immutable(self) - - def pop(self, key, default=None): - is_immutable(self) - - def popitem(self): - is_immutable(self) - - def __setitem__(self, key, value): - is_immutable(self) - - def __delitem__(self, key): - is_immutable(self) - - def clear(self): - is_immutable(self) - - -class ImmutableMultiDictMixin(ImmutableDictMixin): - """Makes a :class:`MultiDict` immutable. - - .. versionadded:: 0.5 - - :private: - """ - - def __reduce_ex__(self, protocol): - return type(self), (list(self.items(multi=True)),) - - def _iter_hashitems(self): - return self.items(multi=True) - - def add(self, key, value): - is_immutable(self) - - def popitemlist(self): - is_immutable(self) - - def poplist(self, key): - is_immutable(self) - - def setlist(self, key, new_list): - is_immutable(self) - - def setlistdefault(self, key, default_list=None): - is_immutable(self) - - -def _calls_update(name): - def oncall(self, *args, **kw): - rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw) - - if self.on_update is not None: - self.on_update(self) - - return rv - - oncall.__name__ = name - return oncall - - -class UpdateDictMixin(dict): - """Makes dicts call `self.on_update` on modifications. - - .. versionadded:: 0.5 - - :private: - """ - - on_update = None - - def setdefault(self, key, default=None): - modified = key not in self - rv = super().setdefault(key, default) - if modified and self.on_update is not None: - self.on_update(self) - return rv - - def pop(self, key, default=_missing): - modified = key in self - if default is _missing: - rv = super().pop(key) - else: - rv = super().pop(key, default) - if modified and self.on_update is not None: - self.on_update(self) - return rv - - __setitem__ = _calls_update("__setitem__") - __delitem__ = _calls_update("__delitem__") - clear = _calls_update("clear") - popitem = _calls_update("popitem") - update = _calls_update("update") - - -class TypeConversionDict(dict): - """Works like a regular dict but the :meth:`get` method can perform - type conversions. :class:`MultiDict` and :class:`CombinedMultiDict` - are subclasses of this class and provide the same feature. - - .. versionadded:: 0.5 - """ - - def get(self, key, default=None, type=None): - """Return the default value if the requested data doesn't exist. - If `type` is provided and is a callable it should convert the value, - return it or raise a :exc:`ValueError` if that is not possible. In - this case the function will return the default as if the value was not - found: - - >>> d = TypeConversionDict(foo='42', bar='blub') - >>> d.get('foo', type=int) - 42 - >>> d.get('bar', -1, type=int) - -1 - - :param key: The key to be looked up. - :param default: The default value to be returned if the key can't - be looked up. If not further specified `None` is - returned. - :param type: A callable that is used to cast the value in the - :class:`MultiDict`. If a :exc:`ValueError` is raised - by this callable the default value is returned. - """ - try: - rv = self[key] - except KeyError: - return default - if type is not None: - try: - rv = type(rv) - except ValueError: - rv = default - return rv - - -class ImmutableTypeConversionDict(ImmutableDictMixin, TypeConversionDict): - """Works like a :class:`TypeConversionDict` but does not support - modifications. - - .. versionadded:: 0.5 - """ - - def copy(self): - """Return a shallow mutable copy of this object. Keep in mind that - the standard library's :func:`copy` function is a no-op for this class - like for any other python immutable type (eg: :class:`tuple`). - """ - return TypeConversionDict(self) - - def __copy__(self): - return self - - -class MultiDict(TypeConversionDict): - """A :class:`MultiDict` is a dictionary subclass customized to deal with - multiple values for the same key which is for example used by the parsing - functions in the wrappers. This is necessary because some HTML form - elements pass multiple values for the same key. - - :class:`MultiDict` implements all standard dictionary methods. - Internally, it saves all values for a key as a list, but the standard dict - access methods will only return the first value for a key. If you want to - gain access to the other values, too, you have to use the `list` methods as - explained below. - - Basic Usage: - - >>> d = MultiDict([('a', 'b'), ('a', 'c')]) - >>> d - MultiDict([('a', 'b'), ('a', 'c')]) - >>> d['a'] - 'b' - >>> d.getlist('a') - ['b', 'c'] - >>> 'a' in d - True - - It behaves like a normal dict thus all dict functions will only return the - first value when multiple values for one key are found. - - From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a - subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will - render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP - exceptions. - - A :class:`MultiDict` can be constructed from an iterable of - ``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2 - onwards some keyword parameters. - - :param mapping: the initial value for the :class:`MultiDict`. Either a - regular dict, an iterable of ``(key, value)`` tuples - or `None`. - """ - - def __init__(self, mapping=None): - if isinstance(mapping, MultiDict): - dict.__init__(self, ((k, l[:]) for k, l in mapping.lists())) - elif isinstance(mapping, dict): - tmp = {} - for key, value in mapping.items(): - if isinstance(value, (tuple, list)): - if len(value) == 0: - continue - value = list(value) - else: - value = [value] - tmp[key] = value - dict.__init__(self, tmp) - else: - tmp = {} - for key, value in mapping or (): - tmp.setdefault(key, []).append(value) - dict.__init__(self, tmp) - - def __getstate__(self): - return dict(self.lists()) - - def __setstate__(self, value): - dict.clear(self) - dict.update(self, value) - - def __iter__(self): - # Work around https://bugs.python.org/issue43246. - # (`return super().__iter__()` also works here, which makes this look - # even more like it should be a no-op, yet it isn't.) - return dict.__iter__(self) - - def __getitem__(self, key): - """Return the first data value for this key; - raises KeyError if not found. - - :param key: The key to be looked up. - :raise KeyError: if the key does not exist. - """ - - if key in self: - lst = dict.__getitem__(self, key) - if len(lst) > 0: - return lst[0] - raise exceptions.BadRequestKeyError(key) - - def __setitem__(self, key, value): - """Like :meth:`add` but removes an existing key first. - - :param key: the key for the value. - :param value: the value to set. - """ - dict.__setitem__(self, key, [value]) - - def add(self, key, value): - """Adds a new value for the key. - - .. versionadded:: 0.6 - - :param key: the key for the value. - :param value: the value to add. - """ - dict.setdefault(self, key, []).append(value) - - def getlist(self, key, type=None): - """Return the list of items for a given key. If that key is not in the - `MultiDict`, the return value will be an empty list. Just like `get`, - `getlist` accepts a `type` parameter. All items will be converted - with the callable defined there. - - :param key: The key to be looked up. - :param type: A callable that is used to cast the value in the - :class:`MultiDict`. If a :exc:`ValueError` is raised - by this callable the value will be removed from the list. - :return: a :class:`list` of all the values for the key. - """ - try: - rv = dict.__getitem__(self, key) - except KeyError: - return [] - if type is None: - return list(rv) - result = [] - for item in rv: - try: - result.append(type(item)) - except ValueError: - pass - return result - - def setlist(self, key, new_list): - """Remove the old values for a key and add new ones. Note that the list - you pass the values in will be shallow-copied before it is inserted in - the dictionary. - - >>> d = MultiDict() - >>> d.setlist('foo', ['1', '2']) - >>> d['foo'] - '1' - >>> d.getlist('foo') - ['1', '2'] - - :param key: The key for which the values are set. - :param new_list: An iterable with the new values for the key. Old values - are removed first. - """ - dict.__setitem__(self, key, list(new_list)) - - def setdefault(self, key, default=None): - """Returns the value for the key if it is in the dict, otherwise it - returns `default` and sets that value for `key`. - - :param key: The key to be looked up. - :param default: The default value to be returned if the key is not - in the dict. If not further specified it's `None`. - """ - if key not in self: - self[key] = default - else: - default = self[key] - return default - - def setlistdefault(self, key, default_list=None): - """Like `setdefault` but sets multiple values. The list returned - is not a copy, but the list that is actually used internally. This - means that you can put new values into the dict by appending items - to the list: - - >>> d = MultiDict({"foo": 1}) - >>> d.setlistdefault("foo").extend([2, 3]) - >>> d.getlist("foo") - [1, 2, 3] - - :param key: The key to be looked up. - :param default_list: An iterable of default values. It is either copied - (in case it was a list) or converted into a list - before returned. - :return: a :class:`list` - """ - if key not in self: - default_list = list(default_list or ()) - dict.__setitem__(self, key, default_list) - else: - default_list = dict.__getitem__(self, key) - return default_list - - def items(self, multi=False): - """Return an iterator of ``(key, value)`` pairs. - - :param multi: If set to `True` the iterator returned will have a pair - for each value of each key. Otherwise it will only - contain pairs for the first value of each key. - """ - for key, values in dict.items(self): - if multi: - for value in values: - yield key, value - else: - yield key, values[0] - - def lists(self): - """Return a iterator of ``(key, values)`` pairs, where values is the list - of all values associated with the key.""" - for key, values in dict.items(self): - yield key, list(values) - - def values(self): - """Returns an iterator of the first value on every key's value list.""" - for values in dict.values(self): - yield values[0] - - def listvalues(self): - """Return an iterator of all values associated with a key. Zipping - :meth:`keys` and this is the same as calling :meth:`lists`: - - >>> d = MultiDict({"foo": [1, 2, 3]}) - >>> zip(d.keys(), d.listvalues()) == d.lists() - True - """ - return dict.values(self) - - def copy(self): - """Return a shallow copy of this object.""" - return self.__class__(self) - - def deepcopy(self, memo=None): - """Return a deep copy of this object.""" - return self.__class__(deepcopy(self.to_dict(flat=False), memo)) - - def to_dict(self, flat=True): - """Return the contents as regular dict. If `flat` is `True` the - returned dict will only have the first item present, if `flat` is - `False` all values will be returned as lists. - - :param flat: If set to `False` the dict returned will have lists - with all the values in it. Otherwise it will only - contain the first value for each key. - :return: a :class:`dict` - """ - if flat: - return dict(self.items()) - return dict(self.lists()) - - def update(self, mapping): - """update() extends rather than replaces existing key lists: - - >>> a = MultiDict({'x': 1}) - >>> b = MultiDict({'x': 2, 'y': 3}) - >>> a.update(b) - >>> a - MultiDict([('y', 3), ('x', 1), ('x', 2)]) - - If the value list for a key in ``other_dict`` is empty, no new values - will be added to the dict and the key will not be created: - - >>> x = {'empty_list': []} - >>> y = MultiDict() - >>> y.update(x) - >>> y - MultiDict([]) - """ - for key, value in iter_multi_items(mapping): - MultiDict.add(self, key, value) - - def pop(self, key, default=_missing): - """Pop the first item for a list on the dict. Afterwards the - key is removed from the dict, so additional values are discarded: - - >>> d = MultiDict({"foo": [1, 2, 3]}) - >>> d.pop("foo") - 1 - >>> "foo" in d - False - - :param key: the key to pop. - :param default: if provided the value to return if the key was - not in the dictionary. - """ - try: - lst = dict.pop(self, key) - - if len(lst) == 0: - raise exceptions.BadRequestKeyError(key) - - return lst[0] - except KeyError: - if default is not _missing: - return default - - raise exceptions.BadRequestKeyError(key) from None - - def popitem(self): - """Pop an item from the dict.""" - try: - item = dict.popitem(self) - - if len(item[1]) == 0: - raise exceptions.BadRequestKeyError(item[0]) - - return (item[0], item[1][0]) - except KeyError as e: - raise exceptions.BadRequestKeyError(e.args[0]) from None - - def poplist(self, key): - """Pop the list for a key from the dict. If the key is not in the dict - an empty list is returned. - - .. versionchanged:: 0.5 - If the key does no longer exist a list is returned instead of - raising an error. - """ - return dict.pop(self, key, []) - - def popitemlist(self): - """Pop a ``(key, list)`` tuple from the dict.""" - try: - return dict.popitem(self) - except KeyError as e: - raise exceptions.BadRequestKeyError(e.args[0]) from None - - def __copy__(self): - return self.copy() - - def __deepcopy__(self, memo): - return self.deepcopy(memo=memo) - - def __repr__(self): - return f"{type(self).__name__}({list(self.items(multi=True))!r})" - - -class _omd_bucket: - """Wraps values in the :class:`OrderedMultiDict`. This makes it - possible to keep an order over multiple different keys. It requires - a lot of extra memory and slows down access a lot, but makes it - possible to access elements in O(1) and iterate in O(n). - """ - - __slots__ = ("prev", "key", "value", "next") - - def __init__(self, omd, key, value): - self.prev = omd._last_bucket - self.key = key - self.value = value - self.next = None - - if omd._first_bucket is None: - omd._first_bucket = self - if omd._last_bucket is not None: - omd._last_bucket.next = self - omd._last_bucket = self - - def unlink(self, omd): - if self.prev: - self.prev.next = self.next - if self.next: - self.next.prev = self.prev - if omd._first_bucket is self: - omd._first_bucket = self.next - if omd._last_bucket is self: - omd._last_bucket = self.prev - - -class OrderedMultiDict(MultiDict): - """Works like a regular :class:`MultiDict` but preserves the - order of the fields. To convert the ordered multi dict into a - list you can use the :meth:`items` method and pass it ``multi=True``. - - In general an :class:`OrderedMultiDict` is an order of magnitude - slower than a :class:`MultiDict`. - - .. admonition:: note - - Due to a limitation in Python you cannot convert an ordered - multi dict into a regular dict by using ``dict(multidict)``. - Instead you have to use the :meth:`to_dict` method, otherwise - the internal bucket objects are exposed. - """ - - def __init__(self, mapping=None): - dict.__init__(self) - self._first_bucket = self._last_bucket = None - if mapping is not None: - OrderedMultiDict.update(self, mapping) - - def __eq__(self, other): - if not isinstance(other, MultiDict): - return NotImplemented - if isinstance(other, OrderedMultiDict): - iter1 = iter(self.items(multi=True)) - iter2 = iter(other.items(multi=True)) - try: - for k1, v1 in iter1: - k2, v2 = next(iter2) - if k1 != k2 or v1 != v2: - return False - except StopIteration: - return False - try: - next(iter2) - except StopIteration: - return True - return False - if len(self) != len(other): - return False - for key, values in self.lists(): - if other.getlist(key) != values: - return False - return True - - __hash__ = None - - def __reduce_ex__(self, protocol): - return type(self), (list(self.items(multi=True)),) - - def __getstate__(self): - return list(self.items(multi=True)) - - def __setstate__(self, values): - dict.clear(self) - for key, value in values: - self.add(key, value) - - def __getitem__(self, key): - if key in self: - return dict.__getitem__(self, key)[0].value - raise exceptions.BadRequestKeyError(key) - - def __setitem__(self, key, value): - self.poplist(key) - self.add(key, value) - - def __delitem__(self, key): - self.pop(key) - - def keys(self): - return (key for key, value in self.items()) - - def __iter__(self): - return iter(self.keys()) - - def values(self): - return (value for key, value in self.items()) - - def items(self, multi=False): - ptr = self._first_bucket - if multi: - while ptr is not None: - yield ptr.key, ptr.value - ptr = ptr.next - else: - returned_keys = set() - while ptr is not None: - if ptr.key not in returned_keys: - returned_keys.add(ptr.key) - yield ptr.key, ptr.value - ptr = ptr.next - - def lists(self): - returned_keys = set() - ptr = self._first_bucket - while ptr is not None: - if ptr.key not in returned_keys: - yield ptr.key, self.getlist(ptr.key) - returned_keys.add(ptr.key) - ptr = ptr.next - - def listvalues(self): - for _key, values in self.lists(): - yield values - - def add(self, key, value): - dict.setdefault(self, key, []).append(_omd_bucket(self, key, value)) - - def getlist(self, key, type=None): - try: - rv = dict.__getitem__(self, key) - except KeyError: - return [] - if type is None: - return [x.value for x in rv] - result = [] - for item in rv: - try: - result.append(type(item.value)) - except ValueError: - pass - return result - - def setlist(self, key, new_list): - self.poplist(key) - for value in new_list: - self.add(key, value) - - def setlistdefault(self, key, default_list=None): - raise TypeError("setlistdefault is unsupported for ordered multi dicts") - - def update(self, mapping): - for key, value in iter_multi_items(mapping): - OrderedMultiDict.add(self, key, value) - - def poplist(self, key): - buckets = dict.pop(self, key, ()) - for bucket in buckets: - bucket.unlink(self) - return [x.value for x in buckets] - - def pop(self, key, default=_missing): - try: - buckets = dict.pop(self, key) - except KeyError: - if default is not _missing: - return default - - raise exceptions.BadRequestKeyError(key) from None - - for bucket in buckets: - bucket.unlink(self) - - return buckets[0].value - - def popitem(self): - try: - key, buckets = dict.popitem(self) - except KeyError as e: - raise exceptions.BadRequestKeyError(e.args[0]) from None - - for bucket in buckets: - bucket.unlink(self) - - return key, buckets[0].value - - def popitemlist(self): - try: - key, buckets = dict.popitem(self) - except KeyError as e: - raise exceptions.BadRequestKeyError(e.args[0]) from None - - for bucket in buckets: - bucket.unlink(self) - - return key, [x.value for x in buckets] - - -def _options_header_vkw(value, kw): - return http.dump_options_header( - value, {k.replace("_", "-"): v for k, v in kw.items()} - ) - - -def _unicodify_header_value(value): - if isinstance(value, bytes): - value = value.decode("latin-1") - if not isinstance(value, str): - value = str(value) - return value - - -class Headers: - """An object that stores some headers. It has a dict-like interface, - but is ordered, can store the same key multiple times, and iterating - yields ``(key, value)`` pairs instead of only keys. - - This data structure is useful if you want a nicer way to handle WSGI - headers which are stored as tuples in a list. - - From Werkzeug 0.3 onwards, the :exc:`KeyError` raised by this class is - also a subclass of the :class:`~exceptions.BadRequest` HTTP exception - and will render a page for a ``400 BAD REQUEST`` if caught in a - catch-all for HTTP exceptions. - - Headers is mostly compatible with the Python :class:`wsgiref.headers.Headers` - class, with the exception of `__getitem__`. :mod:`wsgiref` will return - `None` for ``headers['missing']``, whereas :class:`Headers` will raise - a :class:`KeyError`. - - To create a new ``Headers`` object, pass it a list, dict, or - other ``Headers`` object with default values. These values are - validated the same way values added later are. - - :param defaults: The list of default values for the :class:`Headers`. - - .. versionchanged:: 2.1.0 - Default values are validated the same as values added later. - - .. versionchanged:: 0.9 - This data structure now stores unicode values similar to how the - multi dicts do it. The main difference is that bytes can be set as - well which will automatically be latin1 decoded. - - .. versionchanged:: 0.9 - The :meth:`linked` function was removed without replacement as it - was an API that does not support the changes to the encoding model. - """ - - def __init__(self, defaults=None): - self._list = [] - if defaults is not None: - self.extend(defaults) - - def __getitem__(self, key, _get_mode=False): - if not _get_mode: - if isinstance(key, int): - return self._list[key] - elif isinstance(key, slice): - return self.__class__(self._list[key]) - if not isinstance(key, str): - raise exceptions.BadRequestKeyError(key) - ikey = key.lower() - for k, v in self._list: - if k.lower() == ikey: - return v - # micro optimization: if we are in get mode we will catch that - # exception one stack level down so we can raise a standard - # key error instead of our special one. - if _get_mode: - raise KeyError() - raise exceptions.BadRequestKeyError(key) - - def __eq__(self, other): - def lowered(item): - return (item[0].lower(),) + item[1:] - - return other.__class__ is self.__class__ and set( - map(lowered, other._list) - ) == set(map(lowered, self._list)) - - __hash__ = None - - def get(self, key, default=None, type=None, as_bytes=False): - """Return the default value if the requested data doesn't exist. - If `type` is provided and is a callable it should convert the value, - return it or raise a :exc:`ValueError` if that is not possible. In - this case the function will return the default as if the value was not - found: - - >>> d = Headers([('Content-Length', '42')]) - >>> d.get('Content-Length', type=int) - 42 - - .. versionadded:: 0.9 - Added support for `as_bytes`. - - :param key: The key to be looked up. - :param default: The default value to be returned if the key can't - be looked up. If not further specified `None` is - returned. - :param type: A callable that is used to cast the value in the - :class:`Headers`. If a :exc:`ValueError` is raised - by this callable the default value is returned. - :param as_bytes: return bytes instead of strings. - """ - try: - rv = self.__getitem__(key, _get_mode=True) - except KeyError: - return default - if as_bytes: - rv = rv.encode("latin1") - if type is None: - return rv - try: - return type(rv) - except ValueError: - return default - - def getlist(self, key, type=None, as_bytes=False): - """Return the list of items for a given key. If that key is not in the - :class:`Headers`, the return value will be an empty list. Just like - :meth:`get`, :meth:`getlist` accepts a `type` parameter. All items will - be converted with the callable defined there. - - .. versionadded:: 0.9 - Added support for `as_bytes`. - - :param key: The key to be looked up. - :param type: A callable that is used to cast the value in the - :class:`Headers`. If a :exc:`ValueError` is raised - by this callable the value will be removed from the list. - :return: a :class:`list` of all the values for the key. - :param as_bytes: return bytes instead of strings. - """ - ikey = key.lower() - result = [] - for k, v in self: - if k.lower() == ikey: - if as_bytes: - v = v.encode("latin1") - if type is not None: - try: - v = type(v) - except ValueError: - continue - result.append(v) - return result - - def get_all(self, name): - """Return a list of all the values for the named field. - - This method is compatible with the :mod:`wsgiref` - :meth:`~wsgiref.headers.Headers.get_all` method. - """ - return self.getlist(name) - - def items(self, lower=False): - for key, value in self: - if lower: - key = key.lower() - yield key, value - - def keys(self, lower=False): - for key, _ in self.items(lower): - yield key - - def values(self): - for _, value in self.items(): - yield value - - def extend(self, *args, **kwargs): - """Extend headers in this object with items from another object - containing header items as well as keyword arguments. - - To replace existing keys instead of extending, use - :meth:`update` instead. - - If provided, the first argument can be another :class:`Headers` - object, a :class:`MultiDict`, :class:`dict`, or iterable of - pairs. - - .. versionchanged:: 1.0 - Support :class:`MultiDict`. Allow passing ``kwargs``. - """ - if len(args) > 1: - raise TypeError(f"update expected at most 1 arguments, got {len(args)}") - - if args: - for key, value in iter_multi_items(args[0]): - self.add(key, value) - - for key, value in iter_multi_items(kwargs): - self.add(key, value) - - def __delitem__(self, key, _index_operation=True): - if _index_operation and isinstance(key, (int, slice)): - del self._list[key] - return - key = key.lower() - new = [] - for k, v in self._list: - if k.lower() != key: - new.append((k, v)) - self._list[:] = new - - def remove(self, key): - """Remove a key. - - :param key: The key to be removed. - """ - return self.__delitem__(key, _index_operation=False) - - def pop(self, key=None, default=_missing): - """Removes and returns a key or index. - - :param key: The key to be popped. If this is an integer the item at - that position is removed, if it's a string the value for - that key is. If the key is omitted or `None` the last - item is removed. - :return: an item. - """ - if key is None: - return self._list.pop() - if isinstance(key, int): - return self._list.pop(key) - try: - rv = self[key] - self.remove(key) - except KeyError: - if default is not _missing: - return default - raise - return rv - - def popitem(self): - """Removes a key or index and returns a (key, value) item.""" - return self.pop() - - def __contains__(self, key): - """Check if a key is present.""" - try: - self.__getitem__(key, _get_mode=True) - except KeyError: - return False - return True - - def __iter__(self): - """Yield ``(key, value)`` tuples.""" - return iter(self._list) - - def __len__(self): - return len(self._list) - - def add(self, _key, _value, **kw): - """Add a new header tuple to the list. - - Keyword arguments can specify additional parameters for the header - value, with underscores converted to dashes:: - - >>> d = Headers() - >>> d.add('Content-Type', 'text/plain') - >>> d.add('Content-Disposition', 'attachment', filename='foo.png') - - The keyword argument dumping uses :func:`dump_options_header` - behind the scenes. - - .. versionadded:: 0.4.1 - keyword arguments were added for :mod:`wsgiref` compatibility. - """ - if kw: - _value = _options_header_vkw(_value, kw) - _key = _unicodify_header_value(_key) - _value = _unicodify_header_value(_value) - self._validate_value(_value) - self._list.append((_key, _value)) - - def _validate_value(self, value): - if not isinstance(value, str): - raise TypeError("Value should be a string.") - if "\n" in value or "\r" in value: - raise ValueError( - "Detected newline in header value. This is " - "a potential security problem" - ) - - def add_header(self, _key, _value, **_kw): - """Add a new header tuple to the list. - - An alias for :meth:`add` for compatibility with the :mod:`wsgiref` - :meth:`~wsgiref.headers.Headers.add_header` method. - """ - self.add(_key, _value, **_kw) - - def clear(self): - """Clears all headers.""" - del self._list[:] - - def set(self, _key, _value, **kw): - """Remove all header tuples for `key` and add a new one. The newly - added key either appears at the end of the list if there was no - entry or replaces the first one. - - Keyword arguments can specify additional parameters for the header - value, with underscores converted to dashes. See :meth:`add` for - more information. - - .. versionchanged:: 0.6.1 - :meth:`set` now accepts the same arguments as :meth:`add`. - - :param key: The key to be inserted. - :param value: The value to be inserted. - """ - if kw: - _value = _options_header_vkw(_value, kw) - _key = _unicodify_header_value(_key) - _value = _unicodify_header_value(_value) - self._validate_value(_value) - if not self._list: - self._list.append((_key, _value)) - return - listiter = iter(self._list) - ikey = _key.lower() - for idx, (old_key, _old_value) in enumerate(listiter): - if old_key.lower() == ikey: - # replace first occurrence - self._list[idx] = (_key, _value) - break - else: - self._list.append((_key, _value)) - return - self._list[idx + 1 :] = [t for t in listiter if t[0].lower() != ikey] - - def setlist(self, key, values): - """Remove any existing values for a header and add new ones. - - :param key: The header key to set. - :param values: An iterable of values to set for the key. - - .. versionadded:: 1.0 - """ - if values: - values_iter = iter(values) - self.set(key, next(values_iter)) - - for value in values_iter: - self.add(key, value) - else: - self.remove(key) - - def setdefault(self, key, default): - """Return the first value for the key if it is in the headers, - otherwise set the header to the value given by ``default`` and - return that. - - :param key: The header key to get. - :param default: The value to set for the key if it is not in the - headers. - """ - if key in self: - return self[key] - - self.set(key, default) - return default - - def setlistdefault(self, key, default): - """Return the list of values for the key if it is in the - headers, otherwise set the header to the list of values given - by ``default`` and return that. - - Unlike :meth:`MultiDict.setlistdefault`, modifying the returned - list will not affect the headers. - - :param key: The header key to get. - :param default: An iterable of values to set for the key if it - is not in the headers. - - .. versionadded:: 1.0 - """ - if key not in self: - self.setlist(key, default) - - return self.getlist(key) - - def __setitem__(self, key, value): - """Like :meth:`set` but also supports index/slice based setting.""" - if isinstance(key, (slice, int)): - if isinstance(key, int): - value = [value] - value = [ - (_unicodify_header_value(k), _unicodify_header_value(v)) - for (k, v) in value - ] - for _, v in value: - self._validate_value(v) - if isinstance(key, int): - self._list[key] = value[0] - else: - self._list[key] = value - else: - self.set(key, value) - - def update(self, *args, **kwargs): - """Replace headers in this object with items from another - headers object and keyword arguments. - - To extend existing keys instead of replacing, use :meth:`extend` - instead. - - If provided, the first argument can be another :class:`Headers` - object, a :class:`MultiDict`, :class:`dict`, or iterable of - pairs. - - .. versionadded:: 1.0 - """ - if len(args) > 1: - raise TypeError(f"update expected at most 1 arguments, got {len(args)}") - - if args: - mapping = args[0] - - if isinstance(mapping, (Headers, MultiDict)): - for key in mapping.keys(): - self.setlist(key, mapping.getlist(key)) - elif isinstance(mapping, dict): - for key, value in mapping.items(): - if isinstance(value, (list, tuple)): - self.setlist(key, value) - else: - self.set(key, value) - else: - for key, value in mapping: - self.set(key, value) - - for key, value in kwargs.items(): - if isinstance(value, (list, tuple)): - self.setlist(key, value) - else: - self.set(key, value) - - def to_wsgi_list(self): - """Convert the headers into a list suitable for WSGI. - - :return: list - """ - return list(self) - - def copy(self): - return self.__class__(self._list) - - def __copy__(self): - return self.copy() - - def __str__(self): - """Returns formatted headers suitable for HTTP transmission.""" - strs = [] - for key, value in self.to_wsgi_list(): - strs.append(f"{key}: {value}") - strs.append("\r\n") - return "\r\n".join(strs) - - def __repr__(self): - return f"{type(self).__name__}({list(self)!r})" - - -class ImmutableHeadersMixin: - """Makes a :class:`Headers` immutable. We do not mark them as - hashable though since the only usecase for this datastructure - in Werkzeug is a view on a mutable structure. - - .. versionadded:: 0.5 - - :private: - """ - - def __delitem__(self, key, **kwargs): - is_immutable(self) - - def __setitem__(self, key, value): - is_immutable(self) - - def set(self, _key, _value, **kw): - is_immutable(self) - - def setlist(self, key, values): - is_immutable(self) - - def add(self, _key, _value, **kw): - is_immutable(self) - - def add_header(self, _key, _value, **_kw): - is_immutable(self) - - def remove(self, key): - is_immutable(self) - - def extend(self, *args, **kwargs): - is_immutable(self) - - def update(self, *args, **kwargs): - is_immutable(self) - - def insert(self, pos, value): - is_immutable(self) - - def pop(self, key=None, default=_missing): - is_immutable(self) - - def popitem(self): - is_immutable(self) - - def setdefault(self, key, default): - is_immutable(self) - - def setlistdefault(self, key, default): - is_immutable(self) - - -class EnvironHeaders(ImmutableHeadersMixin, Headers): - """Read only version of the headers from a WSGI environment. This - provides the same interface as `Headers` and is constructed from - a WSGI environment. - - From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a - subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will - render a page for a ``400 BAD REQUEST`` if caught in a catch-all for - HTTP exceptions. - """ - - def __init__(self, environ): - self.environ = environ - - def __eq__(self, other): - return self.environ is other.environ - - __hash__ = None - - def __getitem__(self, key, _get_mode=False): - # _get_mode is a no-op for this class as there is no index but - # used because get() calls it. - if not isinstance(key, str): - raise KeyError(key) - key = key.upper().replace("-", "_") - if key in ("CONTENT_TYPE", "CONTENT_LENGTH"): - return _unicodify_header_value(self.environ[key]) - return _unicodify_header_value(self.environ[f"HTTP_{key}"]) - - def __len__(self): - # the iter is necessary because otherwise list calls our - # len which would call list again and so forth. - return len(list(iter(self))) - - def __iter__(self): - for key, value in self.environ.items(): - if key.startswith("HTTP_") and key not in ( - "HTTP_CONTENT_TYPE", - "HTTP_CONTENT_LENGTH", - ): - yield ( - key[5:].replace("_", "-").title(), - _unicodify_header_value(value), - ) - elif key in ("CONTENT_TYPE", "CONTENT_LENGTH") and value: - yield (key.replace("_", "-").title(), _unicodify_header_value(value)) - - def copy(self): - raise TypeError(f"cannot create {type(self).__name__!r} copies") - - -class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict): - """A read only :class:`MultiDict` that you can pass multiple :class:`MultiDict` - instances as sequence and it will combine the return values of all wrapped - dicts: - - >>> from werkzeug.datastructures import CombinedMultiDict, MultiDict - >>> post = MultiDict([('foo', 'bar')]) - >>> get = MultiDict([('blub', 'blah')]) - >>> combined = CombinedMultiDict([get, post]) - >>> combined['foo'] - 'bar' - >>> combined['blub'] - 'blah' - - This works for all read operations and will raise a `TypeError` for - methods that usually change data which isn't possible. - - From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a - subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will - render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP - exceptions. - """ - - def __reduce_ex__(self, protocol): - return type(self), (self.dicts,) - - def __init__(self, dicts=None): - self.dicts = list(dicts) or [] - - @classmethod - def fromkeys(cls, keys, value=None): - raise TypeError(f"cannot create {cls.__name__!r} instances by fromkeys") - - def __getitem__(self, key): - for d in self.dicts: - if key in d: - return d[key] - raise exceptions.BadRequestKeyError(key) - - def get(self, key, default=None, type=None): - for d in self.dicts: - if key in d: - if type is not None: - try: - return type(d[key]) - except ValueError: - continue - return d[key] - return default - - def getlist(self, key, type=None): - rv = [] - for d in self.dicts: - rv.extend(d.getlist(key, type)) - return rv - - def _keys_impl(self): - """This function exists so __len__ can be implemented more efficiently, - saving one list creation from an iterator. - """ - rv = set() - rv.update(*self.dicts) - return rv - - def keys(self): - return self._keys_impl() - - def __iter__(self): - return iter(self.keys()) - - def items(self, multi=False): - found = set() - for d in self.dicts: - for key, value in d.items(multi): - if multi: - yield key, value - elif key not in found: - found.add(key) - yield key, value - - def values(self): - for _key, value in self.items(): - yield value - - def lists(self): - rv = {} - for d in self.dicts: - for key, values in d.lists(): - rv.setdefault(key, []).extend(values) - return list(rv.items()) - - def listvalues(self): - return (x[1] for x in self.lists()) - - def copy(self): - """Return a shallow mutable copy of this object. - - This returns a :class:`MultiDict` representing the data at the - time of copying. The copy will no longer reflect changes to the - wrapped dicts. - - .. versionchanged:: 0.15 - Return a mutable :class:`MultiDict`. - """ - return MultiDict(self) - - def to_dict(self, flat=True): - """Return the contents as regular dict. If `flat` is `True` the - returned dict will only have the first item present, if `flat` is - `False` all values will be returned as lists. - - :param flat: If set to `False` the dict returned will have lists - with all the values in it. Otherwise it will only - contain the first item for each key. - :return: a :class:`dict` - """ - if flat: - return dict(self.items()) - - return dict(self.lists()) - - def __len__(self): - return len(self._keys_impl()) - - def __contains__(self, key): - for d in self.dicts: - if key in d: - return True - return False - - def __repr__(self): - return f"{type(self).__name__}({self.dicts!r})" - - -class FileMultiDict(MultiDict): - """A special :class:`MultiDict` that has convenience methods to add - files to it. This is used for :class:`EnvironBuilder` and generally - useful for unittesting. - - .. versionadded:: 0.5 - """ - - def add_file(self, name, file, filename=None, content_type=None): - """Adds a new file to the dict. `file` can be a file name or - a :class:`file`-like or a :class:`FileStorage` object. - - :param name: the name of the field. - :param file: a filename or :class:`file`-like object - :param filename: an optional filename - :param content_type: an optional content type - """ - if isinstance(file, FileStorage): - value = file - else: - if isinstance(file, str): - if filename is None: - filename = file - file = open(file, "rb") - if filename and content_type is None: - content_type = ( - mimetypes.guess_type(filename)[0] or "application/octet-stream" - ) - value = FileStorage(file, filename, name, content_type) - - self.add(name, value) - - -class ImmutableDict(ImmutableDictMixin, dict): - """An immutable :class:`dict`. - - .. versionadded:: 0.5 - """ - - def __repr__(self): - return f"{type(self).__name__}({dict.__repr__(self)})" - - def copy(self): - """Return a shallow mutable copy of this object. Keep in mind that - the standard library's :func:`copy` function is a no-op for this class - like for any other python immutable type (eg: :class:`tuple`). - """ - return dict(self) - - def __copy__(self): - return self - - -class ImmutableMultiDict(ImmutableMultiDictMixin, MultiDict): - """An immutable :class:`MultiDict`. - - .. versionadded:: 0.5 - """ - - def copy(self): - """Return a shallow mutable copy of this object. Keep in mind that - the standard library's :func:`copy` function is a no-op for this class - like for any other python immutable type (eg: :class:`tuple`). - """ - return MultiDict(self) - - def __copy__(self): - return self - - -class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict): - """An immutable :class:`OrderedMultiDict`. - - .. versionadded:: 0.6 - """ - - def _iter_hashitems(self): - return enumerate(self.items(multi=True)) - - def copy(self): - """Return a shallow mutable copy of this object. Keep in mind that - the standard library's :func:`copy` function is a no-op for this class - like for any other python immutable type (eg: :class:`tuple`). - """ - return OrderedMultiDict(self) - - def __copy__(self): - return self - - -class Accept(ImmutableList): - """An :class:`Accept` object is just a list subclass for lists of - ``(value, quality)`` tuples. It is automatically sorted by specificity - and quality. - - All :class:`Accept` objects work similar to a list but provide extra - functionality for working with the data. Containment checks are - normalized to the rules of that header: - - >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)]) - >>> a.best - 'ISO-8859-1' - >>> 'iso-8859-1' in a - True - >>> 'UTF8' in a - True - >>> 'utf7' in a - False - - To get the quality for an item you can use normal item lookup: - - >>> print a['utf-8'] - 0.7 - >>> a['utf7'] - 0 - - .. versionchanged:: 0.5 - :class:`Accept` objects are forced immutable now. - - .. versionchanged:: 1.0.0 - :class:`Accept` internal values are no longer ordered - alphabetically for equal quality tags. Instead the initial - order is preserved. - - """ - - def __init__(self, values=()): - if values is None: - list.__init__(self) - self.provided = False - elif isinstance(values, Accept): - self.provided = values.provided - list.__init__(self, values) - else: - self.provided = True - values = sorted( - values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True - ) - list.__init__(self, values) - - def _specificity(self, value): - """Returns a tuple describing the value's specificity.""" - return (value != "*",) - - def _value_matches(self, value, item): - """Check if a value matches a given accept item.""" - return item == "*" or item.lower() == value.lower() - - def __getitem__(self, key): - """Besides index lookup (getting item n) you can also pass it a string - to get the quality for the item. If the item is not in the list, the - returned quality is ``0``. - """ - if isinstance(key, str): - return self.quality(key) - return list.__getitem__(self, key) - - def quality(self, key): - """Returns the quality of the key. - - .. versionadded:: 0.6 - In previous versions you had to use the item-lookup syntax - (eg: ``obj[key]`` instead of ``obj.quality(key)``) - """ - for item, quality in self: - if self._value_matches(key, item): - return quality - return 0 - - def __contains__(self, value): - for item, _quality in self: - if self._value_matches(value, item): - return True - return False - - def __repr__(self): - pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self) - return f"{type(self).__name__}([{pairs_str}])" - - def index(self, key): - """Get the position of an entry or raise :exc:`ValueError`. - - :param key: The key to be looked up. - - .. versionchanged:: 0.5 - This used to raise :exc:`IndexError`, which was inconsistent - with the list API. - """ - if isinstance(key, str): - for idx, (item, _quality) in enumerate(self): - if self._value_matches(key, item): - return idx - raise ValueError(key) - return list.index(self, key) - - def find(self, key): - """Get the position of an entry or return -1. - - :param key: The key to be looked up. - """ - try: - return self.index(key) - except ValueError: - return -1 - - def values(self): - """Iterate over all values.""" - for item in self: - yield item[0] - - def to_header(self): - """Convert the header set into an HTTP header string.""" - result = [] - for value, quality in self: - if quality != 1: - value = f"{value};q={quality}" - result.append(value) - return ",".join(result) - - def __str__(self): - return self.to_header() - - def _best_single_match(self, match): - for client_item, quality in self: - if self._value_matches(match, client_item): - # self is sorted by specificity descending, we can exit - return client_item, quality - return None - - def best_match(self, matches, default=None): - """Returns the best match from a list of possible matches based - on the specificity and quality of the client. If two items have the - same quality and specificity, the one is returned that comes first. - - :param matches: a list of matches to check for - :param default: the value that is returned if none match - """ - result = default - best_quality = -1 - best_specificity = (-1,) - for server_item in matches: - match = self._best_single_match(server_item) - if not match: - continue - client_item, quality = match - specificity = self._specificity(client_item) - if quality <= 0 or quality < best_quality: - continue - # better quality or same quality but more specific => better match - if quality > best_quality or specificity > best_specificity: - result = server_item - best_quality = quality - best_specificity = specificity - return result - - @property - def best(self): - """The best match as value.""" - if self: - return self[0][0] - - -_mime_split_re = re.compile(r"/|(?:\s*;\s*)") - - -def _normalize_mime(value): - return _mime_split_re.split(value.lower()) - - -class MIMEAccept(Accept): - """Like :class:`Accept` but with special methods and behavior for - mimetypes. - """ - - def _specificity(self, value): - return tuple(x != "*" for x in _mime_split_re.split(value)) - - def _value_matches(self, value, item): - # item comes from the client, can't match if it's invalid. - if "/" not in item: - return False - - # value comes from the application, tell the developer when it - # doesn't look valid. - if "/" not in value: - raise ValueError(f"invalid mimetype {value!r}") - - # Split the match value into type, subtype, and a sorted list of parameters. - normalized_value = _normalize_mime(value) - value_type, value_subtype = normalized_value[:2] - value_params = sorted(normalized_value[2:]) - - # "*/*" is the only valid value that can start with "*". - if value_type == "*" and value_subtype != "*": - raise ValueError(f"invalid mimetype {value!r}") - - # Split the accept item into type, subtype, and parameters. - normalized_item = _normalize_mime(item) - item_type, item_subtype = normalized_item[:2] - item_params = sorted(normalized_item[2:]) - - # "*/not-*" from the client is invalid, can't match. - if item_type == "*" and item_subtype != "*": - return False - - return ( - (item_type == "*" and item_subtype == "*") - or (value_type == "*" and value_subtype == "*") - ) or ( - item_type == value_type - and ( - item_subtype == "*" - or value_subtype == "*" - or (item_subtype == value_subtype and item_params == value_params) - ) - ) - - @property - def accept_html(self): - """True if this object accepts HTML.""" - return ( - "text/html" in self or "application/xhtml+xml" in self or self.accept_xhtml - ) - - @property - def accept_xhtml(self): - """True if this object accepts XHTML.""" - return "application/xhtml+xml" in self or "application/xml" in self - - @property - def accept_json(self): - """True if this object accepts JSON.""" - return "application/json" in self - - -_locale_delim_re = re.compile(r"[_-]") - - -def _normalize_lang(value): - """Process a language tag for matching.""" - return _locale_delim_re.split(value.lower()) - - -class LanguageAccept(Accept): - """Like :class:`Accept` but with normalization for language tags.""" - - def _value_matches(self, value, item): - return item == "*" or _normalize_lang(value) == _normalize_lang(item) - - def best_match(self, matches, default=None): - """Given a list of supported values, finds the best match from - the list of accepted values. - - Language tags are normalized for the purpose of matching, but - are returned unchanged. - - If no exact match is found, this will fall back to matching - the first subtag (primary language only), first with the - accepted values then with the match values. This partial is not - applied to any other language subtags. - - The default is returned if no exact or fallback match is found. - - :param matches: A list of supported languages to find a match. - :param default: The value that is returned if none match. - """ - # Look for an exact match first. If a client accepts "en-US", - # "en-US" is a valid match at this point. - result = super().best_match(matches) - - if result is not None: - return result - - # Fall back to accepting primary tags. If a client accepts - # "en-US", "en" is a valid match at this point. Need to use - # re.split to account for 2 or 3 letter codes. - fallback = Accept( - [(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self] - ) - result = fallback.best_match(matches) - - if result is not None: - return result - - # Fall back to matching primary tags. If the client accepts - # "en", "en-US" is a valid match at this point. - fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches] - result = super().best_match(fallback_matches) - - # Return a value from the original match list. Find the first - # original value that starts with the matched primary tag. - if result is not None: - return next(item for item in matches if item.startswith(result)) - - return default - - -class CharsetAccept(Accept): - """Like :class:`Accept` but with normalization for charsets.""" - - def _value_matches(self, value, item): - def _normalize(name): - try: - return codecs.lookup(name).name - except LookupError: - return name.lower() - - return item == "*" or _normalize(value) == _normalize(item) - - -def cache_control_property(key, empty, type): - """Return a new property object for a cache header. Useful if you - want to add support for a cache extension in a subclass. - - .. versionchanged:: 2.0 - Renamed from ``cache_property``. - """ - return property( - lambda x: x._get_cache_value(key, empty, type), - lambda x, v: x._set_cache_value(key, v, type), - lambda x: x._del_cache_value(key), - f"accessor for {key!r}", - ) - - -class _CacheControl(UpdateDictMixin, dict): - """Subclass of a dict that stores values for a Cache-Control header. It - has accessors for all the cache-control directives specified in RFC 2616. - The class does not differentiate between request and response directives. - - Because the cache-control directives in the HTTP header use dashes the - python descriptors use underscores for that. - - To get a header of the :class:`CacheControl` object again you can convert - the object into a string or call the :meth:`to_header` method. If you plan - to subclass it and add your own items have a look at the sourcecode for - that class. - - .. versionchanged:: 2.1.0 - Setting int properties such as ``max_age`` will convert the - value to an int. - - .. versionchanged:: 0.4 - - Setting `no_cache` or `private` to boolean `True` will set the implicit - none-value which is ``*``: - - >>> cc = ResponseCacheControl() - >>> cc.no_cache = True - >>> cc - <ResponseCacheControl 'no-cache'> - >>> cc.no_cache - '*' - >>> cc.no_cache = None - >>> cc - <ResponseCacheControl ''> - - In versions before 0.5 the behavior documented here affected the now - no longer existing `CacheControl` class. - """ - - no_cache = cache_control_property("no-cache", "*", None) - no_store = cache_control_property("no-store", None, bool) - max_age = cache_control_property("max-age", -1, int) - no_transform = cache_control_property("no-transform", None, None) - - def __init__(self, values=(), on_update=None): - dict.__init__(self, values or ()) - self.on_update = on_update - self.provided = values is not None - - def _get_cache_value(self, key, empty, type): - """Used internally by the accessor properties.""" - if type is bool: - return key in self - if key in self: - value = self[key] - if value is None: - return empty - elif type is not None: - try: - value = type(value) - except ValueError: - pass - return value - return None - - def _set_cache_value(self, key, value, type): - """Used internally by the accessor properties.""" - if type is bool: - if value: - self[key] = None - else: - self.pop(key, None) - else: - if value is None: - self.pop(key, None) - elif value is True: - self[key] = None - else: - if type is not None: - self[key] = type(value) - else: - self[key] = value - - def _del_cache_value(self, key): - """Used internally by the accessor properties.""" - if key in self: - del self[key] - - def to_header(self): - """Convert the stored values into a cache control header.""" - return http.dump_header(self) - - def __str__(self): - return self.to_header() - - def __repr__(self): - kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) - return f"<{type(self).__name__} {kv_str}>" - - cache_property = staticmethod(cache_control_property) - - -class RequestCacheControl(ImmutableDictMixin, _CacheControl): - """A cache control for requests. This is immutable and gives access - to all the request-relevant cache control headers. - - To get a header of the :class:`RequestCacheControl` object again you can - convert the object into a string or call the :meth:`to_header` method. If - you plan to subclass it and add your own items have a look at the sourcecode - for that class. - - .. versionchanged:: 2.1.0 - Setting int properties such as ``max_age`` will convert the - value to an int. - - .. versionadded:: 0.5 - In previous versions a `CacheControl` class existed that was used - both for request and response. - """ - - max_stale = cache_control_property("max-stale", "*", int) - min_fresh = cache_control_property("min-fresh", "*", int) - only_if_cached = cache_control_property("only-if-cached", None, bool) - - -class ResponseCacheControl(_CacheControl): - """A cache control for responses. Unlike :class:`RequestCacheControl` - this is mutable and gives access to response-relevant cache control - headers. - - To get a header of the :class:`ResponseCacheControl` object again you can - convert the object into a string or call the :meth:`to_header` method. If - you plan to subclass it and add your own items have a look at the sourcecode - for that class. - - .. versionchanged:: 2.1.1 - ``s_maxage`` converts the value to an int. - - .. versionchanged:: 2.1.0 - Setting int properties such as ``max_age`` will convert the - value to an int. - - .. versionadded:: 0.5 - In previous versions a `CacheControl` class existed that was used - both for request and response. - """ - - public = cache_control_property("public", None, bool) - private = cache_control_property("private", "*", None) - must_revalidate = cache_control_property("must-revalidate", None, bool) - proxy_revalidate = cache_control_property("proxy-revalidate", None, bool) - s_maxage = cache_control_property("s-maxage", None, int) - immutable = cache_control_property("immutable", None, bool) - - -def csp_property(key): - """Return a new property object for a content security policy header. - Useful if you want to add support for a csp extension in a - subclass. - """ - return property( - lambda x: x._get_value(key), - lambda x, v: x._set_value(key, v), - lambda x: x._del_value(key), - f"accessor for {key!r}", - ) - - -class ContentSecurityPolicy(UpdateDictMixin, dict): - """Subclass of a dict that stores values for a Content Security Policy - header. It has accessors for all the level 3 policies. - - Because the csp directives in the HTTP header use dashes the - python descriptors use underscores for that. - - To get a header of the :class:`ContentSecuirtyPolicy` object again - you can convert the object into a string or call the - :meth:`to_header` method. If you plan to subclass it and add your - own items have a look at the sourcecode for that class. - - .. versionadded:: 1.0.0 - Support for Content Security Policy headers was added. - - """ - - base_uri = csp_property("base-uri") - child_src = csp_property("child-src") - connect_src = csp_property("connect-src") - default_src = csp_property("default-src") - font_src = csp_property("font-src") - form_action = csp_property("form-action") - frame_ancestors = csp_property("frame-ancestors") - frame_src = csp_property("frame-src") - img_src = csp_property("img-src") - manifest_src = csp_property("manifest-src") - media_src = csp_property("media-src") - navigate_to = csp_property("navigate-to") - object_src = csp_property("object-src") - prefetch_src = csp_property("prefetch-src") - plugin_types = csp_property("plugin-types") - report_to = csp_property("report-to") - report_uri = csp_property("report-uri") - sandbox = csp_property("sandbox") - script_src = csp_property("script-src") - script_src_attr = csp_property("script-src-attr") - script_src_elem = csp_property("script-src-elem") - style_src = csp_property("style-src") - style_src_attr = csp_property("style-src-attr") - style_src_elem = csp_property("style-src-elem") - worker_src = csp_property("worker-src") - - def __init__(self, values=(), on_update=None): - dict.__init__(self, values or ()) - self.on_update = on_update - self.provided = values is not None - - def _get_value(self, key): - """Used internally by the accessor properties.""" - return self.get(key) - - def _set_value(self, key, value): - """Used internally by the accessor properties.""" - if value is None: - self.pop(key, None) - else: - self[key] = value - - def _del_value(self, key): - """Used internally by the accessor properties.""" - if key in self: - del self[key] - - def to_header(self): - """Convert the stored values into a cache control header.""" - return http.dump_csp_header(self) - - def __str__(self): - return self.to_header() - - def __repr__(self): - kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) - return f"<{type(self).__name__} {kv_str}>" - - -class CallbackDict(UpdateDictMixin, dict): - """A dict that calls a function passed every time something is changed. - The function is passed the dict instance. - """ - - def __init__(self, initial=None, on_update=None): - dict.__init__(self, initial or ()) - self.on_update = on_update - - def __repr__(self): - return f"<{type(self).__name__} {dict.__repr__(self)}>" - - -class HeaderSet(MutableSet): - """Similar to the :class:`ETags` class this implements a set-like structure. - Unlike :class:`ETags` this is case insensitive and used for vary, allow, and - content-language headers. - - If not constructed using the :func:`parse_set_header` function the - instantiation works like this: - - >>> hs = HeaderSet(['foo', 'bar', 'baz']) - >>> hs - HeaderSet(['foo', 'bar', 'baz']) - """ - - def __init__(self, headers=None, on_update=None): - self._headers = list(headers or ()) - self._set = {x.lower() for x in self._headers} - self.on_update = on_update - - def add(self, header): - """Add a new header to the set.""" - self.update((header,)) - - def remove(self, header): - """Remove a header from the set. This raises an :exc:`KeyError` if the - header is not in the set. - - .. versionchanged:: 0.5 - In older versions a :exc:`IndexError` was raised instead of a - :exc:`KeyError` if the object was missing. - - :param header: the header to be removed. - """ - key = header.lower() - if key not in self._set: - raise KeyError(header) - self._set.remove(key) - for idx, key in enumerate(self._headers): - if key.lower() == header: - del self._headers[idx] - break - if self.on_update is not None: - self.on_update(self) - - def update(self, iterable): - """Add all the headers from the iterable to the set. - - :param iterable: updates the set with the items from the iterable. - """ - inserted_any = False - for header in iterable: - key = header.lower() - if key not in self._set: - self._headers.append(header) - self._set.add(key) - inserted_any = True - if inserted_any and self.on_update is not None: - self.on_update(self) - - def discard(self, header): - """Like :meth:`remove` but ignores errors. - - :param header: the header to be discarded. - """ - try: - self.remove(header) - except KeyError: - pass - - def find(self, header): - """Return the index of the header in the set or return -1 if not found. - - :param header: the header to be looked up. - """ - header = header.lower() - for idx, item in enumerate(self._headers): - if item.lower() == header: - return idx - return -1 - - def index(self, header): - """Return the index of the header in the set or raise an - :exc:`IndexError`. - - :param header: the header to be looked up. - """ - rv = self.find(header) - if rv < 0: - raise IndexError(header) - return rv - - def clear(self): - """Clear the set.""" - self._set.clear() - del self._headers[:] - if self.on_update is not None: - self.on_update(self) - - def as_set(self, preserve_casing=False): - """Return the set as real python set type. When calling this, all - the items are converted to lowercase and the ordering is lost. - - :param preserve_casing: if set to `True` the items in the set returned - will have the original case like in the - :class:`HeaderSet`, otherwise they will - be lowercase. - """ - if preserve_casing: - return set(self._headers) - return set(self._set) - - def to_header(self): - """Convert the header set into an HTTP header string.""" - return ", ".join(map(http.quote_header_value, self._headers)) - - def __getitem__(self, idx): - return self._headers[idx] - - def __delitem__(self, idx): - rv = self._headers.pop(idx) - self._set.remove(rv.lower()) - if self.on_update is not None: - self.on_update(self) - - def __setitem__(self, idx, value): - old = self._headers[idx] - self._set.remove(old.lower()) - self._headers[idx] = value - self._set.add(value.lower()) - if self.on_update is not None: - self.on_update(self) - - def __contains__(self, header): - return header.lower() in self._set - - def __len__(self): - return len(self._set) - - def __iter__(self): - return iter(self._headers) - - def __bool__(self): - return bool(self._set) - - def __str__(self): - return self.to_header() - - def __repr__(self): - return f"{type(self).__name__}({self._headers!r})" - - -class ETags(Collection): - """A set that can be used to check if one etag is present in a collection - of etags. - """ - - def __init__(self, strong_etags=None, weak_etags=None, star_tag=False): - if not star_tag and strong_etags: - self._strong = frozenset(strong_etags) - else: - self._strong = frozenset() - - self._weak = frozenset(weak_etags or ()) - self.star_tag = star_tag - - def as_set(self, include_weak=False): - """Convert the `ETags` object into a python set. Per default all the - weak etags are not part of this set.""" - rv = set(self._strong) - if include_weak: - rv.update(self._weak) - return rv - - def is_weak(self, etag): - """Check if an etag is weak.""" - return etag in self._weak - - def is_strong(self, etag): - """Check if an etag is strong.""" - return etag in self._strong - - def contains_weak(self, etag): - """Check if an etag is part of the set including weak and strong tags.""" - return self.is_weak(etag) or self.contains(etag) - - def contains(self, etag): - """Check if an etag is part of the set ignoring weak tags. - It is also possible to use the ``in`` operator. - """ - if self.star_tag: - return True - return self.is_strong(etag) - - def contains_raw(self, etag): - """When passed a quoted tag it will check if this tag is part of the - set. If the tag is weak it is checked against weak and strong tags, - otherwise strong only.""" - etag, weak = http.unquote_etag(etag) - if weak: - return self.contains_weak(etag) - return self.contains(etag) - - def to_header(self): - """Convert the etags set into a HTTP header string.""" - if self.star_tag: - return "*" - return ", ".join( - [f'"{x}"' for x in self._strong] + [f'W/"{x}"' for x in self._weak] - ) - - def __call__(self, etag=None, data=None, include_weak=False): - if [etag, data].count(None) != 1: - raise TypeError("either tag or data required, but at least one") - if etag is None: - etag = http.generate_etag(data) - if include_weak: - if etag in self._weak: - return True - return etag in self._strong - - def __bool__(self): - return bool(self.star_tag or self._strong or self._weak) - - def __str__(self): - return self.to_header() - - def __len__(self): - return len(self._strong) - - def __iter__(self): - return iter(self._strong) - - def __contains__(self, etag): - return self.contains(etag) - - def __repr__(self): - return f"<{type(self).__name__} {str(self)!r}>" - - -class IfRange: - """Very simple object that represents the `If-Range` header in parsed - form. It will either have neither a etag or date or one of either but - never both. - - .. versionadded:: 0.7 - """ - - def __init__(self, etag=None, date=None): - #: The etag parsed and unquoted. Ranges always operate on strong - #: etags so the weakness information is not necessary. - self.etag = etag - #: The date in parsed format or `None`. - self.date = date - - def to_header(self): - """Converts the object back into an HTTP header.""" - if self.date is not None: - return http.http_date(self.date) - if self.etag is not None: - return http.quote_etag(self.etag) - return "" - - def __str__(self): - return self.to_header() - - def __repr__(self): - return f"<{type(self).__name__} {str(self)!r}>" - - -class Range: - """Represents a ``Range`` header. All methods only support only - bytes as the unit. Stores a list of ranges if given, but the methods - only work if only one range is provided. - - :raise ValueError: If the ranges provided are invalid. - - .. versionchanged:: 0.15 - The ranges passed in are validated. - - .. versionadded:: 0.7 - """ - - def __init__(self, units, ranges): - #: The units of this range. Usually "bytes". - self.units = units - #: A list of ``(begin, end)`` tuples for the range header provided. - #: The ranges are non-inclusive. - self.ranges = ranges - - for start, end in ranges: - if start is None or (end is not None and (start < 0 or start >= end)): - raise ValueError(f"{(start, end)} is not a valid range.") - - def range_for_length(self, length): - """If the range is for bytes, the length is not None and there is - exactly one range and it is satisfiable it returns a ``(start, stop)`` - tuple, otherwise `None`. - """ - if self.units != "bytes" or length is None or len(self.ranges) != 1: - return None - start, end = self.ranges[0] - if end is None: - end = length - if start < 0: - start += length - if http.is_byte_range_valid(start, end, length): - return start, min(end, length) - return None - - def make_content_range(self, length): - """Creates a :class:`~werkzeug.datastructures.ContentRange` object - from the current range and given content length. - """ - rng = self.range_for_length(length) - if rng is not None: - return ContentRange(self.units, rng[0], rng[1], length) - return None - - def to_header(self): - """Converts the object back into an HTTP header.""" - ranges = [] - for begin, end in self.ranges: - if end is None: - ranges.append(f"{begin}-" if begin >= 0 else str(begin)) - else: - ranges.append(f"{begin}-{end - 1}") - return f"{self.units}={','.join(ranges)}" - - def to_content_range_header(self, length): - """Converts the object into `Content-Range` HTTP header, - based on given length - """ - range = self.range_for_length(length) - if range is not None: - return f"{self.units} {range[0]}-{range[1] - 1}/{length}" - return None - - def __str__(self): - return self.to_header() - - def __repr__(self): - return f"<{type(self).__name__} {str(self)!r}>" - - -def _callback_property(name): - def fget(self): - return getattr(self, name) - - def fset(self, value): - setattr(self, name, value) - if self.on_update is not None: - self.on_update(self) - - return property(fget, fset) - - -class ContentRange: - """Represents the content range header. - - .. versionadded:: 0.7 - """ - - def __init__(self, units, start, stop, length=None, on_update=None): - assert http.is_byte_range_valid(start, stop, length), "Bad range provided" - self.on_update = on_update - self.set(start, stop, length, units) - - #: The units to use, usually "bytes" - units = _callback_property("_units") - #: The start point of the range or `None`. - start = _callback_property("_start") - #: The stop point of the range (non-inclusive) or `None`. Can only be - #: `None` if also start is `None`. - stop = _callback_property("_stop") - #: The length of the range or `None`. - length = _callback_property("_length") - - def set(self, start, stop, length=None, units="bytes"): - """Simple method to update the ranges.""" - assert http.is_byte_range_valid(start, stop, length), "Bad range provided" - self._units = units - self._start = start - self._stop = stop - self._length = length - if self.on_update is not None: - self.on_update(self) - - def unset(self): - """Sets the units to `None` which indicates that the header should - no longer be used. - """ - self.set(None, None, units=None) - - def to_header(self): - if self.units is None: - return "" - if self.length is None: - length = "*" - else: - length = self.length - if self.start is None: - return f"{self.units} */{length}" - return f"{self.units} {self.start}-{self.stop - 1}/{length}" - - def __bool__(self): - return self.units is not None - - def __str__(self): - return self.to_header() - - def __repr__(self): - return f"<{type(self).__name__} {str(self)!r}>" - - -class Authorization(ImmutableDictMixin, dict): - """Represents an ``Authorization`` header sent by the client. - - This is returned by - :func:`~werkzeug.http.parse_authorization_header`. It can be useful - to create the object manually to pass to the test - :class:`~werkzeug.test.Client`. - - .. versionchanged:: 0.5 - This object became immutable. - """ - - def __init__(self, auth_type, data=None): - dict.__init__(self, data or {}) - self.type = auth_type - - @property - def username(self): - """The username transmitted. This is set for both basic and digest - auth all the time. - """ - return self.get("username") - - @property - def password(self): - """When the authentication type is basic this is the password - transmitted by the client, else `None`. - """ - return self.get("password") - - @property - def realm(self): - """This is the server realm sent back for HTTP digest auth.""" - return self.get("realm") - - @property - def nonce(self): - """The nonce the server sent for digest auth, sent back by the client. - A nonce should be unique for every 401 response for HTTP digest auth. - """ - return self.get("nonce") - - @property - def uri(self): - """The URI from Request-URI of the Request-Line; duplicated because - proxies are allowed to change the Request-Line in transit. HTTP - digest auth only. - """ - return self.get("uri") - - @property - def nc(self): - """The nonce count value transmitted by clients if a qop-header is - also transmitted. HTTP digest auth only. - """ - return self.get("nc") - - @property - def cnonce(self): - """If the server sent a qop-header in the ``WWW-Authenticate`` - header, the client has to provide this value for HTTP digest auth. - See the RFC for more details. - """ - return self.get("cnonce") - - @property - def response(self): - """A string of 32 hex digits computed as defined in RFC 2617, which - proves that the user knows a password. Digest auth only. - """ - return self.get("response") - - @property - def opaque(self): - """The opaque header from the server returned unchanged by the client. - It is recommended that this string be base64 or hexadecimal data. - Digest auth only. - """ - return self.get("opaque") - - @property - def qop(self): - """Indicates what "quality of protection" the client has applied to - the message for HTTP digest auth. Note that this is a single token, - not a quoted list of alternatives as in WWW-Authenticate. - """ - return self.get("qop") - - def to_header(self): - """Convert to a string value for an ``Authorization`` header. - - .. versionadded:: 2.0 - Added to support passing authorization to the test client. - """ - if self.type == "basic": - value = base64.b64encode( - f"{self.username}:{self.password}".encode() - ).decode("utf8") - return f"Basic {value}" - - if self.type == "digest": - return f"Digest {http.dump_header(self)}" - - raise ValueError(f"Unsupported type {self.type!r}.") - - -def auth_property(name, doc=None): - """A static helper function for Authentication subclasses to add - extra authentication system properties onto a class:: - - class FooAuthenticate(WWWAuthenticate): - special_realm = auth_property('special_realm') - - For more information have a look at the sourcecode to see how the - regular properties (:attr:`realm` etc.) are implemented. - """ - - def _set_value(self, value): - if value is None: - self.pop(name, None) - else: - self[name] = str(value) - - return property(lambda x: x.get(name), _set_value, doc=doc) - - -def _set_property(name, doc=None): - def fget(self): - def on_update(header_set): - if not header_set and name in self: - del self[name] - elif header_set: - self[name] = header_set.to_header() - - return http.parse_set_header(self.get(name), on_update) - - return property(fget, doc=doc) - - -class WWWAuthenticate(UpdateDictMixin, dict): - """Provides simple access to `WWW-Authenticate` headers.""" - - #: list of keys that require quoting in the generated header - _require_quoting = frozenset(["domain", "nonce", "opaque", "realm", "qop"]) - - def __init__(self, auth_type=None, values=None, on_update=None): - dict.__init__(self, values or ()) - if auth_type: - self["__auth_type__"] = auth_type - self.on_update = on_update - - def set_basic(self, realm="authentication required"): - """Clear the auth info and enable basic auth.""" - dict.clear(self) - dict.update(self, {"__auth_type__": "basic", "realm": realm}) - if self.on_update: - self.on_update(self) - - def set_digest( - self, realm, nonce, qop=("auth",), opaque=None, algorithm=None, stale=False - ): - """Clear the auth info and enable digest auth.""" - d = { - "__auth_type__": "digest", - "realm": realm, - "nonce": nonce, - "qop": http.dump_header(qop), - } - if stale: - d["stale"] = "TRUE" - if opaque is not None: - d["opaque"] = opaque - if algorithm is not None: - d["algorithm"] = algorithm - dict.clear(self) - dict.update(self, d) - if self.on_update: - self.on_update(self) - - def to_header(self): - """Convert the stored values into a WWW-Authenticate header.""" - d = dict(self) - auth_type = d.pop("__auth_type__", None) or "basic" - kv_items = ( - (k, http.quote_header_value(v, allow_token=k not in self._require_quoting)) - for k, v in d.items() - ) - kv_string = ", ".join([f"{k}={v}" for k, v in kv_items]) - return f"{auth_type.title()} {kv_string}" - - def __str__(self): - return self.to_header() - - def __repr__(self): - return f"<{type(self).__name__} {self.to_header()!r}>" - - type = auth_property( - "__auth_type__", - doc="""The type of the auth mechanism. HTTP currently specifies - ``Basic`` and ``Digest``.""", - ) - realm = auth_property( - "realm", - doc="""A string to be displayed to users so they know which - username and password to use. This string should contain at - least the name of the host performing the authentication and - might additionally indicate the collection of users who might - have access.""", - ) - domain = _set_property( - "domain", - doc="""A list of URIs that define the protection space. If a URI - is an absolute path, it is relative to the canonical root URL of - the server being accessed.""", - ) - nonce = auth_property( - "nonce", - doc=""" - A server-specified data string which should be uniquely generated - each time a 401 response is made. It is recommended that this - string be base64 or hexadecimal data.""", - ) - opaque = auth_property( - "opaque", - doc="""A string of data, specified by the server, which should - be returned by the client unchanged in the Authorization header - of subsequent requests with URIs in the same protection space. - It is recommended that this string be base64 or hexadecimal - data.""", - ) - algorithm = auth_property( - "algorithm", - doc="""A string indicating a pair of algorithms used to produce - the digest and a checksum. If this is not present it is assumed - to be "MD5". If the algorithm is not understood, the challenge - should be ignored (and a different one used, if there is more - than one).""", - ) - qop = _set_property( - "qop", - doc="""A set of quality-of-privacy directives such as auth and - auth-int.""", - ) - - @property - def stale(self): - """A flag, indicating that the previous request from the client - was rejected because the nonce value was stale. - """ - val = self.get("stale") - if val is not None: - return val.lower() == "true" - - @stale.setter - def stale(self, value): - if value is None: - self.pop("stale", None) - else: - self["stale"] = "TRUE" if value else "FALSE" - - auth_property = staticmethod(auth_property) - - -class FileStorage: - """The :class:`FileStorage` class is a thin wrapper over incoming files. - It is used by the request object to represent uploaded files. All the - attributes of the wrapper stream are proxied by the file storage so - it's possible to do ``storage.read()`` instead of the long form - ``storage.stream.read()``. - """ - - def __init__( - self, - stream=None, - filename=None, - name=None, - content_type=None, - content_length=None, - headers=None, - ): - self.name = name - self.stream = stream or BytesIO() - - # If no filename is provided, attempt to get the filename from - # the stream object. Python names special streams like - # ``<stderr>`` with angular brackets, skip these streams. - if filename is None: - filename = getattr(stream, "name", None) - - if filename is not None: - filename = os.fsdecode(filename) - - if filename and filename[0] == "<" and filename[-1] == ">": - filename = None - else: - filename = os.fsdecode(filename) - - self.filename = filename - - if headers is None: - headers = Headers() - self.headers = headers - if content_type is not None: - headers["Content-Type"] = content_type - if content_length is not None: - headers["Content-Length"] = str(content_length) - - def _parse_content_type(self): - if not hasattr(self, "_parsed_content_type"): - self._parsed_content_type = http.parse_options_header(self.content_type) - - @property - def content_type(self): - """The content-type sent in the header. Usually not available""" - return self.headers.get("content-type") - - @property - def content_length(self): - """The content-length sent in the header. Usually not available""" - try: - return int(self.headers.get("content-length") or 0) - except ValueError: - return 0 - - @property - def mimetype(self): - """Like :attr:`content_type`, but without parameters (eg, without - charset, type etc.) and always lowercase. For example if the content - type is ``text/HTML; charset=utf-8`` the mimetype would be - ``'text/html'``. - - .. versionadded:: 0.7 - """ - self._parse_content_type() - return self._parsed_content_type[0].lower() - - @property - def mimetype_params(self): - """The mimetype parameters as dict. For example if the content - type is ``text/html; charset=utf-8`` the params would be - ``{'charset': 'utf-8'}``. - - .. versionadded:: 0.7 - """ - self._parse_content_type() - return self._parsed_content_type[1] - - def save(self, dst, buffer_size=16384): - """Save the file to a destination path or file object. If the - destination is a file object you have to close it yourself after the - call. The buffer size is the number of bytes held in memory during - the copy process. It defaults to 16KB. - - For secure file saving also have a look at :func:`secure_filename`. - - :param dst: a filename, :class:`os.PathLike`, or open file - object to write to. - :param buffer_size: Passed as the ``length`` parameter of - :func:`shutil.copyfileobj`. - - .. versionchanged:: 1.0 - Supports :mod:`pathlib`. - """ - from shutil import copyfileobj - - close_dst = False - - if hasattr(dst, "__fspath__"): - dst = fspath(dst) - - if isinstance(dst, str): - dst = open(dst, "wb") - close_dst = True - - try: - copyfileobj(self.stream, dst, buffer_size) - finally: - if close_dst: - dst.close() - - def close(self): - """Close the underlying file if possible.""" - try: - self.stream.close() - except Exception: - pass - - def __bool__(self): - return bool(self.filename) - - def __getattr__(self, name): - try: - return getattr(self.stream, name) - except AttributeError: - # SpooledTemporaryFile doesn't implement IOBase, get the - # attribute from its backing file instead. - # https://github.com/python/cpython/pull/3249 - if hasattr(self.stream, "_file"): - return getattr(self.stream._file, name) - raise - - def __iter__(self): - return iter(self.stream) - - def __repr__(self): - return f"<{type(self).__name__}: {self.filename!r} ({self.content_type!r})>" - - -# circular dependencies -from . import http diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures.pyi deleted file mode 100644 index 7bf7297898c..00000000000 --- a/contrib/python/Werkzeug/py3/werkzeug/datastructures.pyi +++ /dev/null @@ -1,921 +0,0 @@ -from datetime import datetime -from os import PathLike -from typing import Any -from typing import Callable -from typing import Collection -from typing import Dict -from typing import FrozenSet -from typing import Generic -from typing import Hashable -from typing import IO -from typing import Iterable -from typing import Iterator -from typing import List -from typing import Mapping -from typing import NoReturn -from typing import Optional -from typing import overload -from typing import Set -from typing import Tuple -from typing import Type -from typing import TypeVar -from typing import Union -from _typeshed import SupportsKeysAndGetItem -from _typeshed.wsgi import WSGIEnvironment - -from typing_extensions import Literal -from typing_extensions import SupportsIndex - -K = TypeVar("K") -V = TypeVar("V") -T = TypeVar("T") -D = TypeVar("D") -_CD = TypeVar("_CD", bound="CallbackDict") - -def is_immutable(self: object) -> NoReturn: ... -def iter_multi_items( - mapping: Union[Mapping[K, Union[V, Iterable[V]]], Iterable[Tuple[K, V]]] -) -> Iterator[Tuple[K, V]]: ... - -class ImmutableListMixin(List[V]): - _hash_cache: Optional[int] - def __hash__(self) -> int: ... # type: ignore - def __delitem__(self, key: Union[SupportsIndex, slice]) -> NoReturn: ... - def __iadd__(self, other: t.Any) -> NoReturn: ... # type: ignore - def __imul__(self, other: SupportsIndex) -> NoReturn: ... - def __setitem__( # type: ignore - self, key: Union[int, slice], value: V - ) -> NoReturn: ... - def append(self, value: V) -> NoReturn: ... - def remove(self, value: V) -> NoReturn: ... - def extend(self, values: Iterable[V]) -> NoReturn: ... - def insert(self, pos: SupportsIndex, value: V) -> NoReturn: ... - def pop(self, index: SupportsIndex = -1) -> NoReturn: ... - def reverse(self) -> NoReturn: ... - def sort( - self, key: Optional[Callable[[V], Any]] = None, reverse: bool = False - ) -> NoReturn: ... - -class ImmutableList(ImmutableListMixin[V]): ... - -class ImmutableDictMixin(Dict[K, V]): - _hash_cache: Optional[int] - @classmethod - def fromkeys( # type: ignore - cls, keys: Iterable[K], value: Optional[V] = None - ) -> ImmutableDictMixin[K, V]: ... - def _iter_hashitems(self) -> Iterable[Hashable]: ... - def __hash__(self) -> int: ... # type: ignore - def setdefault(self, key: K, default: Optional[V] = None) -> NoReturn: ... - def update(self, *args: Any, **kwargs: V) -> NoReturn: ... - def pop(self, key: K, default: Optional[V] = None) -> NoReturn: ... # type: ignore - def popitem(self) -> NoReturn: ... - def __setitem__(self, key: K, value: V) -> NoReturn: ... - def __delitem__(self, key: K) -> NoReturn: ... - def clear(self) -> NoReturn: ... - -class ImmutableMultiDictMixin(ImmutableDictMixin[K, V]): - def _iter_hashitems(self) -> Iterable[Hashable]: ... - def add(self, key: K, value: V) -> NoReturn: ... - def popitemlist(self) -> NoReturn: ... - def poplist(self, key: K) -> NoReturn: ... - def setlist(self, key: K, new_list: Iterable[V]) -> NoReturn: ... - def setlistdefault( - self, key: K, default_list: Optional[Iterable[V]] = None - ) -> NoReturn: ... - -def _calls_update(name: str) -> Callable[[UpdateDictMixin[K, V]], Any]: ... - -class UpdateDictMixin(Dict[K, V]): - on_update: Optional[Callable[[UpdateDictMixin[K, V]], None]] - def setdefault(self, key: K, default: Optional[V] = None) -> V: ... - @overload - def pop(self, key: K) -> V: ... - @overload - def pop(self, key: K, default: Union[V, T] = ...) -> Union[V, T]: ... - def __setitem__(self, key: K, value: V) -> None: ... - def __delitem__(self, key: K) -> None: ... - def clear(self) -> None: ... - def popitem(self) -> Tuple[K, V]: ... - @overload - def update(self, __m: SupportsKeysAndGetItem[K, V], **kwargs: V) -> None: ... - @overload - def update(self, __m: Iterable[Tuple[K, V]], **kwargs: V) -> None: ... - @overload - def update(self, **kwargs: V) -> None: ... - -class TypeConversionDict(Dict[K, V]): - @overload - def get(self, key: K, default: None = ..., type: None = ...) -> Optional[V]: ... - @overload - def get(self, key: K, default: D, type: None = ...) -> Union[D, V]: ... - @overload - def get(self, key: K, default: D, type: Callable[[V], T]) -> Union[D, T]: ... - @overload - def get(self, key: K, type: Callable[[V], T]) -> Optional[T]: ... - -class ImmutableTypeConversionDict(ImmutableDictMixin[K, V], TypeConversionDict[K, V]): - def copy(self) -> TypeConversionDict[K, V]: ... - def __copy__(self) -> ImmutableTypeConversionDict: ... - -class MultiDict(TypeConversionDict[K, V]): - def __init__( - self, - mapping: Optional[ - Union[Mapping[K, Union[Iterable[V], V]], Iterable[Tuple[K, V]]] - ] = None, - ) -> None: ... - def __getitem__(self, item: K) -> V: ... - def __setitem__(self, key: K, value: V) -> None: ... - def add(self, key: K, value: V) -> None: ... - @overload - def getlist(self, key: K) -> List[V]: ... - @overload - def getlist(self, key: K, type: Callable[[V], T] = ...) -> List[T]: ... - def setlist(self, key: K, new_list: Iterable[V]) -> None: ... - def setdefault(self, key: K, default: Optional[V] = None) -> V: ... - def setlistdefault( - self, key: K, default_list: Optional[Iterable[V]] = None - ) -> List[V]: ... - def items(self, multi: bool = False) -> Iterator[Tuple[K, V]]: ... # type: ignore - def lists(self) -> Iterator[Tuple[K, List[V]]]: ... - def values(self) -> Iterator[V]: ... # type: ignore - def listvalues(self) -> Iterator[List[V]]: ... - def copy(self) -> MultiDict[K, V]: ... - def deepcopy(self, memo: Any = None) -> MultiDict[K, V]: ... - @overload - def to_dict(self) -> Dict[K, V]: ... - @overload - def to_dict(self, flat: Literal[False]) -> Dict[K, List[V]]: ... - def update( # type: ignore - self, mapping: Union[Mapping[K, Union[Iterable[V], V]], Iterable[Tuple[K, V]]] - ) -> None: ... - @overload - def pop(self, key: K) -> V: ... - @overload - def pop(self, key: K, default: Union[V, T] = ...) -> Union[V, T]: ... - def popitem(self) -> Tuple[K, V]: ... - def poplist(self, key: K) -> List[V]: ... - def popitemlist(self) -> Tuple[K, List[V]]: ... - def __copy__(self) -> MultiDict[K, V]: ... - def __deepcopy__(self, memo: Any) -> MultiDict[K, V]: ... - -class _omd_bucket(Generic[K, V]): - prev: Optional[_omd_bucket] - next: Optional[_omd_bucket] - key: K - value: V - def __init__(self, omd: OrderedMultiDict, key: K, value: V) -> None: ... - def unlink(self, omd: OrderedMultiDict) -> None: ... - -class OrderedMultiDict(MultiDict[K, V]): - _first_bucket: Optional[_omd_bucket] - _last_bucket: Optional[_omd_bucket] - def __init__(self, mapping: Optional[Mapping[K, V]] = None) -> None: ... - def __eq__(self, other: object) -> bool: ... - def __getitem__(self, key: K) -> V: ... - def __setitem__(self, key: K, value: V) -> None: ... - def __delitem__(self, key: K) -> None: ... - def keys(self) -> Iterator[K]: ... # type: ignore - def __iter__(self) -> Iterator[K]: ... - def values(self) -> Iterator[V]: ... # type: ignore - def items(self, multi: bool = False) -> Iterator[Tuple[K, V]]: ... # type: ignore - def lists(self) -> Iterator[Tuple[K, List[V]]]: ... - def listvalues(self) -> Iterator[List[V]]: ... - def add(self, key: K, value: V) -> None: ... - @overload - def getlist(self, key: K) -> List[V]: ... - @overload - def getlist(self, key: K, type: Callable[[V], T] = ...) -> List[T]: ... - def setlist(self, key: K, new_list: Iterable[V]) -> None: ... - def setlistdefault( - self, key: K, default_list: Optional[Iterable[V]] = None - ) -> List[V]: ... - def update( # type: ignore - self, mapping: Union[Mapping[K, V], Iterable[Tuple[K, V]]] - ) -> None: ... - def poplist(self, key: K) -> List[V]: ... - @overload - def pop(self, key: K) -> V: ... - @overload - def pop(self, key: K, default: Union[V, T] = ...) -> Union[V, T]: ... - def popitem(self) -> Tuple[K, V]: ... - def popitemlist(self) -> Tuple[K, List[V]]: ... - -def _options_header_vkw( - value: str, kw: Mapping[str, Optional[Union[str, int]]] -) -> str: ... -def _unicodify_header_value(value: Union[str, int]) -> str: ... - -HV = Union[str, int] - -class Headers(Dict[str, str]): - _list: List[Tuple[str, str]] - def __init__( - self, - defaults: Optional[ - Union[Mapping[str, Union[HV, Iterable[HV]]], Iterable[Tuple[str, HV]]] - ] = None, - ) -> None: ... - @overload - def __getitem__(self, key: str) -> str: ... - @overload - def __getitem__(self, key: int) -> Tuple[str, str]: ... - @overload - def __getitem__(self, key: slice) -> Headers: ... - @overload - def __getitem__(self, key: str, _get_mode: Literal[True] = ...) -> str: ... - def __eq__(self, other: object) -> bool: ... - @overload # type: ignore - def get(self, key: str, default: str) -> str: ... - @overload - def get(self, key: str, default: Optional[str] = None) -> Optional[str]: ... - @overload - def get( - self, key: str, default: Optional[T] = None, type: Callable[[str], T] = ... - ) -> Optional[T]: ... - @overload - def getlist(self, key: str) -> List[str]: ... - @overload - def getlist(self, key: str, type: Callable[[str], T]) -> List[T]: ... - def get_all(self, name: str) -> List[str]: ... - def items( # type: ignore - self, lower: bool = False - ) -> Iterator[Tuple[str, str]]: ... - def keys(self, lower: bool = False) -> Iterator[str]: ... # type: ignore - def values(self) -> Iterator[str]: ... # type: ignore - def extend( - self, - *args: Union[Mapping[str, Union[HV, Iterable[HV]]], Iterable[Tuple[str, HV]]], - **kwargs: Union[HV, Iterable[HV]], - ) -> None: ... - @overload - def __delitem__(self, key: Union[str, int, slice]) -> None: ... - @overload - def __delitem__(self, key: str, _index_operation: Literal[False]) -> None: ... - def remove(self, key: str) -> None: ... - @overload # type: ignore - def pop(self, key: str, default: Optional[str] = None) -> str: ... - @overload - def pop( - self, key: Optional[int] = None, default: Optional[Tuple[str, str]] = None - ) -> Tuple[str, str]: ... - def popitem(self) -> Tuple[str, str]: ... - def __contains__(self, key: str) -> bool: ... # type: ignore - def has_key(self, key: str) -> bool: ... - def __iter__(self) -> Iterator[Tuple[str, str]]: ... # type: ignore - def add(self, _key: str, _value: HV, **kw: HV) -> None: ... - def _validate_value(self, value: str) -> None: ... - def add_header(self, _key: str, _value: HV, **_kw: HV) -> None: ... - def clear(self) -> None: ... - def set(self, _key: str, _value: HV, **kw: HV) -> None: ... - def setlist(self, key: str, values: Iterable[HV]) -> None: ... - def setdefault(self, key: str, default: HV) -> str: ... # type: ignore - def setlistdefault(self, key: str, default: Iterable[HV]) -> None: ... - @overload - def __setitem__(self, key: str, value: HV) -> None: ... - @overload - def __setitem__(self, key: int, value: Tuple[str, HV]) -> None: ... - @overload - def __setitem__(self, key: slice, value: Iterable[Tuple[str, HV]]) -> None: ... - @overload - def update( - self, __m: SupportsKeysAndGetItem[str, HV], **kwargs: Union[HV, Iterable[HV]] - ) -> None: ... - @overload - def update( - self, __m: Iterable[Tuple[str, HV]], **kwargs: Union[HV, Iterable[HV]] - ) -> None: ... - @overload - def update(self, **kwargs: Union[HV, Iterable[HV]]) -> None: ... - def to_wsgi_list(self) -> List[Tuple[str, str]]: ... - def copy(self) -> Headers: ... - def __copy__(self) -> Headers: ... - -class ImmutableHeadersMixin(Headers): - def __delitem__(self, key: Any, _index_operation: bool = True) -> NoReturn: ... - def __setitem__(self, key: Any, value: Any) -> NoReturn: ... - def set(self, _key: Any, _value: Any, **kw: Any) -> NoReturn: ... - def setlist(self, key: Any, values: Any) -> NoReturn: ... - def add(self, _key: Any, _value: Any, **kw: Any) -> NoReturn: ... - def add_header(self, _key: Any, _value: Any, **_kw: Any) -> NoReturn: ... - def remove(self, key: Any) -> NoReturn: ... - def extend(self, *args: Any, **kwargs: Any) -> NoReturn: ... - def update(self, *args: Any, **kwargs: Any) -> NoReturn: ... - def insert(self, pos: Any, value: Any) -> NoReturn: ... - def pop(self, key: Any = None, default: Any = ...) -> NoReturn: ... - def popitem(self) -> NoReturn: ... - def setdefault(self, key: Any, default: Any) -> NoReturn: ... # type: ignore - def setlistdefault(self, key: Any, default: Any) -> NoReturn: ... - -class EnvironHeaders(ImmutableHeadersMixin, Headers): - environ: WSGIEnvironment - def __init__(self, environ: WSGIEnvironment) -> None: ... - def __eq__(self, other: object) -> bool: ... - def __getitem__( # type: ignore - self, key: str, _get_mode: Literal[False] = False - ) -> str: ... - def __iter__(self) -> Iterator[Tuple[str, str]]: ... # type: ignore - def copy(self) -> NoReturn: ... - -class CombinedMultiDict(ImmutableMultiDictMixin[K, V], MultiDict[K, V]): # type: ignore - dicts: List[MultiDict[K, V]] - def __init__(self, dicts: Optional[Iterable[MultiDict[K, V]]]) -> None: ... - @classmethod - def fromkeys(cls, keys: Any, value: Any = None) -> NoReturn: ... - def __getitem__(self, key: K) -> V: ... - @overload # type: ignore - def get(self, key: K) -> Optional[V]: ... - @overload - def get(self, key: K, default: Union[V, T] = ...) -> Union[V, T]: ... - @overload - def get( - self, key: K, default: Optional[T] = None, type: Callable[[V], T] = ... - ) -> Optional[T]: ... - @overload - def getlist(self, key: K) -> List[V]: ... - @overload - def getlist(self, key: K, type: Callable[[V], T] = ...) -> List[T]: ... - def _keys_impl(self) -> Set[K]: ... - def keys(self) -> Set[K]: ... # type: ignore - def __iter__(self) -> Set[K]: ... # type: ignore - def items(self, multi: bool = False) -> Iterator[Tuple[K, V]]: ... # type: ignore - def values(self) -> Iterator[V]: ... # type: ignore - def lists(self) -> Iterator[Tuple[K, List[V]]]: ... - def listvalues(self) -> Iterator[List[V]]: ... - def copy(self) -> MultiDict[K, V]: ... - @overload - def to_dict(self) -> Dict[K, V]: ... - @overload - def to_dict(self, flat: Literal[False]) -> Dict[K, List[V]]: ... - def __contains__(self, key: K) -> bool: ... # type: ignore - def has_key(self, key: K) -> bool: ... - -class FileMultiDict(MultiDict[str, "FileStorage"]): - def add_file( - self, - name: str, - file: Union[FileStorage, str, IO[bytes]], - filename: Optional[str] = None, - content_type: Optional[str] = None, - ) -> None: ... - -class ImmutableDict(ImmutableDictMixin[K, V], Dict[K, V]): - def copy(self) -> Dict[K, V]: ... - def __copy__(self) -> ImmutableDict[K, V]: ... - -class ImmutableMultiDict( # type: ignore - ImmutableMultiDictMixin[K, V], MultiDict[K, V] -): - def copy(self) -> MultiDict[K, V]: ... - def __copy__(self) -> ImmutableMultiDict[K, V]: ... - -class ImmutableOrderedMultiDict( # type: ignore - ImmutableMultiDictMixin[K, V], OrderedMultiDict[K, V] -): - def _iter_hashitems(self) -> Iterator[Tuple[int, Tuple[K, V]]]: ... - def copy(self) -> OrderedMultiDict[K, V]: ... - def __copy__(self) -> ImmutableOrderedMultiDict[K, V]: ... - -class Accept(ImmutableList[Tuple[str, int]]): - provided: bool - def __init__( - self, values: Optional[Union[Accept, Iterable[Tuple[str, float]]]] = None - ) -> None: ... - def _specificity(self, value: str) -> Tuple[bool, ...]: ... - def _value_matches(self, value: str, item: str) -> bool: ... - @overload # type: ignore - def __getitem__(self, key: str) -> int: ... - @overload - def __getitem__(self, key: int) -> Tuple[str, int]: ... - @overload - def __getitem__(self, key: slice) -> Iterable[Tuple[str, int]]: ... - def quality(self, key: str) -> int: ... - def __contains__(self, value: str) -> bool: ... # type: ignore - def index(self, key: str) -> int: ... # type: ignore - def find(self, key: str) -> int: ... - def values(self) -> Iterator[str]: ... - def to_header(self) -> str: ... - def _best_single_match(self, match: str) -> Optional[Tuple[str, int]]: ... - def best_match( - self, matches: Iterable[str], default: Optional[str] = None - ) -> Optional[str]: ... - @property - def best(self) -> str: ... - -def _normalize_mime(value: str) -> List[str]: ... - -class MIMEAccept(Accept): - def _specificity(self, value: str) -> Tuple[bool, ...]: ... - def _value_matches(self, value: str, item: str) -> bool: ... - @property - def accept_html(self) -> bool: ... - @property - def accept_xhtml(self) -> bool: ... - @property - def accept_json(self) -> bool: ... - -def _normalize_lang(value: str) -> List[str]: ... - -class LanguageAccept(Accept): - def _value_matches(self, value: str, item: str) -> bool: ... - def best_match( - self, matches: Iterable[str], default: Optional[str] = None - ) -> Optional[str]: ... - -class CharsetAccept(Accept): - def _value_matches(self, value: str, item: str) -> bool: ... - -_CPT = TypeVar("_CPT", str, int, bool) -_OptCPT = Optional[_CPT] - -def cache_control_property(key: str, empty: _OptCPT, type: Type[_CPT]) -> property: ... - -class _CacheControl(UpdateDictMixin[str, _OptCPT], Dict[str, _OptCPT]): - provided: bool - def __init__( - self, - values: Union[Mapping[str, _OptCPT], Iterable[Tuple[str, _OptCPT]]] = (), - on_update: Optional[Callable[[_CacheControl], None]] = None, - ) -> None: ... - @property - def no_cache(self) -> Optional[bool]: ... - @no_cache.setter - def no_cache(self, value: Optional[bool]) -> None: ... - @no_cache.deleter - def no_cache(self) -> None: ... - @property - def no_store(self) -> Optional[bool]: ... - @no_store.setter - def no_store(self, value: Optional[bool]) -> None: ... - @no_store.deleter - def no_store(self) -> None: ... - @property - def max_age(self) -> Optional[int]: ... - @max_age.setter - def max_age(self, value: Optional[int]) -> None: ... - @max_age.deleter - def max_age(self) -> None: ... - @property - def no_transform(self) -> Optional[bool]: ... - @no_transform.setter - def no_transform(self, value: Optional[bool]) -> None: ... - @no_transform.deleter - def no_transform(self) -> None: ... - def _get_cache_value(self, key: str, empty: Optional[T], type: Type[T]) -> T: ... - def _set_cache_value(self, key: str, value: Optional[T], type: Type[T]) -> None: ... - def _del_cache_value(self, key: str) -> None: ... - def to_header(self) -> str: ... - @staticmethod - def cache_property(key: str, empty: _OptCPT, type: Type[_CPT]) -> property: ... - -class RequestCacheControl(ImmutableDictMixin[str, _OptCPT], _CacheControl): - @property - def max_stale(self) -> Optional[int]: ... - @max_stale.setter - def max_stale(self, value: Optional[int]) -> None: ... - @max_stale.deleter - def max_stale(self) -> None: ... - @property - def min_fresh(self) -> Optional[int]: ... - @min_fresh.setter - def min_fresh(self, value: Optional[int]) -> None: ... - @min_fresh.deleter - def min_fresh(self) -> None: ... - @property - def only_if_cached(self) -> Optional[bool]: ... - @only_if_cached.setter - def only_if_cached(self, value: Optional[bool]) -> None: ... - @only_if_cached.deleter - def only_if_cached(self) -> None: ... - -class ResponseCacheControl(_CacheControl): - @property - def public(self) -> Optional[bool]: ... - @public.setter - def public(self, value: Optional[bool]) -> None: ... - @public.deleter - def public(self) -> None: ... - @property - def private(self) -> Optional[bool]: ... - @private.setter - def private(self, value: Optional[bool]) -> None: ... - @private.deleter - def private(self) -> None: ... - @property - def must_revalidate(self) -> Optional[bool]: ... - @must_revalidate.setter - def must_revalidate(self, value: Optional[bool]) -> None: ... - @must_revalidate.deleter - def must_revalidate(self) -> None: ... - @property - def proxy_revalidate(self) -> Optional[bool]: ... - @proxy_revalidate.setter - def proxy_revalidate(self, value: Optional[bool]) -> None: ... - @proxy_revalidate.deleter - def proxy_revalidate(self) -> None: ... - @property - def s_maxage(self) -> Optional[int]: ... - @s_maxage.setter - def s_maxage(self, value: Optional[int]) -> None: ... - @s_maxage.deleter - def s_maxage(self) -> None: ... - @property - def immutable(self) -> Optional[bool]: ... - @immutable.setter - def immutable(self, value: Optional[bool]) -> None: ... - @immutable.deleter - def immutable(self) -> None: ... - -def csp_property(key: str) -> property: ... - -class ContentSecurityPolicy(UpdateDictMixin[str, str], Dict[str, str]): - @property - def base_uri(self) -> Optional[str]: ... - @base_uri.setter - def base_uri(self, value: Optional[str]) -> None: ... - @base_uri.deleter - def base_uri(self) -> None: ... - @property - def child_src(self) -> Optional[str]: ... - @child_src.setter - def child_src(self, value: Optional[str]) -> None: ... - @child_src.deleter - def child_src(self) -> None: ... - @property - def connect_src(self) -> Optional[str]: ... - @connect_src.setter - def connect_src(self, value: Optional[str]) -> None: ... - @connect_src.deleter - def connect_src(self) -> None: ... - @property - def default_src(self) -> Optional[str]: ... - @default_src.setter - def default_src(self, value: Optional[str]) -> None: ... - @default_src.deleter - def default_src(self) -> None: ... - @property - def font_src(self) -> Optional[str]: ... - @font_src.setter - def font_src(self, value: Optional[str]) -> None: ... - @font_src.deleter - def font_src(self) -> None: ... - @property - def form_action(self) -> Optional[str]: ... - @form_action.setter - def form_action(self, value: Optional[str]) -> None: ... - @form_action.deleter - def form_action(self) -> None: ... - @property - def frame_ancestors(self) -> Optional[str]: ... - @frame_ancestors.setter - def frame_ancestors(self, value: Optional[str]) -> None: ... - @frame_ancestors.deleter - def frame_ancestors(self) -> None: ... - @property - def frame_src(self) -> Optional[str]: ... - @frame_src.setter - def frame_src(self, value: Optional[str]) -> None: ... - @frame_src.deleter - def frame_src(self) -> None: ... - @property - def img_src(self) -> Optional[str]: ... - @img_src.setter - def img_src(self, value: Optional[str]) -> None: ... - @img_src.deleter - def img_src(self) -> None: ... - @property - def manifest_src(self) -> Optional[str]: ... - @manifest_src.setter - def manifest_src(self, value: Optional[str]) -> None: ... - @manifest_src.deleter - def manifest_src(self) -> None: ... - @property - def media_src(self) -> Optional[str]: ... - @media_src.setter - def media_src(self, value: Optional[str]) -> None: ... - @media_src.deleter - def media_src(self) -> None: ... - @property - def navigate_to(self) -> Optional[str]: ... - @navigate_to.setter - def navigate_to(self, value: Optional[str]) -> None: ... - @navigate_to.deleter - def navigate_to(self) -> None: ... - @property - def object_src(self) -> Optional[str]: ... - @object_src.setter - def object_src(self, value: Optional[str]) -> None: ... - @object_src.deleter - def object_src(self) -> None: ... - @property - def prefetch_src(self) -> Optional[str]: ... - @prefetch_src.setter - def prefetch_src(self, value: Optional[str]) -> None: ... - @prefetch_src.deleter - def prefetch_src(self) -> None: ... - @property - def plugin_types(self) -> Optional[str]: ... - @plugin_types.setter - def plugin_types(self, value: Optional[str]) -> None: ... - @plugin_types.deleter - def plugin_types(self) -> None: ... - @property - def report_to(self) -> Optional[str]: ... - @report_to.setter - def report_to(self, value: Optional[str]) -> None: ... - @report_to.deleter - def report_to(self) -> None: ... - @property - def report_uri(self) -> Optional[str]: ... - @report_uri.setter - def report_uri(self, value: Optional[str]) -> None: ... - @report_uri.deleter - def report_uri(self) -> None: ... - @property - def sandbox(self) -> Optional[str]: ... - @sandbox.setter - def sandbox(self, value: Optional[str]) -> None: ... - @sandbox.deleter - def sandbox(self) -> None: ... - @property - def script_src(self) -> Optional[str]: ... - @script_src.setter - def script_src(self, value: Optional[str]) -> None: ... - @script_src.deleter - def script_src(self) -> None: ... - @property - def script_src_attr(self) -> Optional[str]: ... - @script_src_attr.setter - def script_src_attr(self, value: Optional[str]) -> None: ... - @script_src_attr.deleter - def script_src_attr(self) -> None: ... - @property - def script_src_elem(self) -> Optional[str]: ... - @script_src_elem.setter - def script_src_elem(self, value: Optional[str]) -> None: ... - @script_src_elem.deleter - def script_src_elem(self) -> None: ... - @property - def style_src(self) -> Optional[str]: ... - @style_src.setter - def style_src(self, value: Optional[str]) -> None: ... - @style_src.deleter - def style_src(self) -> None: ... - @property - def style_src_attr(self) -> Optional[str]: ... - @style_src_attr.setter - def style_src_attr(self, value: Optional[str]) -> None: ... - @style_src_attr.deleter - def style_src_attr(self) -> None: ... - @property - def style_src_elem(self) -> Optional[str]: ... - @style_src_elem.setter - def style_src_elem(self, value: Optional[str]) -> None: ... - @style_src_elem.deleter - def style_src_elem(self) -> None: ... - @property - def worker_src(self) -> Optional[str]: ... - @worker_src.setter - def worker_src(self, value: Optional[str]) -> None: ... - @worker_src.deleter - def worker_src(self) -> None: ... - provided: bool - def __init__( - self, - values: Union[Mapping[str, str], Iterable[Tuple[str, str]]] = (), - on_update: Optional[Callable[[ContentSecurityPolicy], None]] = None, - ) -> None: ... - def _get_value(self, key: str) -> Optional[str]: ... - def _set_value(self, key: str, value: str) -> None: ... - def _del_value(self, key: str) -> None: ... - def to_header(self) -> str: ... - -class CallbackDict(UpdateDictMixin[K, V], Dict[K, V]): - def __init__( - self, - initial: Optional[Union[Mapping[K, V], Iterable[Tuple[K, V]]]] = None, - on_update: Optional[Callable[[_CD], None]] = None, - ) -> None: ... - -class HeaderSet(Set[str]): - _headers: List[str] - _set: Set[str] - on_update: Optional[Callable[[HeaderSet], None]] - def __init__( - self, - headers: Optional[Iterable[str]] = None, - on_update: Optional[Callable[[HeaderSet], None]] = None, - ) -> None: ... - def add(self, header: str) -> None: ... - def remove(self, header: str) -> None: ... - def update(self, iterable: Iterable[str]) -> None: ... # type: ignore - def discard(self, header: str) -> None: ... - def find(self, header: str) -> int: ... - def index(self, header: str) -> int: ... - def clear(self) -> None: ... - def as_set(self, preserve_casing: bool = False) -> Set[str]: ... - def to_header(self) -> str: ... - def __getitem__(self, idx: int) -> str: ... - def __delitem__(self, idx: int) -> None: ... - def __setitem__(self, idx: int, value: str) -> None: ... - def __contains__(self, header: str) -> bool: ... # type: ignore - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[str]: ... - -class ETags(Collection[str]): - _strong: FrozenSet[str] - _weak: FrozenSet[str] - star_tag: bool - def __init__( - self, - strong_etags: Optional[Iterable[str]] = None, - weak_etags: Optional[Iterable[str]] = None, - star_tag: bool = False, - ) -> None: ... - def as_set(self, include_weak: bool = False) -> Set[str]: ... - def is_weak(self, etag: str) -> bool: ... - def is_strong(self, etag: str) -> bool: ... - def contains_weak(self, etag: str) -> bool: ... - def contains(self, etag: str) -> bool: ... - def contains_raw(self, etag: str) -> bool: ... - def to_header(self) -> str: ... - def __call__( - self, - etag: Optional[str] = None, - data: Optional[bytes] = None, - include_weak: bool = False, - ) -> bool: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterator[str]: ... - def __contains__(self, item: str) -> bool: ... # type: ignore - -class IfRange: - etag: Optional[str] - date: Optional[datetime] - def __init__( - self, etag: Optional[str] = None, date: Optional[datetime] = None - ) -> None: ... - def to_header(self) -> str: ... - -class Range: - units: str - ranges: List[Tuple[int, Optional[int]]] - def __init__(self, units: str, ranges: List[Tuple[int, Optional[int]]]) -> None: ... - def range_for_length(self, length: Optional[int]) -> Optional[Tuple[int, int]]: ... - def make_content_range(self, length: Optional[int]) -> Optional[ContentRange]: ... - def to_header(self) -> str: ... - def to_content_range_header(self, length: Optional[int]) -> Optional[str]: ... - -def _callback_property(name: str) -> property: ... - -class ContentRange: - on_update: Optional[Callable[[ContentRange], None]] - def __init__( - self, - units: Optional[str], - start: Optional[int], - stop: Optional[int], - length: Optional[int] = None, - on_update: Optional[Callable[[ContentRange], None]] = None, - ) -> None: ... - @property - def units(self) -> Optional[str]: ... - @units.setter - def units(self, value: Optional[str]) -> None: ... - @property - def start(self) -> Optional[int]: ... - @start.setter - def start(self, value: Optional[int]) -> None: ... - @property - def stop(self) -> Optional[int]: ... - @stop.setter - def stop(self, value: Optional[int]) -> None: ... - @property - def length(self) -> Optional[int]: ... - @length.setter - def length(self, value: Optional[int]) -> None: ... - def set( - self, - start: Optional[int], - stop: Optional[int], - length: Optional[int] = None, - units: Optional[str] = "bytes", - ) -> None: ... - def unset(self) -> None: ... - def to_header(self) -> str: ... - -class Authorization(ImmutableDictMixin[str, str], Dict[str, str]): - type: str - def __init__( - self, - auth_type: str, - data: Optional[Union[Mapping[str, str], Iterable[Tuple[str, str]]]] = None, - ) -> None: ... - @property - def username(self) -> Optional[str]: ... - @property - def password(self) -> Optional[str]: ... - @property - def realm(self) -> Optional[str]: ... - @property - def nonce(self) -> Optional[str]: ... - @property - def uri(self) -> Optional[str]: ... - @property - def nc(self) -> Optional[str]: ... - @property - def cnonce(self) -> Optional[str]: ... - @property - def response(self) -> Optional[str]: ... - @property - def opaque(self) -> Optional[str]: ... - @property - def qop(self) -> Optional[str]: ... - def to_header(self) -> str: ... - -def auth_property(name: str, doc: Optional[str] = None) -> property: ... -def _set_property(name: str, doc: Optional[str] = None) -> property: ... - -class WWWAuthenticate(UpdateDictMixin[str, str], Dict[str, str]): - _require_quoting: FrozenSet[str] - def __init__( - self, - auth_type: Optional[str] = None, - values: Optional[Union[Mapping[str, str], Iterable[Tuple[str, str]]]] = None, - on_update: Optional[Callable[[WWWAuthenticate], None]] = None, - ) -> None: ... - def set_basic(self, realm: str = ...) -> None: ... - def set_digest( - self, - realm: str, - nonce: str, - qop: Iterable[str] = ("auth",), - opaque: Optional[str] = None, - algorithm: Optional[str] = None, - stale: bool = False, - ) -> None: ... - def to_header(self) -> str: ... - @property - def type(self) -> Optional[str]: ... - @type.setter - def type(self, value: Optional[str]) -> None: ... - @property - def realm(self) -> Optional[str]: ... - @realm.setter - def realm(self, value: Optional[str]) -> None: ... - @property - def domain(self) -> HeaderSet: ... - @property - def nonce(self) -> Optional[str]: ... - @nonce.setter - def nonce(self, value: Optional[str]) -> None: ... - @property - def opaque(self) -> Optional[str]: ... - @opaque.setter - def opaque(self, value: Optional[str]) -> None: ... - @property - def algorithm(self) -> Optional[str]: ... - @algorithm.setter - def algorithm(self, value: Optional[str]) -> None: ... - @property - def qop(self) -> HeaderSet: ... - @property - def stale(self) -> Optional[bool]: ... - @stale.setter - def stale(self, value: Optional[bool]) -> None: ... - @staticmethod - def auth_property(name: str, doc: Optional[str] = None) -> property: ... - -class FileStorage: - name: Optional[str] - stream: IO[bytes] - filename: Optional[str] - headers: Headers - _parsed_content_type: Tuple[str, Dict[str, str]] - def __init__( - self, - stream: Optional[IO[bytes]] = None, - filename: Union[str, PathLike, None] = None, - name: Optional[str] = None, - content_type: Optional[str] = None, - content_length: Optional[int] = None, - headers: Optional[Headers] = None, - ) -> None: ... - def _parse_content_type(self) -> None: ... - @property - def content_type(self) -> str: ... - @property - def content_length(self) -> int: ... - @property - def mimetype(self) -> str: ... - @property - def mimetype_params(self) -> Dict[str, str]: ... - def save( - self, dst: Union[str, PathLike, IO[bytes]], buffer_size: int = ... - ) -> None: ... - def close(self) -> None: ... - def __bool__(self) -> bool: ... - def __getattr__(self, name: str) -> Any: ... - def __iter__(self) -> Iterator[bytes]: ... - def __repr__(self) -> str: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/__init__.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/__init__.py new file mode 100644 index 00000000000..846ffce6784 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/__init__.py @@ -0,0 +1,34 @@ +from .accept import Accept as Accept +from .accept import CharsetAccept as CharsetAccept +from .accept import LanguageAccept as LanguageAccept +from .accept import MIMEAccept as MIMEAccept +from .auth import Authorization as Authorization +from .auth import WWWAuthenticate as WWWAuthenticate +from .cache_control import RequestCacheControl as RequestCacheControl +from .cache_control import ResponseCacheControl as ResponseCacheControl +from .csp import ContentSecurityPolicy as ContentSecurityPolicy +from .etag import ETags as ETags +from .file_storage import FileMultiDict as FileMultiDict +from .file_storage import FileStorage as FileStorage +from .headers import EnvironHeaders as EnvironHeaders +from .headers import Headers as Headers +from .mixins import ImmutableDictMixin as ImmutableDictMixin +from .mixins import ImmutableHeadersMixin as ImmutableHeadersMixin +from .mixins import ImmutableListMixin as ImmutableListMixin +from .mixins import ImmutableMultiDictMixin as ImmutableMultiDictMixin +from .mixins import UpdateDictMixin as UpdateDictMixin +from .range import ContentRange as ContentRange +from .range import IfRange as IfRange +from .range import Range as Range +from .structures import CallbackDict as CallbackDict +from .structures import CombinedMultiDict as CombinedMultiDict +from .structures import HeaderSet as HeaderSet +from .structures import ImmutableDict as ImmutableDict +from .structures import ImmutableList as ImmutableList +from .structures import ImmutableMultiDict as ImmutableMultiDict +from .structures import ImmutableOrderedMultiDict as ImmutableOrderedMultiDict +from .structures import ImmutableTypeConversionDict as ImmutableTypeConversionDict +from .structures import iter_multi_items as iter_multi_items +from .structures import MultiDict as MultiDict +from .structures import OrderedMultiDict as OrderedMultiDict +from .structures import TypeConversionDict as TypeConversionDict diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/accept.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/accept.py new file mode 100644 index 00000000000..d80f0bbb850 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/accept.py @@ -0,0 +1,326 @@ +from __future__ import annotations + +import codecs +import re + +from .structures import ImmutableList + + +class Accept(ImmutableList): + """An :class:`Accept` object is just a list subclass for lists of + ``(value, quality)`` tuples. It is automatically sorted by specificity + and quality. + + All :class:`Accept` objects work similar to a list but provide extra + functionality for working with the data. Containment checks are + normalized to the rules of that header: + + >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)]) + >>> a.best + 'ISO-8859-1' + >>> 'iso-8859-1' in a + True + >>> 'UTF8' in a + True + >>> 'utf7' in a + False + + To get the quality for an item you can use normal item lookup: + + >>> print a['utf-8'] + 0.7 + >>> a['utf7'] + 0 + + .. versionchanged:: 0.5 + :class:`Accept` objects are forced immutable now. + + .. versionchanged:: 1.0.0 + :class:`Accept` internal values are no longer ordered + alphabetically for equal quality tags. Instead the initial + order is preserved. + + """ + + def __init__(self, values=()): + if values is None: + list.__init__(self) + self.provided = False + elif isinstance(values, Accept): + self.provided = values.provided + list.__init__(self, values) + else: + self.provided = True + values = sorted( + values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True + ) + list.__init__(self, values) + + def _specificity(self, value): + """Returns a tuple describing the value's specificity.""" + return (value != "*",) + + def _value_matches(self, value, item): + """Check if a value matches a given accept item.""" + return item == "*" or item.lower() == value.lower() + + def __getitem__(self, key): + """Besides index lookup (getting item n) you can also pass it a string + to get the quality for the item. If the item is not in the list, the + returned quality is ``0``. + """ + if isinstance(key, str): + return self.quality(key) + return list.__getitem__(self, key) + + def quality(self, key): + """Returns the quality of the key. + + .. versionadded:: 0.6 + In previous versions you had to use the item-lookup syntax + (eg: ``obj[key]`` instead of ``obj.quality(key)``) + """ + for item, quality in self: + if self._value_matches(key, item): + return quality + return 0 + + def __contains__(self, value): + for item, _quality in self: + if self._value_matches(value, item): + return True + return False + + def __repr__(self): + pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self) + return f"{type(self).__name__}([{pairs_str}])" + + def index(self, key): + """Get the position of an entry or raise :exc:`ValueError`. + + :param key: The key to be looked up. + + .. versionchanged:: 0.5 + This used to raise :exc:`IndexError`, which was inconsistent + with the list API. + """ + if isinstance(key, str): + for idx, (item, _quality) in enumerate(self): + if self._value_matches(key, item): + return idx + raise ValueError(key) + return list.index(self, key) + + def find(self, key): + """Get the position of an entry or return -1. + + :param key: The key to be looked up. + """ + try: + return self.index(key) + except ValueError: + return -1 + + def values(self): + """Iterate over all values.""" + for item in self: + yield item[0] + + def to_header(self): + """Convert the header set into an HTTP header string.""" + result = [] + for value, quality in self: + if quality != 1: + value = f"{value};q={quality}" + result.append(value) + return ",".join(result) + + def __str__(self): + return self.to_header() + + def _best_single_match(self, match): + for client_item, quality in self: + if self._value_matches(match, client_item): + # self is sorted by specificity descending, we can exit + return client_item, quality + return None + + def best_match(self, matches, default=None): + """Returns the best match from a list of possible matches based + on the specificity and quality of the client. If two items have the + same quality and specificity, the one is returned that comes first. + + :param matches: a list of matches to check for + :param default: the value that is returned if none match + """ + result = default + best_quality = -1 + best_specificity = (-1,) + for server_item in matches: + match = self._best_single_match(server_item) + if not match: + continue + client_item, quality = match + specificity = self._specificity(client_item) + if quality <= 0 or quality < best_quality: + continue + # better quality or same quality but more specific => better match + if quality > best_quality or specificity > best_specificity: + result = server_item + best_quality = quality + best_specificity = specificity + return result + + @property + def best(self): + """The best match as value.""" + if self: + return self[0][0] + + +_mime_split_re = re.compile(r"/|(?:\s*;\s*)") + + +def _normalize_mime(value): + return _mime_split_re.split(value.lower()) + + +class MIMEAccept(Accept): + """Like :class:`Accept` but with special methods and behavior for + mimetypes. + """ + + def _specificity(self, value): + return tuple(x != "*" for x in _mime_split_re.split(value)) + + def _value_matches(self, value, item): + # item comes from the client, can't match if it's invalid. + if "/" not in item: + return False + + # value comes from the application, tell the developer when it + # doesn't look valid. + if "/" not in value: + raise ValueError(f"invalid mimetype {value!r}") + + # Split the match value into type, subtype, and a sorted list of parameters. + normalized_value = _normalize_mime(value) + value_type, value_subtype = normalized_value[:2] + value_params = sorted(normalized_value[2:]) + + # "*/*" is the only valid value that can start with "*". + if value_type == "*" and value_subtype != "*": + raise ValueError(f"invalid mimetype {value!r}") + + # Split the accept item into type, subtype, and parameters. + normalized_item = _normalize_mime(item) + item_type, item_subtype = normalized_item[:2] + item_params = sorted(normalized_item[2:]) + + # "*/not-*" from the client is invalid, can't match. + if item_type == "*" and item_subtype != "*": + return False + + return ( + (item_type == "*" and item_subtype == "*") + or (value_type == "*" and value_subtype == "*") + ) or ( + item_type == value_type + and ( + item_subtype == "*" + or value_subtype == "*" + or (item_subtype == value_subtype and item_params == value_params) + ) + ) + + @property + def accept_html(self): + """True if this object accepts HTML.""" + return ( + "text/html" in self or "application/xhtml+xml" in self or self.accept_xhtml + ) + + @property + def accept_xhtml(self): + """True if this object accepts XHTML.""" + return "application/xhtml+xml" in self or "application/xml" in self + + @property + def accept_json(self): + """True if this object accepts JSON.""" + return "application/json" in self + + +_locale_delim_re = re.compile(r"[_-]") + + +def _normalize_lang(value): + """Process a language tag for matching.""" + return _locale_delim_re.split(value.lower()) + + +class LanguageAccept(Accept): + """Like :class:`Accept` but with normalization for language tags.""" + + def _value_matches(self, value, item): + return item == "*" or _normalize_lang(value) == _normalize_lang(item) + + def best_match(self, matches, default=None): + """Given a list of supported values, finds the best match from + the list of accepted values. + + Language tags are normalized for the purpose of matching, but + are returned unchanged. + + If no exact match is found, this will fall back to matching + the first subtag (primary language only), first with the + accepted values then with the match values. This partial is not + applied to any other language subtags. + + The default is returned if no exact or fallback match is found. + + :param matches: A list of supported languages to find a match. + :param default: The value that is returned if none match. + """ + # Look for an exact match first. If a client accepts "en-US", + # "en-US" is a valid match at this point. + result = super().best_match(matches) + + if result is not None: + return result + + # Fall back to accepting primary tags. If a client accepts + # "en-US", "en" is a valid match at this point. Need to use + # re.split to account for 2 or 3 letter codes. + fallback = Accept( + [(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self] + ) + result = fallback.best_match(matches) + + if result is not None: + return result + + # Fall back to matching primary tags. If the client accepts + # "en", "en-US" is a valid match at this point. + fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches] + result = super().best_match(fallback_matches) + + # Return a value from the original match list. Find the first + # original value that starts with the matched primary tag. + if result is not None: + return next(item for item in matches if item.startswith(result)) + + return default + + +class CharsetAccept(Accept): + """Like :class:`Accept` but with normalization for charsets.""" + + def _value_matches(self, value, item): + def _normalize(name): + try: + return codecs.lookup(name).name + except LookupError: + return name.lower() + + return item == "*" or _normalize(value) == _normalize(item) diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/accept.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/accept.pyi new file mode 100644 index 00000000000..4b74dd9505d --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/accept.pyi @@ -0,0 +1,54 @@ +from collections.abc import Iterable +from collections.abc import Iterator +from typing import overload + +from .structures import ImmutableList + +class Accept(ImmutableList[tuple[str, int]]): + provided: bool + def __init__( + self, values: Accept | Iterable[tuple[str, float]] | None = None + ) -> None: ... + def _specificity(self, value: str) -> tuple[bool, ...]: ... + def _value_matches(self, value: str, item: str) -> bool: ... + @overload # type: ignore + def __getitem__(self, key: str) -> int: ... + @overload + def __getitem__(self, key: int) -> tuple[str, int]: ... + @overload + def __getitem__(self, key: slice) -> Iterable[tuple[str, int]]: ... + def quality(self, key: str) -> int: ... + def __contains__(self, value: str) -> bool: ... # type: ignore + def index(self, key: str) -> int: ... # type: ignore + def find(self, key: str) -> int: ... + def values(self) -> Iterator[str]: ... + def to_header(self) -> str: ... + def _best_single_match(self, match: str) -> tuple[str, int] | None: ... + @overload + def best_match(self, matches: Iterable[str], default: str) -> str: ... + @overload + def best_match( + self, matches: Iterable[str], default: str | None = None + ) -> str | None: ... + @property + def best(self) -> str: ... + +def _normalize_mime(value: str) -> list[str]: ... + +class MIMEAccept(Accept): + def _specificity(self, value: str) -> tuple[bool, ...]: ... + def _value_matches(self, value: str, item: str) -> bool: ... + @property + def accept_html(self) -> bool: ... + @property + def accept_xhtml(self) -> bool: ... + @property + def accept_json(self) -> bool: ... + +def _normalize_lang(value: str) -> list[str]: ... + +class LanguageAccept(Accept): + def _value_matches(self, value: str, item: str) -> bool: ... + +class CharsetAccept(Accept): + def _value_matches(self, value: str, item: str) -> bool: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/auth.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/auth.py new file mode 100644 index 00000000000..2f2515020c0 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/auth.py @@ -0,0 +1,510 @@ +from __future__ import annotations + +import base64 +import binascii +import typing as t +import warnings +from functools import wraps + +from ..http import dump_header +from ..http import parse_dict_header +from ..http import parse_set_header +from ..http import quote_header_value +from .structures import CallbackDict +from .structures import HeaderSet + +if t.TYPE_CHECKING: + import typing_extensions as te + + +class Authorization: + """Represents the parts of an ``Authorization`` request header. + + :attr:`.Request.authorization` returns an instance if the header is set. + + An instance can be used with the test :class:`.Client` request methods' ``auth`` + parameter to send the header in test requests. + + Depending on the auth scheme, either :attr:`parameters` or :attr:`token` will be + set. The ``Basic`` scheme's token is decoded into the ``username`` and ``password`` + parameters. + + For convenience, ``auth["key"]`` and ``auth.key`` both access the key in the + :attr:`parameters` dict, along with ``auth.get("key")`` and ``"key" in auth``. + + .. versionchanged:: 2.3 + The ``token`` parameter and attribute was added to support auth schemes that use + a token instead of parameters, such as ``Bearer``. + + .. versionchanged:: 2.3 + The object is no longer a ``dict``. + + .. versionchanged:: 0.5 + The object is an immutable dict. + """ + + def __init__( + self, + auth_type: str, + data: dict[str, str] | None = None, + token: str | None = None, + ) -> None: + self.type = auth_type + """The authorization scheme, like ``basic``, ``digest``, or ``bearer``.""" + + if data is None: + data = {} + + self.parameters = data + """A dict of parameters parsed from the header. Either this or :attr:`token` + will have a value for a given scheme. + """ + + self.token = token + """A token parsed from the header. Either this or :attr:`parameters` will have a + value for a given scheme. + + .. versionadded:: 2.3 + """ + + def __getattr__(self, name: str) -> str | None: + return self.parameters.get(name) + + def __getitem__(self, name: str) -> str | None: + return self.parameters.get(name) + + def get(self, key: str, default: str | None = None) -> str | None: + return self.parameters.get(key, default) + + def __contains__(self, key: str) -> bool: + return key in self.parameters + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Authorization): + return NotImplemented + + return ( + other.type == self.type + and other.token == self.token + and other.parameters == self.parameters + ) + + @classmethod + def from_header(cls, value: str | None) -> te.Self | None: + """Parse an ``Authorization`` header value and return an instance, or ``None`` + if the value is empty. + + :param value: The header value to parse. + + .. versionadded:: 2.3 + """ + if not value: + return None + + scheme, _, rest = value.partition(" ") + scheme = scheme.lower() + rest = rest.strip() + + if scheme == "basic": + try: + username, _, password = base64.b64decode(rest).decode().partition(":") + except (binascii.Error, UnicodeError): + return None + + return cls(scheme, {"username": username, "password": password}) + + if "=" in rest.rstrip("="): + # = that is not trailing, this is parameters. + return cls(scheme, parse_dict_header(rest), None) + + # No = or only trailing =, this is a token. + return cls(scheme, None, rest) + + def to_header(self) -> str: + """Produce an ``Authorization`` header value representing this data. + + .. versionadded:: 2.0 + """ + if self.type == "basic": + value = base64.b64encode( + f"{self.username}:{self.password}".encode() + ).decode("utf8") + return f"Basic {value}" + + if self.token is not None: + return f"{self.type.title()} {self.token}" + + return f"{self.type.title()} {dump_header(self.parameters)}" + + def __str__(self) -> str: + return self.to_header() + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.to_header()}>" + + +def auth_property(name: str, doc: str | None = None) -> property: + """A static helper function for Authentication subclasses to add + extra authentication system properties onto a class:: + + class FooAuthenticate(WWWAuthenticate): + special_realm = auth_property('special_realm') + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. + """ + warnings.warn( + "'auth_property' is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + + def _set_value(self, value): # type: ignore[no-untyped-def] + if value is None: + self.pop(name, None) + else: + self[name] = str(value) + + return property(lambda x: x.get(name), _set_value, doc=doc) + + +class WWWAuthenticate: + """Represents the parts of a ``WWW-Authenticate`` response header. + + Set :attr:`.Response.www_authenticate` to an instance of list of instances to set + values for this header in the response. Modifying this instance will modify the + header value. + + Depending on the auth scheme, either :attr:`parameters` or :attr:`token` should be + set. The ``Basic`` scheme will encode ``username`` and ``password`` parameters to a + token. + + For convenience, ``auth["key"]`` and ``auth.key`` both act on the :attr:`parameters` + dict, and can be used to get, set, or delete parameters. ``auth.get("key")`` and + ``"key" in auth`` are also provided. + + .. versionchanged:: 2.3 + The ``token`` parameter and attribute was added to support auth schemes that use + a token instead of parameters, such as ``Bearer``. + + .. versionchanged:: 2.3 + The object is no longer a ``dict``. + + .. versionchanged:: 2.3 + The ``on_update`` parameter was removed. + """ + + def __init__( + self, + auth_type: str | None = None, + values: dict[str, str] | None = None, + token: str | None = None, + ): + if auth_type is None: + warnings.warn( + "An auth type must be given as the first parameter. Assuming 'basic' is" + " deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + auth_type = "basic" + + self._type = auth_type.lower() + self._parameters: dict[str, str] = CallbackDict( # type: ignore[misc] + values, lambda _: self._trigger_on_update() + ) + self._token = token + self._on_update: t.Callable[[WWWAuthenticate], None] | None = None + + def _trigger_on_update(self) -> None: + if self._on_update is not None: + self._on_update(self) + + @property + def type(self) -> str: + """The authorization scheme, like ``basic``, ``digest``, or ``bearer``.""" + return self._type + + @type.setter + def type(self, value: str) -> None: + self._type = value + self._trigger_on_update() + + @property + def parameters(self) -> dict[str, str]: + """A dict of parameters for the header. Only one of this or :attr:`token` should + have a value for a given scheme. + """ + return self._parameters + + @parameters.setter + def parameters(self, value: dict[str, str]) -> None: + self._parameters = CallbackDict( # type: ignore[misc] + value, lambda _: self._trigger_on_update() + ) + self._trigger_on_update() + + @property + def token(self) -> str | None: + """A dict of parameters for the header. Only one of this or :attr:`token` should + have a value for a given scheme. + """ + return self._token + + @token.setter + def token(self, value: str | None) -> None: + """A token for the header. Only one of this or :attr:`parameters` should have a + value for a given scheme. + + .. versionadded:: 2.3 + """ + self._token = value + self._trigger_on_update() + + def set_basic(self, realm: str = "authentication required") -> None: + """Clear any existing data and set a ``Basic`` challenge. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Create and assign an instance instead. + """ + warnings.warn( + "The 'set_basic' method is deprecated and will be removed in Werkzeug 3.0." + " Create and assign an instance instead." + ) + self._type = "basic" + dict.clear(self.parameters) # type: ignore[arg-type] + dict.update( + self.parameters, # type: ignore[arg-type] + {"realm": realm}, # type: ignore[dict-item] + ) + self._token = None + self._trigger_on_update() + + def set_digest( + self, + realm: str, + nonce: str, + qop: t.Sequence[str] = ("auth",), + opaque: str | None = None, + algorithm: str | None = None, + stale: bool = False, + ) -> None: + """Clear any existing data and set a ``Digest`` challenge. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Create and assign an instance instead. + """ + warnings.warn( + "The 'set_digest' method is deprecated and will be removed in Werkzeug 3.0." + " Create and assign an instance instead." + ) + self._type = "digest" + dict.clear(self.parameters) # type: ignore[arg-type] + parameters = { + "realm": realm, + "nonce": nonce, + "qop": ", ".join(qop), + "stale": "TRUE" if stale else "FALSE", + } + + if opaque is not None: + parameters["opaque"] = opaque + + if algorithm is not None: + parameters["algorithm"] = algorithm + + dict.update(self.parameters, parameters) # type: ignore[arg-type] + self._token = None + self._trigger_on_update() + + def __getitem__(self, key: str) -> str | None: + return self.parameters.get(key) + + def __setitem__(self, key: str, value: str | None) -> None: + if value is None: + if key in self.parameters: + del self.parameters[key] + else: + self.parameters[key] = value + + self._trigger_on_update() + + def __delitem__(self, key: str) -> None: + if key in self.parameters: + del self.parameters[key] + self._trigger_on_update() + + def __getattr__(self, name: str) -> str | None: + return self[name] + + def __setattr__(self, name: str, value: str | None) -> None: + if name in {"_type", "_parameters", "_token", "_on_update"}: + super().__setattr__(name, value) + else: + self[name] = value + + def __delattr__(self, name: str) -> None: + del self[name] + + def __contains__(self, key: str) -> bool: + return key in self.parameters + + def __eq__(self, other: object) -> bool: + if not isinstance(other, WWWAuthenticate): + return NotImplemented + + return ( + other.type == self.type + and other.token == self.token + and other.parameters == self.parameters + ) + + def get(self, key: str, default: str | None = None) -> str | None: + return self.parameters.get(key, default) + + @classmethod + def from_header(cls, value: str | None) -> te.Self | None: + """Parse a ``WWW-Authenticate`` header value and return an instance, or ``None`` + if the value is empty. + + :param value: The header value to parse. + + .. versionadded:: 2.3 + """ + if not value: + return None + + scheme, _, rest = value.partition(" ") + scheme = scheme.lower() + rest = rest.strip() + + if "=" in rest.rstrip("="): + # = that is not trailing, this is parameters. + return cls(scheme, parse_dict_header(rest), None) + + # No = or only trailing =, this is a token. + return cls(scheme, None, rest) + + def to_header(self) -> str: + """Produce a ``WWW-Authenticate`` header value representing this data.""" + if self.token is not None: + return f"{self.type.title()} {self.token}" + + if self.type == "digest": + items = [] + + for key, value in self.parameters.items(): + if key in {"realm", "domain", "nonce", "opaque", "qop"}: + value = quote_header_value(value, allow_token=False) + else: + value = quote_header_value(value) + + items.append(f"{key}={value}") + + return f"Digest {', '.join(items)}" + + return f"{self.type.title()} {dump_header(self.parameters)}" + + def __str__(self) -> str: + return self.to_header() + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.to_header()}>" + + @property + def qop(self) -> set[str]: + """The ``qop`` parameter as a set. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. It will become the same as other + parameters, returning a string. + """ + warnings.warn( + "The 'qop' property is deprecated and will be removed in Werkzeug 3.0." + " It will become the same as other parameters, returning a string.", + DeprecationWarning, + stacklevel=2, + ) + + def on_update(value: HeaderSet) -> None: + if not value: + if "qop" in self: + del self["qop"] + + return + + self.parameters["qop"] = value.to_header() + + return parse_set_header(self.parameters.get("qop"), on_update) + + @property + def stale(self) -> bool | None: + """The ``stale`` parameter as a boolean. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. It will become the same as other + parameters, returning a string. + """ + warnings.warn( + "The 'stale' property is deprecated and will be removed in Werkzeug 3.0." + " It will become the same as other parameters, returning a string.", + DeprecationWarning, + stacklevel=2, + ) + + if "stale" in self.parameters: + return self.parameters["stale"].lower() == "true" + + return None + + @stale.setter + def stale(self, value: bool | str | None) -> None: + if value is None: + if "stale" in self.parameters: + del self.parameters["stale"] + + return + + if isinstance(value, bool): + warnings.warn( + "Setting the 'stale' property to a boolean is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + self.parameters["stale"] = "TRUE" if value else "FALSE" + else: + self.parameters["stale"] = value + + auth_property = staticmethod(auth_property) + + +def _deprecated_dict_method(f): # type: ignore[no-untyped-def] + @wraps(f) + def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] + warnings.warn( + "Treating 'Authorization' and 'WWWAuthenticate' as a dict is deprecated and" + " will be removed in Werkzeug 3.0. Use the 'parameters' attribute instead.", + DeprecationWarning, + stacklevel=2, + ) + return f(*args, **kwargs) + + return wrapper + + +for name in ( + "__iter__", + "clear", + "copy", + "items", + "keys", + "pop", + "popitem", + "setdefault", + "update", + "values", +): + f = _deprecated_dict_method(getattr(dict, name)) + setattr(Authorization, name, f) + setattr(WWWAuthenticate, name, f) diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/cache_control.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/cache_control.py new file mode 100644 index 00000000000..bff4c18bbd5 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/cache_control.py @@ -0,0 +1,175 @@ +from __future__ import annotations + +from .mixins import ImmutableDictMixin +from .mixins import UpdateDictMixin + + +def cache_control_property(key, empty, type): + """Return a new property object for a cache header. Useful if you + want to add support for a cache extension in a subclass. + + .. versionchanged:: 2.0 + Renamed from ``cache_property``. + """ + return property( + lambda x: x._get_cache_value(key, empty, type), + lambda x, v: x._set_cache_value(key, v, type), + lambda x: x._del_cache_value(key), + f"accessor for {key!r}", + ) + + +class _CacheControl(UpdateDictMixin, dict): + """Subclass of a dict that stores values for a Cache-Control header. It + has accessors for all the cache-control directives specified in RFC 2616. + The class does not differentiate between request and response directives. + + Because the cache-control directives in the HTTP header use dashes the + python descriptors use underscores for that. + + To get a header of the :class:`CacheControl` object again you can convert + the object into a string or call the :meth:`to_header` method. If you plan + to subclass it and add your own items have a look at the sourcecode for + that class. + + .. versionchanged:: 2.1.0 + Setting int properties such as ``max_age`` will convert the + value to an int. + + .. versionchanged:: 0.4 + + Setting `no_cache` or `private` to boolean `True` will set the implicit + none-value which is ``*``: + + >>> cc = ResponseCacheControl() + >>> cc.no_cache = True + >>> cc + <ResponseCacheControl 'no-cache'> + >>> cc.no_cache + '*' + >>> cc.no_cache = None + >>> cc + <ResponseCacheControl ''> + + In versions before 0.5 the behavior documented here affected the now + no longer existing `CacheControl` class. + """ + + no_cache = cache_control_property("no-cache", "*", None) + no_store = cache_control_property("no-store", None, bool) + max_age = cache_control_property("max-age", -1, int) + no_transform = cache_control_property("no-transform", None, None) + + def __init__(self, values=(), on_update=None): + dict.__init__(self, values or ()) + self.on_update = on_update + self.provided = values is not None + + def _get_cache_value(self, key, empty, type): + """Used internally by the accessor properties.""" + if type is bool: + return key in self + if key in self: + value = self[key] + if value is None: + return empty + elif type is not None: + try: + value = type(value) + except ValueError: + pass + return value + return None + + def _set_cache_value(self, key, value, type): + """Used internally by the accessor properties.""" + if type is bool: + if value: + self[key] = None + else: + self.pop(key, None) + else: + if value is None: + self.pop(key, None) + elif value is True: + self[key] = None + else: + if type is not None: + self[key] = type(value) + else: + self[key] = value + + def _del_cache_value(self, key): + """Used internally by the accessor properties.""" + if key in self: + del self[key] + + def to_header(self): + """Convert the stored values into a cache control header.""" + return http.dump_header(self) + + def __str__(self): + return self.to_header() + + def __repr__(self): + kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) + return f"<{type(self).__name__} {kv_str}>" + + cache_property = staticmethod(cache_control_property) + + +class RequestCacheControl(ImmutableDictMixin, _CacheControl): + """A cache control for requests. This is immutable and gives access + to all the request-relevant cache control headers. + + To get a header of the :class:`RequestCacheControl` object again you can + convert the object into a string or call the :meth:`to_header` method. If + you plan to subclass it and add your own items have a look at the sourcecode + for that class. + + .. versionchanged:: 2.1.0 + Setting int properties such as ``max_age`` will convert the + value to an int. + + .. versionadded:: 0.5 + In previous versions a `CacheControl` class existed that was used + both for request and response. + """ + + max_stale = cache_control_property("max-stale", "*", int) + min_fresh = cache_control_property("min-fresh", "*", int) + only_if_cached = cache_control_property("only-if-cached", None, bool) + + +class ResponseCacheControl(_CacheControl): + """A cache control for responses. Unlike :class:`RequestCacheControl` + this is mutable and gives access to response-relevant cache control + headers. + + To get a header of the :class:`ResponseCacheControl` object again you can + convert the object into a string or call the :meth:`to_header` method. If + you plan to subclass it and add your own items have a look at the sourcecode + for that class. + + .. versionchanged:: 2.1.1 + ``s_maxage`` converts the value to an int. + + .. versionchanged:: 2.1.0 + Setting int properties such as ``max_age`` will convert the + value to an int. + + .. versionadded:: 0.5 + In previous versions a `CacheControl` class existed that was used + both for request and response. + """ + + public = cache_control_property("public", None, bool) + private = cache_control_property("private", "*", None) + must_revalidate = cache_control_property("must-revalidate", None, bool) + proxy_revalidate = cache_control_property("proxy-revalidate", None, bool) + s_maxage = cache_control_property("s-maxage", None, int) + immutable = cache_control_property("immutable", None, bool) + + +# circular dependencies +from .. import http diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/cache_control.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/cache_control.pyi new file mode 100644 index 00000000000..06fe667a24d --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/cache_control.pyi @@ -0,0 +1,109 @@ +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Mapping +from typing import TypeVar + +from .mixins import ImmutableDictMixin +from .mixins import UpdateDictMixin + +T = TypeVar("T") +_CPT = TypeVar("_CPT", str, int, bool) +_OptCPT = _CPT | None + +def cache_control_property(key: str, empty: _OptCPT, type: type[_CPT]) -> property: ... + +class _CacheControl(UpdateDictMixin[str, _OptCPT], dict[str, _OptCPT]): + provided: bool + def __init__( + self, + values: Mapping[str, _OptCPT] | Iterable[tuple[str, _OptCPT]] = (), + on_update: Callable[[_CacheControl], None] | None = None, + ) -> None: ... + @property + def no_cache(self) -> bool | None: ... + @no_cache.setter + def no_cache(self, value: bool | None) -> None: ... + @no_cache.deleter + def no_cache(self) -> None: ... + @property + def no_store(self) -> bool | None: ... + @no_store.setter + def no_store(self, value: bool | None) -> None: ... + @no_store.deleter + def no_store(self) -> None: ... + @property + def max_age(self) -> int | None: ... + @max_age.setter + def max_age(self, value: int | None) -> None: ... + @max_age.deleter + def max_age(self) -> None: ... + @property + def no_transform(self) -> bool | None: ... + @no_transform.setter + def no_transform(self, value: bool | None) -> None: ... + @no_transform.deleter + def no_transform(self) -> None: ... + def _get_cache_value(self, key: str, empty: T | None, type: type[T]) -> T: ... + def _set_cache_value(self, key: str, value: T | None, type: type[T]) -> None: ... + def _del_cache_value(self, key: str) -> None: ... + def to_header(self) -> str: ... + @staticmethod + def cache_property(key: str, empty: _OptCPT, type: type[_CPT]) -> property: ... + +class RequestCacheControl(ImmutableDictMixin[str, _OptCPT], _CacheControl): + @property + def max_stale(self) -> int | None: ... + @max_stale.setter + def max_stale(self, value: int | None) -> None: ... + @max_stale.deleter + def max_stale(self) -> None: ... + @property + def min_fresh(self) -> int | None: ... + @min_fresh.setter + def min_fresh(self, value: int | None) -> None: ... + @min_fresh.deleter + def min_fresh(self) -> None: ... + @property + def only_if_cached(self) -> bool | None: ... + @only_if_cached.setter + def only_if_cached(self, value: bool | None) -> None: ... + @only_if_cached.deleter + def only_if_cached(self) -> None: ... + +class ResponseCacheControl(_CacheControl): + @property + def public(self) -> bool | None: ... + @public.setter + def public(self, value: bool | None) -> None: ... + @public.deleter + def public(self) -> None: ... + @property + def private(self) -> bool | None: ... + @private.setter + def private(self, value: bool | None) -> None: ... + @private.deleter + def private(self) -> None: ... + @property + def must_revalidate(self) -> bool | None: ... + @must_revalidate.setter + def must_revalidate(self, value: bool | None) -> None: ... + @must_revalidate.deleter + def must_revalidate(self) -> None: ... + @property + def proxy_revalidate(self) -> bool | None: ... + @proxy_revalidate.setter + def proxy_revalidate(self, value: bool | None) -> None: ... + @proxy_revalidate.deleter + def proxy_revalidate(self) -> None: ... + @property + def s_maxage(self) -> int | None: ... + @s_maxage.setter + def s_maxage(self, value: int | None) -> None: ... + @s_maxage.deleter + def s_maxage(self) -> None: ... + @property + def immutable(self) -> bool | None: ... + @immutable.setter + def immutable(self, value: bool | None) -> None: ... + @immutable.deleter + def immutable(self) -> None: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/csp.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/csp.py new file mode 100644 index 00000000000..dde9414951c --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/csp.py @@ -0,0 +1,94 @@ +from __future__ import annotations + +from .mixins import UpdateDictMixin + + +def csp_property(key): + """Return a new property object for a content security policy header. + Useful if you want to add support for a csp extension in a + subclass. + """ + return property( + lambda x: x._get_value(key), + lambda x, v: x._set_value(key, v), + lambda x: x._del_value(key), + f"accessor for {key!r}", + ) + + +class ContentSecurityPolicy(UpdateDictMixin, dict): + """Subclass of a dict that stores values for a Content Security Policy + header. It has accessors for all the level 3 policies. + + Because the csp directives in the HTTP header use dashes the + python descriptors use underscores for that. + + To get a header of the :class:`ContentSecuirtyPolicy` object again + you can convert the object into a string or call the + :meth:`to_header` method. If you plan to subclass it and add your + own items have a look at the sourcecode for that class. + + .. versionadded:: 1.0.0 + Support for Content Security Policy headers was added. + + """ + + base_uri = csp_property("base-uri") + child_src = csp_property("child-src") + connect_src = csp_property("connect-src") + default_src = csp_property("default-src") + font_src = csp_property("font-src") + form_action = csp_property("form-action") + frame_ancestors = csp_property("frame-ancestors") + frame_src = csp_property("frame-src") + img_src = csp_property("img-src") + manifest_src = csp_property("manifest-src") + media_src = csp_property("media-src") + navigate_to = csp_property("navigate-to") + object_src = csp_property("object-src") + prefetch_src = csp_property("prefetch-src") + plugin_types = csp_property("plugin-types") + report_to = csp_property("report-to") + report_uri = csp_property("report-uri") + sandbox = csp_property("sandbox") + script_src = csp_property("script-src") + script_src_attr = csp_property("script-src-attr") + script_src_elem = csp_property("script-src-elem") + style_src = csp_property("style-src") + style_src_attr = csp_property("style-src-attr") + style_src_elem = csp_property("style-src-elem") + worker_src = csp_property("worker-src") + + def __init__(self, values=(), on_update=None): + dict.__init__(self, values or ()) + self.on_update = on_update + self.provided = values is not None + + def _get_value(self, key): + """Used internally by the accessor properties.""" + return self.get(key) + + def _set_value(self, key, value): + """Used internally by the accessor properties.""" + if value is None: + self.pop(key, None) + else: + self[key] = value + + def _del_value(self, key): + """Used internally by the accessor properties.""" + if key in self: + del self[key] + + def to_header(self): + """Convert the stored values into a cache control header.""" + from ..http import dump_csp_header + + return dump_csp_header(self) + + def __str__(self): + return self.to_header() + + def __repr__(self): + kv_str = " ".join(f"{k}={v!r}" for k, v in sorted(self.items())) + return f"<{type(self).__name__} {kv_str}>" diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/csp.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/csp.pyi new file mode 100644 index 00000000000..f9e2ac0f463 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/csp.pyi @@ -0,0 +1,169 @@ +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Mapping + +from .mixins import UpdateDictMixin + +def csp_property(key: str) -> property: ... + +class ContentSecurityPolicy(UpdateDictMixin[str, str], dict[str, str]): + @property + def base_uri(self) -> str | None: ... + @base_uri.setter + def base_uri(self, value: str | None) -> None: ... + @base_uri.deleter + def base_uri(self) -> None: ... + @property + def child_src(self) -> str | None: ... + @child_src.setter + def child_src(self, value: str | None) -> None: ... + @child_src.deleter + def child_src(self) -> None: ... + @property + def connect_src(self) -> str | None: ... + @connect_src.setter + def connect_src(self, value: str | None) -> None: ... + @connect_src.deleter + def connect_src(self) -> None: ... + @property + def default_src(self) -> str | None: ... + @default_src.setter + def default_src(self, value: str | None) -> None: ... + @default_src.deleter + def default_src(self) -> None: ... + @property + def font_src(self) -> str | None: ... + @font_src.setter + def font_src(self, value: str | None) -> None: ... + @font_src.deleter + def font_src(self) -> None: ... + @property + def form_action(self) -> str | None: ... + @form_action.setter + def form_action(self, value: str | None) -> None: ... + @form_action.deleter + def form_action(self) -> None: ... + @property + def frame_ancestors(self) -> str | None: ... + @frame_ancestors.setter + def frame_ancestors(self, value: str | None) -> None: ... + @frame_ancestors.deleter + def frame_ancestors(self) -> None: ... + @property + def frame_src(self) -> str | None: ... + @frame_src.setter + def frame_src(self, value: str | None) -> None: ... + @frame_src.deleter + def frame_src(self) -> None: ... + @property + def img_src(self) -> str | None: ... + @img_src.setter + def img_src(self, value: str | None) -> None: ... + @img_src.deleter + def img_src(self) -> None: ... + @property + def manifest_src(self) -> str | None: ... + @manifest_src.setter + def manifest_src(self, value: str | None) -> None: ... + @manifest_src.deleter + def manifest_src(self) -> None: ... + @property + def media_src(self) -> str | None: ... + @media_src.setter + def media_src(self, value: str | None) -> None: ... + @media_src.deleter + def media_src(self) -> None: ... + @property + def navigate_to(self) -> str | None: ... + @navigate_to.setter + def navigate_to(self, value: str | None) -> None: ... + @navigate_to.deleter + def navigate_to(self) -> None: ... + @property + def object_src(self) -> str | None: ... + @object_src.setter + def object_src(self, value: str | None) -> None: ... + @object_src.deleter + def object_src(self) -> None: ... + @property + def prefetch_src(self) -> str | None: ... + @prefetch_src.setter + def prefetch_src(self, value: str | None) -> None: ... + @prefetch_src.deleter + def prefetch_src(self) -> None: ... + @property + def plugin_types(self) -> str | None: ... + @plugin_types.setter + def plugin_types(self, value: str | None) -> None: ... + @plugin_types.deleter + def plugin_types(self) -> None: ... + @property + def report_to(self) -> str | None: ... + @report_to.setter + def report_to(self, value: str | None) -> None: ... + @report_to.deleter + def report_to(self) -> None: ... + @property + def report_uri(self) -> str | None: ... + @report_uri.setter + def report_uri(self, value: str | None) -> None: ... + @report_uri.deleter + def report_uri(self) -> None: ... + @property + def sandbox(self) -> str | None: ... + @sandbox.setter + def sandbox(self, value: str | None) -> None: ... + @sandbox.deleter + def sandbox(self) -> None: ... + @property + def script_src(self) -> str | None: ... + @script_src.setter + def script_src(self, value: str | None) -> None: ... + @script_src.deleter + def script_src(self) -> None: ... + @property + def script_src_attr(self) -> str | None: ... + @script_src_attr.setter + def script_src_attr(self, value: str | None) -> None: ... + @script_src_attr.deleter + def script_src_attr(self) -> None: ... + @property + def script_src_elem(self) -> str | None: ... + @script_src_elem.setter + def script_src_elem(self, value: str | None) -> None: ... + @script_src_elem.deleter + def script_src_elem(self) -> None: ... + @property + def style_src(self) -> str | None: ... + @style_src.setter + def style_src(self, value: str | None) -> None: ... + @style_src.deleter + def style_src(self) -> None: ... + @property + def style_src_attr(self) -> str | None: ... + @style_src_attr.setter + def style_src_attr(self, value: str | None) -> None: ... + @style_src_attr.deleter + def style_src_attr(self) -> None: ... + @property + def style_src_elem(self) -> str | None: ... + @style_src_elem.setter + def style_src_elem(self, value: str | None) -> None: ... + @style_src_elem.deleter + def style_src_elem(self) -> None: ... + @property + def worker_src(self) -> str | None: ... + @worker_src.setter + def worker_src(self, value: str | None) -> None: ... + @worker_src.deleter + def worker_src(self) -> None: ... + provided: bool + def __init__( + self, + values: Mapping[str, str] | Iterable[tuple[str, str]] = (), + on_update: Callable[[ContentSecurityPolicy], None] | None = None, + ) -> None: ... + def _get_value(self, key: str) -> str | None: ... + def _set_value(self, key: str, value: str) -> None: ... + def _del_value(self, key: str) -> None: ... + def to_header(self) -> str: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/etag.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/etag.py new file mode 100644 index 00000000000..747d9966ddc --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/etag.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from collections.abc import Collection + + +class ETags(Collection): + """A set that can be used to check if one etag is present in a collection + of etags. + """ + + def __init__(self, strong_etags=None, weak_etags=None, star_tag=False): + if not star_tag and strong_etags: + self._strong = frozenset(strong_etags) + else: + self._strong = frozenset() + + self._weak = frozenset(weak_etags or ()) + self.star_tag = star_tag + + def as_set(self, include_weak=False): + """Convert the `ETags` object into a python set. Per default all the + weak etags are not part of this set.""" + rv = set(self._strong) + if include_weak: + rv.update(self._weak) + return rv + + def is_weak(self, etag): + """Check if an etag is weak.""" + return etag in self._weak + + def is_strong(self, etag): + """Check if an etag is strong.""" + return etag in self._strong + + def contains_weak(self, etag): + """Check if an etag is part of the set including weak and strong tags.""" + return self.is_weak(etag) or self.contains(etag) + + def contains(self, etag): + """Check if an etag is part of the set ignoring weak tags. + It is also possible to use the ``in`` operator. + """ + if self.star_tag: + return True + return self.is_strong(etag) + + def contains_raw(self, etag): + """When passed a quoted tag it will check if this tag is part of the + set. If the tag is weak it is checked against weak and strong tags, + otherwise strong only.""" + from ..http import unquote_etag + + etag, weak = unquote_etag(etag) + if weak: + return self.contains_weak(etag) + return self.contains(etag) + + def to_header(self): + """Convert the etags set into a HTTP header string.""" + if self.star_tag: + return "*" + return ", ".join( + [f'"{x}"' for x in self._strong] + [f'W/"{x}"' for x in self._weak] + ) + + def __call__(self, etag=None, data=None, include_weak=False): + if [etag, data].count(None) != 1: + raise TypeError("either tag or data required, but at least one") + if etag is None: + from ..http import generate_etag + + etag = generate_etag(data) + if include_weak: + if etag in self._weak: + return True + return etag in self._strong + + def __bool__(self): + return bool(self.star_tag or self._strong or self._weak) + + def __str__(self): + return self.to_header() + + def __len__(self): + return len(self._strong) + + def __iter__(self): + return iter(self._strong) + + def __contains__(self, etag): + return self.contains(etag) + + def __repr__(self): + return f"<{type(self).__name__} {str(self)!r}>" diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/etag.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/etag.pyi new file mode 100644 index 00000000000..88e54f1548b --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/etag.pyi @@ -0,0 +1,30 @@ +from collections.abc import Collection +from collections.abc import Iterable +from collections.abc import Iterator + +class ETags(Collection[str]): + _strong: frozenset[str] + _weak: frozenset[str] + star_tag: bool + def __init__( + self, + strong_etags: Iterable[str] | None = None, + weak_etags: Iterable[str] | None = None, + star_tag: bool = False, + ) -> None: ... + def as_set(self, include_weak: bool = False) -> set[str]: ... + def is_weak(self, etag: str) -> bool: ... + def is_strong(self, etag: str) -> bool: ... + def contains_weak(self, etag: str) -> bool: ... + def contains(self, etag: str) -> bool: ... + def contains_raw(self, etag: str) -> bool: ... + def to_header(self) -> str: ... + def __call__( + self, + etag: str | None = None, + data: bytes | None = None, + include_weak: bool = False, + ) -> bool: ... + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... + def __contains__(self, item: str) -> bool: ... # type: ignore diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/file_storage.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/file_storage.py new file mode 100644 index 00000000000..e878a56d4f4 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/file_storage.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +import mimetypes +from io import BytesIO +from os import fsdecode +from os import fspath + +from .._internal import _plain_int +from .structures import MultiDict + + +class FileStorage: + """The :class:`FileStorage` class is a thin wrapper over incoming files. + It is used by the request object to represent uploaded files. All the + attributes of the wrapper stream are proxied by the file storage so + it's possible to do ``storage.read()`` instead of the long form + ``storage.stream.read()``. + """ + + def __init__( + self, + stream=None, + filename=None, + name=None, + content_type=None, + content_length=None, + headers=None, + ): + self.name = name + self.stream = stream or BytesIO() + + # If no filename is provided, attempt to get the filename from + # the stream object. Python names special streams like + # ``<stderr>`` with angular brackets, skip these streams. + if filename is None: + filename = getattr(stream, "name", None) + + if filename is not None: + filename = fsdecode(filename) + + if filename and filename[0] == "<" and filename[-1] == ">": + filename = None + else: + filename = fsdecode(filename) + + self.filename = filename + + if headers is None: + from .headers import Headers + + headers = Headers() + self.headers = headers + if content_type is not None: + headers["Content-Type"] = content_type + if content_length is not None: + headers["Content-Length"] = str(content_length) + + def _parse_content_type(self): + if not hasattr(self, "_parsed_content_type"): + self._parsed_content_type = http.parse_options_header(self.content_type) + + @property + def content_type(self): + """The content-type sent in the header. Usually not available""" + return self.headers.get("content-type") + + @property + def content_length(self): + """The content-length sent in the header. Usually not available""" + if "content-length" in self.headers: + try: + return _plain_int(self.headers["content-length"]) + except ValueError: + pass + + return 0 + + @property + def mimetype(self): + """Like :attr:`content_type`, but without parameters (eg, without + charset, type etc.) and always lowercase. For example if the content + type is ``text/HTML; charset=utf-8`` the mimetype would be + ``'text/html'``. + + .. versionadded:: 0.7 + """ + self._parse_content_type() + return self._parsed_content_type[0].lower() + + @property + def mimetype_params(self): + """The mimetype parameters as dict. For example if the content + type is ``text/html; charset=utf-8`` the params would be + ``{'charset': 'utf-8'}``. + + .. versionadded:: 0.7 + """ + self._parse_content_type() + return self._parsed_content_type[1] + + def save(self, dst, buffer_size=16384): + """Save the file to a destination path or file object. If the + destination is a file object you have to close it yourself after the + call. The buffer size is the number of bytes held in memory during + the copy process. It defaults to 16KB. + + For secure file saving also have a look at :func:`secure_filename`. + + :param dst: a filename, :class:`os.PathLike`, or open file + object to write to. + :param buffer_size: Passed as the ``length`` parameter of + :func:`shutil.copyfileobj`. + + .. versionchanged:: 1.0 + Supports :mod:`pathlib`. + """ + from shutil import copyfileobj + + close_dst = False + + if hasattr(dst, "__fspath__"): + dst = fspath(dst) + + if isinstance(dst, str): + dst = open(dst, "wb") + close_dst = True + + try: + copyfileobj(self.stream, dst, buffer_size) + finally: + if close_dst: + dst.close() + + def close(self): + """Close the underlying file if possible.""" + try: + self.stream.close() + except Exception: + pass + + def __bool__(self): + return bool(self.filename) + + def __getattr__(self, name): + try: + return getattr(self.stream, name) + except AttributeError: + # SpooledTemporaryFile doesn't implement IOBase, get the + # attribute from its backing file instead. + # https://github.com/python/cpython/pull/3249 + if hasattr(self.stream, "_file"): + return getattr(self.stream._file, name) + raise + + def __iter__(self): + return iter(self.stream) + + def __repr__(self): + return f"<{type(self).__name__}: {self.filename!r} ({self.content_type!r})>" + + +class FileMultiDict(MultiDict): + """A special :class:`MultiDict` that has convenience methods to add + files to it. This is used for :class:`EnvironBuilder` and generally + useful for unittesting. + + .. versionadded:: 0.5 + """ + + def add_file(self, name, file, filename=None, content_type=None): + """Adds a new file to the dict. `file` can be a file name or + a :class:`file`-like or a :class:`FileStorage` object. + + :param name: the name of the field. + :param file: a filename or :class:`file`-like object + :param filename: an optional filename + :param content_type: an optional content type + """ + if isinstance(file, FileStorage): + value = file + else: + if isinstance(file, str): + if filename is None: + filename = file + file = open(file, "rb") + if filename and content_type is None: + content_type = ( + mimetypes.guess_type(filename)[0] or "application/octet-stream" + ) + value = FileStorage(file, filename, name, content_type) + + self.add(name, value) + + +# circular dependencies +from .. import http diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/file_storage.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/file_storage.pyi new file mode 100644 index 00000000000..730789e3549 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/file_storage.pyi @@ -0,0 +1,47 @@ +from collections.abc import Iterator +from os import PathLike +from typing import Any +from typing import IO + +from .headers import Headers +from .structures import MultiDict + +class FileStorage: + name: str | None + stream: IO[bytes] + filename: str | None + headers: Headers + _parsed_content_type: tuple[str, dict[str, str]] + def __init__( + self, + stream: IO[bytes] | None = None, + filename: str | PathLike | None = None, + name: str | None = None, + content_type: str | None = None, + content_length: int | None = None, + headers: Headers | None = None, + ) -> None: ... + def _parse_content_type(self) -> None: ... + @property + def content_type(self) -> str: ... + @property + def content_length(self) -> int: ... + @property + def mimetype(self) -> str: ... + @property + def mimetype_params(self) -> dict[str, str]: ... + def save(self, dst: str | PathLike | IO[bytes], buffer_size: int = ...) -> None: ... + def close(self) -> None: ... + def __bool__(self) -> bool: ... + def __getattr__(self, name: str) -> Any: ... + def __iter__(self) -> Iterator[bytes]: ... + def __repr__(self) -> str: ... + +class FileMultiDict(MultiDict[str, FileStorage]): + def add_file( + self, + name: str, + file: FileStorage | str | IO[bytes], + filename: str | None = None, + content_type: str | None = None, + ) -> None: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/headers.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/headers.py new file mode 100644 index 00000000000..dc060c41e30 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/headers.py @@ -0,0 +1,566 @@ +from __future__ import annotations + +import re +import typing as t +import warnings + +from .._internal import _missing +from ..exceptions import BadRequestKeyError +from .mixins import ImmutableHeadersMixin +from .structures import iter_multi_items +from .structures import MultiDict + + +class Headers: + """An object that stores some headers. It has a dict-like interface, + but is ordered, can store the same key multiple times, and iterating + yields ``(key, value)`` pairs instead of only keys. + + This data structure is useful if you want a nicer way to handle WSGI + headers which are stored as tuples in a list. + + From Werkzeug 0.3 onwards, the :exc:`KeyError` raised by this class is + also a subclass of the :class:`~exceptions.BadRequest` HTTP exception + and will render a page for a ``400 BAD REQUEST`` if caught in a + catch-all for HTTP exceptions. + + Headers is mostly compatible with the Python :class:`wsgiref.headers.Headers` + class, with the exception of `__getitem__`. :mod:`wsgiref` will return + `None` for ``headers['missing']``, whereas :class:`Headers` will raise + a :class:`KeyError`. + + To create a new ``Headers`` object, pass it a list, dict, or + other ``Headers`` object with default values. These values are + validated the same way values added later are. + + :param defaults: The list of default values for the :class:`Headers`. + + .. versionchanged:: 2.1.0 + Default values are validated the same as values added later. + + .. versionchanged:: 0.9 + This data structure now stores unicode values similar to how the + multi dicts do it. The main difference is that bytes can be set as + well which will automatically be latin1 decoded. + + .. versionchanged:: 0.9 + The :meth:`linked` function was removed without replacement as it + was an API that does not support the changes to the encoding model. + """ + + def __init__(self, defaults=None): + self._list = [] + if defaults is not None: + self.extend(defaults) + + def __getitem__(self, key, _get_mode=False): + if not _get_mode: + if isinstance(key, int): + return self._list[key] + elif isinstance(key, slice): + return self.__class__(self._list[key]) + if not isinstance(key, str): + raise BadRequestKeyError(key) + ikey = key.lower() + for k, v in self._list: + if k.lower() == ikey: + return v + # micro optimization: if we are in get mode we will catch that + # exception one stack level down so we can raise a standard + # key error instead of our special one. + if _get_mode: + raise KeyError() + raise BadRequestKeyError(key) + + def __eq__(self, other): + def lowered(item): + return (item[0].lower(),) + item[1:] + + return other.__class__ is self.__class__ and set( + map(lowered, other._list) + ) == set(map(lowered, self._list)) + + __hash__ = None + + def get(self, key, default=None, type=None, as_bytes=None): + """Return the default value if the requested data doesn't exist. + If `type` is provided and is a callable it should convert the value, + return it or raise a :exc:`ValueError` if that is not possible. In + this case the function will return the default as if the value was not + found: + + >>> d = Headers([('Content-Length', '42')]) + >>> d.get('Content-Length', type=int) + 42 + + :param key: The key to be looked up. + :param default: The default value to be returned if the key can't + be looked up. If not further specified `None` is + returned. + :param type: A callable that is used to cast the value in the + :class:`Headers`. If a :exc:`ValueError` is raised + by this callable the default value is returned. + + .. versionchanged:: 2.3 + The ``as_bytes`` parameter is deprecated and will be removed + in Werkzeug 3.0. + + .. versionchanged:: 0.9 + The ``as_bytes`` parameter was added. + """ + if as_bytes is not None: + warnings.warn( + "The 'as_bytes' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + + try: + rv = self.__getitem__(key, _get_mode=True) + except KeyError: + return default + if as_bytes: + rv = rv.encode("latin1") + if type is None: + return rv + try: + return type(rv) + except ValueError: + return default + + def getlist(self, key, type=None, as_bytes=None): + """Return the list of items for a given key. If that key is not in the + :class:`Headers`, the return value will be an empty list. Just like + :meth:`get`, :meth:`getlist` accepts a `type` parameter. All items will + be converted with the callable defined there. + + :param key: The key to be looked up. + :param type: A callable that is used to cast the value in the + :class:`Headers`. If a :exc:`ValueError` is raised + by this callable the value will be removed from the list. + :return: a :class:`list` of all the values for the key. + + .. versionchanged:: 2.3 + The ``as_bytes`` parameter is deprecated and will be removed + in Werkzeug 3.0. + + .. versionchanged:: 0.9 + The ``as_bytes`` parameter was added. + """ + if as_bytes is not None: + warnings.warn( + "The 'as_bytes' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + + ikey = key.lower() + result = [] + for k, v in self: + if k.lower() == ikey: + if as_bytes: + v = v.encode("latin1") + if type is not None: + try: + v = type(v) + except ValueError: + continue + result.append(v) + return result + + def get_all(self, name): + """Return a list of all the values for the named field. + + This method is compatible with the :mod:`wsgiref` + :meth:`~wsgiref.headers.Headers.get_all` method. + """ + return self.getlist(name) + + def items(self, lower=False): + for key, value in self: + if lower: + key = key.lower() + yield key, value + + def keys(self, lower=False): + for key, _ in self.items(lower): + yield key + + def values(self): + for _, value in self.items(): + yield value + + def extend(self, *args, **kwargs): + """Extend headers in this object with items from another object + containing header items as well as keyword arguments. + + To replace existing keys instead of extending, use + :meth:`update` instead. + + If provided, the first argument can be another :class:`Headers` + object, a :class:`MultiDict`, :class:`dict`, or iterable of + pairs. + + .. versionchanged:: 1.0 + Support :class:`MultiDict`. Allow passing ``kwargs``. + """ + if len(args) > 1: + raise TypeError(f"update expected at most 1 arguments, got {len(args)}") + + if args: + for key, value in iter_multi_items(args[0]): + self.add(key, value) + + for key, value in iter_multi_items(kwargs): + self.add(key, value) + + def __delitem__(self, key, _index_operation=True): + if _index_operation and isinstance(key, (int, slice)): + del self._list[key] + return + key = key.lower() + new = [] + for k, v in self._list: + if k.lower() != key: + new.append((k, v)) + self._list[:] = new + + def remove(self, key): + """Remove a key. + + :param key: The key to be removed. + """ + return self.__delitem__(key, _index_operation=False) + + def pop(self, key=None, default=_missing): + """Removes and returns a key or index. + + :param key: The key to be popped. If this is an integer the item at + that position is removed, if it's a string the value for + that key is. If the key is omitted or `None` the last + item is removed. + :return: an item. + """ + if key is None: + return self._list.pop() + if isinstance(key, int): + return self._list.pop(key) + try: + rv = self[key] + self.remove(key) + except KeyError: + if default is not _missing: + return default + raise + return rv + + def popitem(self): + """Removes a key or index and returns a (key, value) item.""" + return self.pop() + + def __contains__(self, key): + """Check if a key is present.""" + try: + self.__getitem__(key, _get_mode=True) + except KeyError: + return False + return True + + def __iter__(self): + """Yield ``(key, value)`` tuples.""" + return iter(self._list) + + def __len__(self): + return len(self._list) + + def add(self, _key, _value, **kw): + """Add a new header tuple to the list. + + Keyword arguments can specify additional parameters for the header + value, with underscores converted to dashes:: + + >>> d = Headers() + >>> d.add('Content-Type', 'text/plain') + >>> d.add('Content-Disposition', 'attachment', filename='foo.png') + + The keyword argument dumping uses :func:`dump_options_header` + behind the scenes. + + .. versionadded:: 0.4.1 + keyword arguments were added for :mod:`wsgiref` compatibility. + """ + if kw: + _value = _options_header_vkw(_value, kw) + _key = _str_header_key(_key) + _value = _str_header_value(_value) + self._list.append((_key, _value)) + + def add_header(self, _key, _value, **_kw): + """Add a new header tuple to the list. + + An alias for :meth:`add` for compatibility with the :mod:`wsgiref` + :meth:`~wsgiref.headers.Headers.add_header` method. + """ + self.add(_key, _value, **_kw) + + def clear(self): + """Clears all headers.""" + del self._list[:] + + def set(self, _key, _value, **kw): + """Remove all header tuples for `key` and add a new one. The newly + added key either appears at the end of the list if there was no + entry or replaces the first one. + + Keyword arguments can specify additional parameters for the header + value, with underscores converted to dashes. See :meth:`add` for + more information. + + .. versionchanged:: 0.6.1 + :meth:`set` now accepts the same arguments as :meth:`add`. + + :param key: The key to be inserted. + :param value: The value to be inserted. + """ + if kw: + _value = _options_header_vkw(_value, kw) + _key = _str_header_key(_key) + _value = _str_header_value(_value) + if not self._list: + self._list.append((_key, _value)) + return + listiter = iter(self._list) + ikey = _key.lower() + for idx, (old_key, _old_value) in enumerate(listiter): + if old_key.lower() == ikey: + # replace first occurrence + self._list[idx] = (_key, _value) + break + else: + self._list.append((_key, _value)) + return + self._list[idx + 1 :] = [t for t in listiter if t[0].lower() != ikey] + + def setlist(self, key, values): + """Remove any existing values for a header and add new ones. + + :param key: The header key to set. + :param values: An iterable of values to set for the key. + + .. versionadded:: 1.0 + """ + if values: + values_iter = iter(values) + self.set(key, next(values_iter)) + + for value in values_iter: + self.add(key, value) + else: + self.remove(key) + + def setdefault(self, key, default): + """Return the first value for the key if it is in the headers, + otherwise set the header to the value given by ``default`` and + return that. + + :param key: The header key to get. + :param default: The value to set for the key if it is not in the + headers. + """ + if key in self: + return self[key] + + self.set(key, default) + return default + + def setlistdefault(self, key, default): + """Return the list of values for the key if it is in the + headers, otherwise set the header to the list of values given + by ``default`` and return that. + + Unlike :meth:`MultiDict.setlistdefault`, modifying the returned + list will not affect the headers. + + :param key: The header key to get. + :param default: An iterable of values to set for the key if it + is not in the headers. + + .. versionadded:: 1.0 + """ + if key not in self: + self.setlist(key, default) + + return self.getlist(key) + + def __setitem__(self, key, value): + """Like :meth:`set` but also supports index/slice based setting.""" + if isinstance(key, (slice, int)): + if isinstance(key, int): + value = [value] + value = [(_str_header_key(k), _str_header_value(v)) for (k, v) in value] + if isinstance(key, int): + self._list[key] = value[0] + else: + self._list[key] = value + else: + self.set(key, value) + + def update(self, *args, **kwargs): + """Replace headers in this object with items from another + headers object and keyword arguments. + + To extend existing keys instead of replacing, use :meth:`extend` + instead. + + If provided, the first argument can be another :class:`Headers` + object, a :class:`MultiDict`, :class:`dict`, or iterable of + pairs. + + .. versionadded:: 1.0 + """ + if len(args) > 1: + raise TypeError(f"update expected at most 1 arguments, got {len(args)}") + + if args: + mapping = args[0] + + if isinstance(mapping, (Headers, MultiDict)): + for key in mapping.keys(): + self.setlist(key, mapping.getlist(key)) + elif isinstance(mapping, dict): + for key, value in mapping.items(): + if isinstance(value, (list, tuple)): + self.setlist(key, value) + else: + self.set(key, value) + else: + for key, value in mapping: + self.set(key, value) + + for key, value in kwargs.items(): + if isinstance(value, (list, tuple)): + self.setlist(key, value) + else: + self.set(key, value) + + def to_wsgi_list(self): + """Convert the headers into a list suitable for WSGI. + + :return: list + """ + return list(self) + + def copy(self): + return self.__class__(self._list) + + def __copy__(self): + return self.copy() + + def __str__(self): + """Returns formatted headers suitable for HTTP transmission.""" + strs = [] + for key, value in self.to_wsgi_list(): + strs.append(f"{key}: {value}") + strs.append("\r\n") + return "\r\n".join(strs) + + def __repr__(self): + return f"{type(self).__name__}({list(self)!r})" + + +def _options_header_vkw(value: str, kw: dict[str, t.Any]): + return http.dump_options_header( + value, {k.replace("_", "-"): v for k, v in kw.items()} + ) + + +def _str_header_key(key: t.Any) -> str: + if not isinstance(key, str): + warnings.warn( + "Header keys must be strings. Passing other types is deprecated and will" + " not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + + if isinstance(key, bytes): + key = key.decode("latin-1") + else: + key = str(key) + + return key + + +_newline_re = re.compile(r"[\r\n]") + + +def _str_header_value(value: t.Any) -> str: + if isinstance(value, bytes): + warnings.warn( + "Passing bytes as a header value is deprecated and will not be supported in" + " Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + value = value.decode("latin-1") + + if not isinstance(value, str): + value = str(value) + + if _newline_re.search(value) is not None: + raise ValueError("Header values must not contain newline characters.") + + return value + + +class EnvironHeaders(ImmutableHeadersMixin, Headers): + """Read only version of the headers from a WSGI environment. This + provides the same interface as `Headers` and is constructed from + a WSGI environment. + From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a + subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will + render a page for a ``400 BAD REQUEST`` if caught in a catch-all for + HTTP exceptions. + """ + + def __init__(self, environ): + self.environ = environ + + def __eq__(self, other): + return self.environ is other.environ + + __hash__ = None + + def __getitem__(self, key, _get_mode=False): + # _get_mode is a no-op for this class as there is no index but + # used because get() calls it. + if not isinstance(key, str): + raise KeyError(key) + key = key.upper().replace("-", "_") + if key in {"CONTENT_TYPE", "CONTENT_LENGTH"}: + return self.environ[key] + return self.environ[f"HTTP_{key}"] + + def __len__(self): + # the iter is necessary because otherwise list calls our + # len which would call list again and so forth. + return len(list(iter(self))) + + def __iter__(self): + for key, value in self.environ.items(): + if key.startswith("HTTP_") and key not in { + "HTTP_CONTENT_TYPE", + "HTTP_CONTENT_LENGTH", + }: + yield key[5:].replace("_", "-").title(), value + elif key in {"CONTENT_TYPE", "CONTENT_LENGTH"} and value: + yield key.replace("_", "-").title(), value + + def copy(self): + raise TypeError(f"cannot create {type(self).__name__!r} copies") + + +# circular dependencies +from .. import http diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/headers.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/headers.pyi new file mode 100644 index 00000000000..86502221ae8 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/headers.pyi @@ -0,0 +1,109 @@ +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from typing import Literal +from typing import NoReturn +from typing import overload +from typing import TypeVar + +from _typeshed import SupportsKeysAndGetItem +from _typeshed.wsgi import WSGIEnvironment + +from .mixins import ImmutableHeadersMixin + +D = TypeVar("D") +T = TypeVar("T") + +class Headers(dict[str, str]): + _list: list[tuple[str, str]] + def __init__( + self, + defaults: Mapping[str, str | Iterable[str]] + | Iterable[tuple[str, str]] + | None = None, + ) -> None: ... + @overload + def __getitem__(self, key: str) -> str: ... + @overload + def __getitem__(self, key: int) -> tuple[str, str]: ... + @overload + def __getitem__(self, key: slice) -> Headers: ... + @overload + def __getitem__(self, key: str, _get_mode: Literal[True] = ...) -> str: ... + def __eq__(self, other: object) -> bool: ... + @overload # type: ignore + def get(self, key: str, default: str) -> str: ... + @overload + def get(self, key: str, default: str | None = None) -> str | None: ... + @overload + def get( + self, key: str, default: T | None = None, type: Callable[[str], T] = ... + ) -> T | None: ... + @overload + def getlist(self, key: str) -> list[str]: ... + @overload + def getlist(self, key: str, type: Callable[[str], T]) -> list[T]: ... + def get_all(self, name: str) -> list[str]: ... + def items( # type: ignore + self, lower: bool = False + ) -> Iterator[tuple[str, str]]: ... + def keys(self, lower: bool = False) -> Iterator[str]: ... # type: ignore + def values(self) -> Iterator[str]: ... # type: ignore + def extend( + self, + *args: Mapping[str, str | Iterable[str]] | Iterable[tuple[str, str]], + **kwargs: str | Iterable[str], + ) -> None: ... + @overload + def __delitem__(self, key: str | int | slice) -> None: ... + @overload + def __delitem__(self, key: str, _index_operation: Literal[False]) -> None: ... + def remove(self, key: str) -> None: ... + @overload # type: ignore + def pop(self, key: str, default: str | None = None) -> str: ... + @overload + def pop( + self, key: int | None = None, default: tuple[str, str] | None = None + ) -> tuple[str, str]: ... + def popitem(self) -> tuple[str, str]: ... + def __contains__(self, key: str) -> bool: ... # type: ignore + def has_key(self, key: str) -> bool: ... + def __iter__(self) -> Iterator[tuple[str, str]]: ... # type: ignore + def add(self, _key: str, _value: str, **kw: str) -> None: ... + def _validate_value(self, value: str) -> None: ... + def add_header(self, _key: str, _value: str, **_kw: str) -> None: ... + def clear(self) -> None: ... + def set(self, _key: str, _value: str, **kw: str) -> None: ... + def setlist(self, key: str, values: Iterable[str]) -> None: ... + def setdefault(self, key: str, default: str) -> str: ... + def setlistdefault(self, key: str, default: Iterable[str]) -> None: ... + @overload + def __setitem__(self, key: str, value: str) -> None: ... + @overload + def __setitem__(self, key: int, value: tuple[str, str]) -> None: ... + @overload + def __setitem__(self, key: slice, value: Iterable[tuple[str, str]]) -> None: ... + @overload + def update( + self, __m: SupportsKeysAndGetItem[str, str], **kwargs: str | Iterable[str] + ) -> None: ... + @overload + def update( + self, __m: Iterable[tuple[str, str]], **kwargs: str | Iterable[str] + ) -> None: ... + @overload + def update(self, **kwargs: str | Iterable[str]) -> None: ... + def to_wsgi_list(self) -> list[tuple[str, str]]: ... + def copy(self) -> Headers: ... + def __copy__(self) -> Headers: ... + +class EnvironHeaders(ImmutableHeadersMixin, Headers): + environ: WSGIEnvironment + def __init__(self, environ: WSGIEnvironment) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __getitem__( # type: ignore + self, key: str, _get_mode: Literal[False] = False + ) -> str: ... + def __iter__(self) -> Iterator[tuple[str, str]]: ... # type: ignore + def copy(self) -> NoReturn: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/mixins.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/mixins.py new file mode 100644 index 00000000000..2c84ca8f23d --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/mixins.py @@ -0,0 +1,242 @@ +from __future__ import annotations + +from itertools import repeat + +from .._internal import _missing + + +def is_immutable(self): + raise TypeError(f"{type(self).__name__!r} objects are immutable") + + +class ImmutableListMixin: + """Makes a :class:`list` immutable. + + .. versionadded:: 0.5 + + :private: + """ + + _hash_cache = None + + def __hash__(self): + if self._hash_cache is not None: + return self._hash_cache + rv = self._hash_cache = hash(tuple(self)) + return rv + + def __reduce_ex__(self, protocol): + return type(self), (list(self),) + + def __delitem__(self, key): + is_immutable(self) + + def __iadd__(self, other): + is_immutable(self) + + def __imul__(self, other): + is_immutable(self) + + def __setitem__(self, key, value): + is_immutable(self) + + def append(self, item): + is_immutable(self) + + def remove(self, item): + is_immutable(self) + + def extend(self, iterable): + is_immutable(self) + + def insert(self, pos, value): + is_immutable(self) + + def pop(self, index=-1): + is_immutable(self) + + def reverse(self): + is_immutable(self) + + def sort(self, key=None, reverse=False): + is_immutable(self) + + +class ImmutableDictMixin: + """Makes a :class:`dict` immutable. + + .. versionadded:: 0.5 + + :private: + """ + + _hash_cache = None + + @classmethod + def fromkeys(cls, keys, value=None): + instance = super().__new__(cls) + instance.__init__(zip(keys, repeat(value))) + return instance + + def __reduce_ex__(self, protocol): + return type(self), (dict(self),) + + def _iter_hashitems(self): + return self.items() + + def __hash__(self): + if self._hash_cache is not None: + return self._hash_cache + rv = self._hash_cache = hash(frozenset(self._iter_hashitems())) + return rv + + def setdefault(self, key, default=None): + is_immutable(self) + + def update(self, *args, **kwargs): + is_immutable(self) + + def pop(self, key, default=None): + is_immutable(self) + + def popitem(self): + is_immutable(self) + + def __setitem__(self, key, value): + is_immutable(self) + + def __delitem__(self, key): + is_immutable(self) + + def clear(self): + is_immutable(self) + + +class ImmutableMultiDictMixin(ImmutableDictMixin): + """Makes a :class:`MultiDict` immutable. + + .. versionadded:: 0.5 + + :private: + """ + + def __reduce_ex__(self, protocol): + return type(self), (list(self.items(multi=True)),) + + def _iter_hashitems(self): + return self.items(multi=True) + + def add(self, key, value): + is_immutable(self) + + def popitemlist(self): + is_immutable(self) + + def poplist(self, key): + is_immutable(self) + + def setlist(self, key, new_list): + is_immutable(self) + + def setlistdefault(self, key, default_list=None): + is_immutable(self) + + +class ImmutableHeadersMixin: + """Makes a :class:`Headers` immutable. We do not mark them as + hashable though since the only usecase for this datastructure + in Werkzeug is a view on a mutable structure. + + .. versionadded:: 0.5 + + :private: + """ + + def __delitem__(self, key, **kwargs): + is_immutable(self) + + def __setitem__(self, key, value): + is_immutable(self) + + def set(self, _key, _value, **kwargs): + is_immutable(self) + + def setlist(self, key, values): + is_immutable(self) + + def add(self, _key, _value, **kwargs): + is_immutable(self) + + def add_header(self, _key, _value, **_kwargs): + is_immutable(self) + + def remove(self, key): + is_immutable(self) + + def extend(self, *args, **kwargs): + is_immutable(self) + + def update(self, *args, **kwargs): + is_immutable(self) + + def insert(self, pos, value): + is_immutable(self) + + def pop(self, key=None, default=_missing): + is_immutable(self) + + def popitem(self): + is_immutable(self) + + def setdefault(self, key, default): + is_immutable(self) + + def setlistdefault(self, key, default): + is_immutable(self) + + +def _calls_update(name): + def oncall(self, *args, **kw): + rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw) + + if self.on_update is not None: + self.on_update(self) + + return rv + + oncall.__name__ = name + return oncall + + +class UpdateDictMixin(dict): + """Makes dicts call `self.on_update` on modifications. + + .. versionadded:: 0.5 + + :private: + """ + + on_update = None + + def setdefault(self, key, default=None): + modified = key not in self + rv = super().setdefault(key, default) + if modified and self.on_update is not None: + self.on_update(self) + return rv + + def pop(self, key, default=_missing): + modified = key in self + if default is _missing: + rv = super().pop(key) + else: + rv = super().pop(key, default) + if modified and self.on_update is not None: + self.on_update(self) + return rv + + __setitem__ = _calls_update("__setitem__") + __delitem__ = _calls_update("__delitem__") + clear = _calls_update("clear") + popitem = _calls_update("popitem") + update = _calls_update("update") diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/mixins.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/mixins.pyi new file mode 100644 index 00000000000..74ed4b81e2e --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/mixins.pyi @@ -0,0 +1,97 @@ +from collections.abc import Callable +from collections.abc import Hashable +from collections.abc import Iterable +from typing import Any +from typing import NoReturn +from typing import overload +from typing import SupportsIndex +from typing import TypeVar + +from _typeshed import SupportsKeysAndGetItem + +from .headers import Headers + +K = TypeVar("K") +T = TypeVar("T") +V = TypeVar("V") + +def is_immutable(self: object) -> NoReturn: ... + +class ImmutableListMixin(list[V]): + _hash_cache: int | None + def __hash__(self) -> int: ... # type: ignore + def __delitem__(self, key: SupportsIndex | slice) -> NoReturn: ... + def __iadd__(self, other: t.Any) -> NoReturn: ... # type: ignore + def __imul__(self, other: SupportsIndex) -> NoReturn: ... + def __setitem__(self, key: int | slice, value: V) -> NoReturn: ... # type: ignore + def append(self, value: V) -> NoReturn: ... + def remove(self, value: V) -> NoReturn: ... + def extend(self, values: Iterable[V]) -> NoReturn: ... + def insert(self, pos: SupportsIndex, value: V) -> NoReturn: ... + def pop(self, index: SupportsIndex = -1) -> NoReturn: ... + def reverse(self) -> NoReturn: ... + def sort( + self, key: Callable[[V], Any] | None = None, reverse: bool = False + ) -> NoReturn: ... + +class ImmutableDictMixin(dict[K, V]): + _hash_cache: int | None + @classmethod + def fromkeys( # type: ignore + cls, keys: Iterable[K], value: V | None = None + ) -> ImmutableDictMixin[K, V]: ... + def _iter_hashitems(self) -> Iterable[Hashable]: ... + def __hash__(self) -> int: ... # type: ignore + def setdefault(self, key: K, default: V | None = None) -> NoReturn: ... + def update(self, *args: Any, **kwargs: V) -> NoReturn: ... + def pop(self, key: K, default: V | None = None) -> NoReturn: ... # type: ignore + def popitem(self) -> NoReturn: ... + def __setitem__(self, key: K, value: V) -> NoReturn: ... + def __delitem__(self, key: K) -> NoReturn: ... + def clear(self) -> NoReturn: ... + +class ImmutableMultiDictMixin(ImmutableDictMixin[K, V]): + def _iter_hashitems(self) -> Iterable[Hashable]: ... + def add(self, key: K, value: V) -> NoReturn: ... + def popitemlist(self) -> NoReturn: ... + def poplist(self, key: K) -> NoReturn: ... + def setlist(self, key: K, new_list: Iterable[V]) -> NoReturn: ... + def setlistdefault( + self, key: K, default_list: Iterable[V] | None = None + ) -> NoReturn: ... + +class ImmutableHeadersMixin(Headers): + def __delitem__(self, key: Any, _index_operation: bool = True) -> NoReturn: ... + def __setitem__(self, key: Any, value: Any) -> NoReturn: ... + def set(self, _key: Any, _value: Any, **kw: Any) -> NoReturn: ... + def setlist(self, key: Any, values: Any) -> NoReturn: ... + def add(self, _key: Any, _value: Any, **kw: Any) -> NoReturn: ... + def add_header(self, _key: Any, _value: Any, **_kw: Any) -> NoReturn: ... + def remove(self, key: Any) -> NoReturn: ... + def extend(self, *args: Any, **kwargs: Any) -> NoReturn: ... + def update(self, *args: Any, **kwargs: Any) -> NoReturn: ... + def insert(self, pos: Any, value: Any) -> NoReturn: ... + def pop(self, key: Any = None, default: Any = ...) -> NoReturn: ... + def popitem(self) -> NoReturn: ... + def setdefault(self, key: Any, default: Any) -> NoReturn: ... + def setlistdefault(self, key: Any, default: Any) -> NoReturn: ... + +def _calls_update(name: str) -> Callable[[UpdateDictMixin[K, V]], Any]: ... + +class UpdateDictMixin(dict[K, V]): + on_update: Callable[[UpdateDictMixin[K, V] | None, None], None] + def setdefault(self, key: K, default: V | None = None) -> V: ... + @overload + def pop(self, key: K) -> V: ... + @overload + def pop(self, key: K, default: V | T = ...) -> V | T: ... + def __setitem__(self, key: K, value: V) -> None: ... + def __delitem__(self, key: K) -> None: ... + def clear(self) -> None: ... + def popitem(self) -> tuple[K, V]: ... + @overload + def update(self, __m: SupportsKeysAndGetItem[K, V], **kwargs: V) -> None: ... + @overload + def update(self, __m: Iterable[tuple[K, V]], **kwargs: V) -> None: ... + @overload + def update(self, **kwargs: V) -> None: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/range.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/range.py new file mode 100644 index 00000000000..7011ea4ae33 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/range.py @@ -0,0 +1,180 @@ +from __future__ import annotations + + +class IfRange: + """Very simple object that represents the `If-Range` header in parsed + form. It will either have neither a etag or date or one of either but + never both. + + .. versionadded:: 0.7 + """ + + def __init__(self, etag=None, date=None): + #: The etag parsed and unquoted. Ranges always operate on strong + #: etags so the weakness information is not necessary. + self.etag = etag + #: The date in parsed format or `None`. + self.date = date + + def to_header(self): + """Converts the object back into an HTTP header.""" + if self.date is not None: + return http.http_date(self.date) + if self.etag is not None: + return http.quote_etag(self.etag) + return "" + + def __str__(self): + return self.to_header() + + def __repr__(self): + return f"<{type(self).__name__} {str(self)!r}>" + + +class Range: + """Represents a ``Range`` header. All methods only support only + bytes as the unit. Stores a list of ranges if given, but the methods + only work if only one range is provided. + + :raise ValueError: If the ranges provided are invalid. + + .. versionchanged:: 0.15 + The ranges passed in are validated. + + .. versionadded:: 0.7 + """ + + def __init__(self, units, ranges): + #: The units of this range. Usually "bytes". + self.units = units + #: A list of ``(begin, end)`` tuples for the range header provided. + #: The ranges are non-inclusive. + self.ranges = ranges + + for start, end in ranges: + if start is None or (end is not None and (start < 0 or start >= end)): + raise ValueError(f"{(start, end)} is not a valid range.") + + def range_for_length(self, length): + """If the range is for bytes, the length is not None and there is + exactly one range and it is satisfiable it returns a ``(start, stop)`` + tuple, otherwise `None`. + """ + if self.units != "bytes" or length is None or len(self.ranges) != 1: + return None + start, end = self.ranges[0] + if end is None: + end = length + if start < 0: + start += length + if http.is_byte_range_valid(start, end, length): + return start, min(end, length) + return None + + def make_content_range(self, length): + """Creates a :class:`~werkzeug.datastructures.ContentRange` object + from the current range and given content length. + """ + rng = self.range_for_length(length) + if rng is not None: + return ContentRange(self.units, rng[0], rng[1], length) + return None + + def to_header(self): + """Converts the object back into an HTTP header.""" + ranges = [] + for begin, end in self.ranges: + if end is None: + ranges.append(f"{begin}-" if begin >= 0 else str(begin)) + else: + ranges.append(f"{begin}-{end - 1}") + return f"{self.units}={','.join(ranges)}" + + def to_content_range_header(self, length): + """Converts the object into `Content-Range` HTTP header, + based on given length + """ + range = self.range_for_length(length) + if range is not None: + return f"{self.units} {range[0]}-{range[1] - 1}/{length}" + return None + + def __str__(self): + return self.to_header() + + def __repr__(self): + return f"<{type(self).__name__} {str(self)!r}>" + + +def _callback_property(name): + def fget(self): + return getattr(self, name) + + def fset(self, value): + setattr(self, name, value) + if self.on_update is not None: + self.on_update(self) + + return property(fget, fset) + + +class ContentRange: + """Represents the content range header. + + .. versionadded:: 0.7 + """ + + def __init__(self, units, start, stop, length=None, on_update=None): + assert http.is_byte_range_valid(start, stop, length), "Bad range provided" + self.on_update = on_update + self.set(start, stop, length, units) + + #: The units to use, usually "bytes" + units = _callback_property("_units") + #: The start point of the range or `None`. + start = _callback_property("_start") + #: The stop point of the range (non-inclusive) or `None`. Can only be + #: `None` if also start is `None`. + stop = _callback_property("_stop") + #: The length of the range or `None`. + length = _callback_property("_length") + + def set(self, start, stop, length=None, units="bytes"): + """Simple method to update the ranges.""" + assert http.is_byte_range_valid(start, stop, length), "Bad range provided" + self._units = units + self._start = start + self._stop = stop + self._length = length + if self.on_update is not None: + self.on_update(self) + + def unset(self): + """Sets the units to `None` which indicates that the header should + no longer be used. + """ + self.set(None, None, units=None) + + def to_header(self): + if self.units is None: + return "" + if self.length is None: + length = "*" + else: + length = self.length + if self.start is None: + return f"{self.units} */{length}" + return f"{self.units} {self.start}-{self.stop - 1}/{length}" + + def __bool__(self): + return self.units is not None + + def __str__(self): + return self.to_header() + + def __repr__(self): + return f"<{type(self).__name__} {str(self)!r}>" + + +# circular dependencies +from .. import http diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/range.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/range.pyi new file mode 100644 index 00000000000..f38ad69ef10 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/range.pyi @@ -0,0 +1,57 @@ +from collections.abc import Callable +from datetime import datetime + +class IfRange: + etag: str | None + date: datetime | None + def __init__( + self, etag: str | None = None, date: datetime | None = None + ) -> None: ... + def to_header(self) -> str: ... + +class Range: + units: str + ranges: list[tuple[int, int | None]] + def __init__(self, units: str, ranges: list[tuple[int, int | None]]) -> None: ... + def range_for_length(self, length: int | None) -> tuple[int, int] | None: ... + def make_content_range(self, length: int | None) -> ContentRange | None: ... + def to_header(self) -> str: ... + def to_content_range_header(self, length: int | None) -> str | None: ... + +def _callback_property(name: str) -> property: ... + +class ContentRange: + on_update: Callable[[ContentRange], None] | None + def __init__( + self, + units: str | None, + start: int | None, + stop: int | None, + length: int | None = None, + on_update: Callable[[ContentRange], None] | None = None, + ) -> None: ... + @property + def units(self) -> str | None: ... + @units.setter + def units(self, value: str | None) -> None: ... + @property + def start(self) -> int | None: ... + @start.setter + def start(self, value: int | None) -> None: ... + @property + def stop(self) -> int | None: ... + @stop.setter + def stop(self, value: int | None) -> None: ... + @property + def length(self) -> int | None: ... + @length.setter + def length(self, value: int | None) -> None: ... + def set( + self, + start: int | None, + stop: int | None, + length: int | None = None, + units: str | None = "bytes", + ) -> None: ... + def unset(self) -> None: ... + def to_header(self) -> str: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/structures.py b/contrib/python/Werkzeug/py3/werkzeug/datastructures/structures.py new file mode 100644 index 00000000000..7ea7bee283f --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/structures.py @@ -0,0 +1,1006 @@ +from __future__ import annotations + +from collections.abc import MutableSet +from copy import deepcopy + +from .. import exceptions +from .._internal import _missing +from .mixins import ImmutableDictMixin +from .mixins import ImmutableListMixin +from .mixins import ImmutableMultiDictMixin +from .mixins import UpdateDictMixin + + +def is_immutable(self): + raise TypeError(f"{type(self).__name__!r} objects are immutable") + + +def iter_multi_items(mapping): + """Iterates over the items of a mapping yielding keys and values + without dropping any from more complex structures. + """ + if isinstance(mapping, MultiDict): + yield from mapping.items(multi=True) + elif isinstance(mapping, dict): + for key, value in mapping.items(): + if isinstance(value, (tuple, list)): + for v in value: + yield key, v + else: + yield key, value + else: + yield from mapping + + +class ImmutableList(ImmutableListMixin, list): + """An immutable :class:`list`. + + .. versionadded:: 0.5 + + :private: + """ + + def __repr__(self): + return f"{type(self).__name__}({list.__repr__(self)})" + + +class TypeConversionDict(dict): + """Works like a regular dict but the :meth:`get` method can perform + type conversions. :class:`MultiDict` and :class:`CombinedMultiDict` + are subclasses of this class and provide the same feature. + + .. versionadded:: 0.5 + """ + + def get(self, key, default=None, type=None): + """Return the default value if the requested data doesn't exist. + If `type` is provided and is a callable it should convert the value, + return it or raise a :exc:`ValueError` if that is not possible. In + this case the function will return the default as if the value was not + found: + + >>> d = TypeConversionDict(foo='42', bar='blub') + >>> d.get('foo', type=int) + 42 + >>> d.get('bar', -1, type=int) + -1 + + :param key: The key to be looked up. + :param default: The default value to be returned if the key can't + be looked up. If not further specified `None` is + returned. + :param type: A callable that is used to cast the value in the + :class:`MultiDict`. If a :exc:`ValueError` is raised + by this callable the default value is returned. + """ + try: + rv = self[key] + except KeyError: + return default + if type is not None: + try: + rv = type(rv) + except ValueError: + rv = default + return rv + + +class ImmutableTypeConversionDict(ImmutableDictMixin, TypeConversionDict): + """Works like a :class:`TypeConversionDict` but does not support + modifications. + + .. versionadded:: 0.5 + """ + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return TypeConversionDict(self) + + def __copy__(self): + return self + + +class MultiDict(TypeConversionDict): + """A :class:`MultiDict` is a dictionary subclass customized to deal with + multiple values for the same key which is for example used by the parsing + functions in the wrappers. This is necessary because some HTML form + elements pass multiple values for the same key. + + :class:`MultiDict` implements all standard dictionary methods. + Internally, it saves all values for a key as a list, but the standard dict + access methods will only return the first value for a key. If you want to + gain access to the other values, too, you have to use the `list` methods as + explained below. + + Basic Usage: + + >>> d = MultiDict([('a', 'b'), ('a', 'c')]) + >>> d + MultiDict([('a', 'b'), ('a', 'c')]) + >>> d['a'] + 'b' + >>> d.getlist('a') + ['b', 'c'] + >>> 'a' in d + True + + It behaves like a normal dict thus all dict functions will only return the + first value when multiple values for one key are found. + + From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a + subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will + render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP + exceptions. + + A :class:`MultiDict` can be constructed from an iterable of + ``(key, value)`` tuples, a dict, a :class:`MultiDict` or from Werkzeug 0.2 + onwards some keyword parameters. + + :param mapping: the initial value for the :class:`MultiDict`. Either a + regular dict, an iterable of ``(key, value)`` tuples + or `None`. + """ + + def __init__(self, mapping=None): + if isinstance(mapping, MultiDict): + dict.__init__(self, ((k, l[:]) for k, l in mapping.lists())) + elif isinstance(mapping, dict): + tmp = {} + for key, value in mapping.items(): + if isinstance(value, (tuple, list)): + if len(value) == 0: + continue + value = list(value) + else: + value = [value] + tmp[key] = value + dict.__init__(self, tmp) + else: + tmp = {} + for key, value in mapping or (): + tmp.setdefault(key, []).append(value) + dict.__init__(self, tmp) + + def __getstate__(self): + return dict(self.lists()) + + def __setstate__(self, value): + dict.clear(self) + dict.update(self, value) + + def __iter__(self): + # Work around https://bugs.python.org/issue43246. + # (`return super().__iter__()` also works here, which makes this look + # even more like it should be a no-op, yet it isn't.) + return dict.__iter__(self) + + def __getitem__(self, key): + """Return the first data value for this key; + raises KeyError if not found. + + :param key: The key to be looked up. + :raise KeyError: if the key does not exist. + """ + + if key in self: + lst = dict.__getitem__(self, key) + if len(lst) > 0: + return lst[0] + raise exceptions.BadRequestKeyError(key) + + def __setitem__(self, key, value): + """Like :meth:`add` but removes an existing key first. + + :param key: the key for the value. + :param value: the value to set. + """ + dict.__setitem__(self, key, [value]) + + def add(self, key, value): + """Adds a new value for the key. + + .. versionadded:: 0.6 + + :param key: the key for the value. + :param value: the value to add. + """ + dict.setdefault(self, key, []).append(value) + + def getlist(self, key, type=None): + """Return the list of items for a given key. If that key is not in the + `MultiDict`, the return value will be an empty list. Just like `get`, + `getlist` accepts a `type` parameter. All items will be converted + with the callable defined there. + + :param key: The key to be looked up. + :param type: A callable that is used to cast the value in the + :class:`MultiDict`. If a :exc:`ValueError` is raised + by this callable the value will be removed from the list. + :return: a :class:`list` of all the values for the key. + """ + try: + rv = dict.__getitem__(self, key) + except KeyError: + return [] + if type is None: + return list(rv) + result = [] + for item in rv: + try: + result.append(type(item)) + except ValueError: + pass + return result + + def setlist(self, key, new_list): + """Remove the old values for a key and add new ones. Note that the list + you pass the values in will be shallow-copied before it is inserted in + the dictionary. + + >>> d = MultiDict() + >>> d.setlist('foo', ['1', '2']) + >>> d['foo'] + '1' + >>> d.getlist('foo') + ['1', '2'] + + :param key: The key for which the values are set. + :param new_list: An iterable with the new values for the key. Old values + are removed first. + """ + dict.__setitem__(self, key, list(new_list)) + + def setdefault(self, key, default=None): + """Returns the value for the key if it is in the dict, otherwise it + returns `default` and sets that value for `key`. + + :param key: The key to be looked up. + :param default: The default value to be returned if the key is not + in the dict. If not further specified it's `None`. + """ + if key not in self: + self[key] = default + else: + default = self[key] + return default + + def setlistdefault(self, key, default_list=None): + """Like `setdefault` but sets multiple values. The list returned + is not a copy, but the list that is actually used internally. This + means that you can put new values into the dict by appending items + to the list: + + >>> d = MultiDict({"foo": 1}) + >>> d.setlistdefault("foo").extend([2, 3]) + >>> d.getlist("foo") + [1, 2, 3] + + :param key: The key to be looked up. + :param default_list: An iterable of default values. It is either copied + (in case it was a list) or converted into a list + before returned. + :return: a :class:`list` + """ + if key not in self: + default_list = list(default_list or ()) + dict.__setitem__(self, key, default_list) + else: + default_list = dict.__getitem__(self, key) + return default_list + + def items(self, multi=False): + """Return an iterator of ``(key, value)`` pairs. + + :param multi: If set to `True` the iterator returned will have a pair + for each value of each key. Otherwise it will only + contain pairs for the first value of each key. + """ + for key, values in dict.items(self): + if multi: + for value in values: + yield key, value + else: + yield key, values[0] + + def lists(self): + """Return a iterator of ``(key, values)`` pairs, where values is the list + of all values associated with the key.""" + for key, values in dict.items(self): + yield key, list(values) + + def values(self): + """Returns an iterator of the first value on every key's value list.""" + for values in dict.values(self): + yield values[0] + + def listvalues(self): + """Return an iterator of all values associated with a key. Zipping + :meth:`keys` and this is the same as calling :meth:`lists`: + + >>> d = MultiDict({"foo": [1, 2, 3]}) + >>> zip(d.keys(), d.listvalues()) == d.lists() + True + """ + return dict.values(self) + + def copy(self): + """Return a shallow copy of this object.""" + return self.__class__(self) + + def deepcopy(self, memo=None): + """Return a deep copy of this object.""" + return self.__class__(deepcopy(self.to_dict(flat=False), memo)) + + def to_dict(self, flat=True): + """Return the contents as regular dict. If `flat` is `True` the + returned dict will only have the first item present, if `flat` is + `False` all values will be returned as lists. + + :param flat: If set to `False` the dict returned will have lists + with all the values in it. Otherwise it will only + contain the first value for each key. + :return: a :class:`dict` + """ + if flat: + return dict(self.items()) + return dict(self.lists()) + + def update(self, mapping): + """update() extends rather than replaces existing key lists: + + >>> a = MultiDict({'x': 1}) + >>> b = MultiDict({'x': 2, 'y': 3}) + >>> a.update(b) + >>> a + MultiDict([('y', 3), ('x', 1), ('x', 2)]) + + If the value list for a key in ``other_dict`` is empty, no new values + will be added to the dict and the key will not be created: + + >>> x = {'empty_list': []} + >>> y = MultiDict() + >>> y.update(x) + >>> y + MultiDict([]) + """ + for key, value in iter_multi_items(mapping): + MultiDict.add(self, key, value) + + def pop(self, key, default=_missing): + """Pop the first item for a list on the dict. Afterwards the + key is removed from the dict, so additional values are discarded: + + >>> d = MultiDict({"foo": [1, 2, 3]}) + >>> d.pop("foo") + 1 + >>> "foo" in d + False + + :param key: the key to pop. + :param default: if provided the value to return if the key was + not in the dictionary. + """ + try: + lst = dict.pop(self, key) + + if len(lst) == 0: + raise exceptions.BadRequestKeyError(key) + + return lst[0] + except KeyError: + if default is not _missing: + return default + + raise exceptions.BadRequestKeyError(key) from None + + def popitem(self): + """Pop an item from the dict.""" + try: + item = dict.popitem(self) + + if len(item[1]) == 0: + raise exceptions.BadRequestKeyError(item[0]) + + return (item[0], item[1][0]) + except KeyError as e: + raise exceptions.BadRequestKeyError(e.args[0]) from None + + def poplist(self, key): + """Pop the list for a key from the dict. If the key is not in the dict + an empty list is returned. + + .. versionchanged:: 0.5 + If the key does no longer exist a list is returned instead of + raising an error. + """ + return dict.pop(self, key, []) + + def popitemlist(self): + """Pop a ``(key, list)`` tuple from the dict.""" + try: + return dict.popitem(self) + except KeyError as e: + raise exceptions.BadRequestKeyError(e.args[0]) from None + + def __copy__(self): + return self.copy() + + def __deepcopy__(self, memo): + return self.deepcopy(memo=memo) + + def __repr__(self): + return f"{type(self).__name__}({list(self.items(multi=True))!r})" + + +class _omd_bucket: + """Wraps values in the :class:`OrderedMultiDict`. This makes it + possible to keep an order over multiple different keys. It requires + a lot of extra memory and slows down access a lot, but makes it + possible to access elements in O(1) and iterate in O(n). + """ + + __slots__ = ("prev", "key", "value", "next") + + def __init__(self, omd, key, value): + self.prev = omd._last_bucket + self.key = key + self.value = value + self.next = None + + if omd._first_bucket is None: + omd._first_bucket = self + if omd._last_bucket is not None: + omd._last_bucket.next = self + omd._last_bucket = self + + def unlink(self, omd): + if self.prev: + self.prev.next = self.next + if self.next: + self.next.prev = self.prev + if omd._first_bucket is self: + omd._first_bucket = self.next + if omd._last_bucket is self: + omd._last_bucket = self.prev + + +class OrderedMultiDict(MultiDict): + """Works like a regular :class:`MultiDict` but preserves the + order of the fields. To convert the ordered multi dict into a + list you can use the :meth:`items` method and pass it ``multi=True``. + + In general an :class:`OrderedMultiDict` is an order of magnitude + slower than a :class:`MultiDict`. + + .. admonition:: note + + Due to a limitation in Python you cannot convert an ordered + multi dict into a regular dict by using ``dict(multidict)``. + Instead you have to use the :meth:`to_dict` method, otherwise + the internal bucket objects are exposed. + """ + + def __init__(self, mapping=None): + dict.__init__(self) + self._first_bucket = self._last_bucket = None + if mapping is not None: + OrderedMultiDict.update(self, mapping) + + def __eq__(self, other): + if not isinstance(other, MultiDict): + return NotImplemented + if isinstance(other, OrderedMultiDict): + iter1 = iter(self.items(multi=True)) + iter2 = iter(other.items(multi=True)) + try: + for k1, v1 in iter1: + k2, v2 = next(iter2) + if k1 != k2 or v1 != v2: + return False + except StopIteration: + return False + try: + next(iter2) + except StopIteration: + return True + return False + if len(self) != len(other): + return False + for key, values in self.lists(): + if other.getlist(key) != values: + return False + return True + + __hash__ = None + + def __reduce_ex__(self, protocol): + return type(self), (list(self.items(multi=True)),) + + def __getstate__(self): + return list(self.items(multi=True)) + + def __setstate__(self, values): + dict.clear(self) + for key, value in values: + self.add(key, value) + + def __getitem__(self, key): + if key in self: + return dict.__getitem__(self, key)[0].value + raise exceptions.BadRequestKeyError(key) + + def __setitem__(self, key, value): + self.poplist(key) + self.add(key, value) + + def __delitem__(self, key): + self.pop(key) + + def keys(self): + return (key for key, value in self.items()) + + def __iter__(self): + return iter(self.keys()) + + def values(self): + return (value for key, value in self.items()) + + def items(self, multi=False): + ptr = self._first_bucket + if multi: + while ptr is not None: + yield ptr.key, ptr.value + ptr = ptr.next + else: + returned_keys = set() + while ptr is not None: + if ptr.key not in returned_keys: + returned_keys.add(ptr.key) + yield ptr.key, ptr.value + ptr = ptr.next + + def lists(self): + returned_keys = set() + ptr = self._first_bucket + while ptr is not None: + if ptr.key not in returned_keys: + yield ptr.key, self.getlist(ptr.key) + returned_keys.add(ptr.key) + ptr = ptr.next + + def listvalues(self): + for _key, values in self.lists(): + yield values + + def add(self, key, value): + dict.setdefault(self, key, []).append(_omd_bucket(self, key, value)) + + def getlist(self, key, type=None): + try: + rv = dict.__getitem__(self, key) + except KeyError: + return [] + if type is None: + return [x.value for x in rv] + result = [] + for item in rv: + try: + result.append(type(item.value)) + except ValueError: + pass + return result + + def setlist(self, key, new_list): + self.poplist(key) + for value in new_list: + self.add(key, value) + + def setlistdefault(self, key, default_list=None): + raise TypeError("setlistdefault is unsupported for ordered multi dicts") + + def update(self, mapping): + for key, value in iter_multi_items(mapping): + OrderedMultiDict.add(self, key, value) + + def poplist(self, key): + buckets = dict.pop(self, key, ()) + for bucket in buckets: + bucket.unlink(self) + return [x.value for x in buckets] + + def pop(self, key, default=_missing): + try: + buckets = dict.pop(self, key) + except KeyError: + if default is not _missing: + return default + + raise exceptions.BadRequestKeyError(key) from None + + for bucket in buckets: + bucket.unlink(self) + + return buckets[0].value + + def popitem(self): + try: + key, buckets = dict.popitem(self) + except KeyError as e: + raise exceptions.BadRequestKeyError(e.args[0]) from None + + for bucket in buckets: + bucket.unlink(self) + + return key, buckets[0].value + + def popitemlist(self): + try: + key, buckets = dict.popitem(self) + except KeyError as e: + raise exceptions.BadRequestKeyError(e.args[0]) from None + + for bucket in buckets: + bucket.unlink(self) + + return key, [x.value for x in buckets] + + +class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict): + """A read only :class:`MultiDict` that you can pass multiple :class:`MultiDict` + instances as sequence and it will combine the return values of all wrapped + dicts: + + >>> from werkzeug.datastructures import CombinedMultiDict, MultiDict + >>> post = MultiDict([('foo', 'bar')]) + >>> get = MultiDict([('blub', 'blah')]) + >>> combined = CombinedMultiDict([get, post]) + >>> combined['foo'] + 'bar' + >>> combined['blub'] + 'blah' + + This works for all read operations and will raise a `TypeError` for + methods that usually change data which isn't possible. + + From Werkzeug 0.3 onwards, the `KeyError` raised by this class is also a + subclass of the :exc:`~exceptions.BadRequest` HTTP exception and will + render a page for a ``400 BAD REQUEST`` if caught in a catch-all for HTTP + exceptions. + """ + + def __reduce_ex__(self, protocol): + return type(self), (self.dicts,) + + def __init__(self, dicts=None): + self.dicts = list(dicts) or [] + + @classmethod + def fromkeys(cls, keys, value=None): + raise TypeError(f"cannot create {cls.__name__!r} instances by fromkeys") + + def __getitem__(self, key): + for d in self.dicts: + if key in d: + return d[key] + raise exceptions.BadRequestKeyError(key) + + def get(self, key, default=None, type=None): + for d in self.dicts: + if key in d: + if type is not None: + try: + return type(d[key]) + except ValueError: + continue + return d[key] + return default + + def getlist(self, key, type=None): + rv = [] + for d in self.dicts: + rv.extend(d.getlist(key, type)) + return rv + + def _keys_impl(self): + """This function exists so __len__ can be implemented more efficiently, + saving one list creation from an iterator. + """ + rv = set() + rv.update(*self.dicts) + return rv + + def keys(self): + return self._keys_impl() + + def __iter__(self): + return iter(self.keys()) + + def items(self, multi=False): + found = set() + for d in self.dicts: + for key, value in d.items(multi): + if multi: + yield key, value + elif key not in found: + found.add(key) + yield key, value + + def values(self): + for _key, value in self.items(): + yield value + + def lists(self): + rv = {} + for d in self.dicts: + for key, values in d.lists(): + rv.setdefault(key, []).extend(values) + return list(rv.items()) + + def listvalues(self): + return (x[1] for x in self.lists()) + + def copy(self): + """Return a shallow mutable copy of this object. + + This returns a :class:`MultiDict` representing the data at the + time of copying. The copy will no longer reflect changes to the + wrapped dicts. + + .. versionchanged:: 0.15 + Return a mutable :class:`MultiDict`. + """ + return MultiDict(self) + + def to_dict(self, flat=True): + """Return the contents as regular dict. If `flat` is `True` the + returned dict will only have the first item present, if `flat` is + `False` all values will be returned as lists. + + :param flat: If set to `False` the dict returned will have lists + with all the values in it. Otherwise it will only + contain the first item for each key. + :return: a :class:`dict` + """ + if flat: + return dict(self.items()) + + return dict(self.lists()) + + def __len__(self): + return len(self._keys_impl()) + + def __contains__(self, key): + for d in self.dicts: + if key in d: + return True + return False + + def __repr__(self): + return f"{type(self).__name__}({self.dicts!r})" + + +class ImmutableDict(ImmutableDictMixin, dict): + """An immutable :class:`dict`. + + .. versionadded:: 0.5 + """ + + def __repr__(self): + return f"{type(self).__name__}({dict.__repr__(self)})" + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return dict(self) + + def __copy__(self): + return self + + +class ImmutableMultiDict(ImmutableMultiDictMixin, MultiDict): + """An immutable :class:`MultiDict`. + + .. versionadded:: 0.5 + """ + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return MultiDict(self) + + def __copy__(self): + return self + + +class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict): + """An immutable :class:`OrderedMultiDict`. + + .. versionadded:: 0.6 + """ + + def _iter_hashitems(self): + return enumerate(self.items(multi=True)) + + def copy(self): + """Return a shallow mutable copy of this object. Keep in mind that + the standard library's :func:`copy` function is a no-op for this class + like for any other python immutable type (eg: :class:`tuple`). + """ + return OrderedMultiDict(self) + + def __copy__(self): + return self + + +class CallbackDict(UpdateDictMixin, dict): + """A dict that calls a function passed every time something is changed. + The function is passed the dict instance. + """ + + def __init__(self, initial=None, on_update=None): + dict.__init__(self, initial or ()) + self.on_update = on_update + + def __repr__(self): + return f"<{type(self).__name__} {dict.__repr__(self)}>" + + +class HeaderSet(MutableSet): + """Similar to the :class:`ETags` class this implements a set-like structure. + Unlike :class:`ETags` this is case insensitive and used for vary, allow, and + content-language headers. + + If not constructed using the :func:`parse_set_header` function the + instantiation works like this: + + >>> hs = HeaderSet(['foo', 'bar', 'baz']) + >>> hs + HeaderSet(['foo', 'bar', 'baz']) + """ + + def __init__(self, headers=None, on_update=None): + self._headers = list(headers or ()) + self._set = {x.lower() for x in self._headers} + self.on_update = on_update + + def add(self, header): + """Add a new header to the set.""" + self.update((header,)) + + def remove(self, header): + """Remove a header from the set. This raises an :exc:`KeyError` if the + header is not in the set. + + .. versionchanged:: 0.5 + In older versions a :exc:`IndexError` was raised instead of a + :exc:`KeyError` if the object was missing. + + :param header: the header to be removed. + """ + key = header.lower() + if key not in self._set: + raise KeyError(header) + self._set.remove(key) + for idx, key in enumerate(self._headers): + if key.lower() == header: + del self._headers[idx] + break + if self.on_update is not None: + self.on_update(self) + + def update(self, iterable): + """Add all the headers from the iterable to the set. + + :param iterable: updates the set with the items from the iterable. + """ + inserted_any = False + for header in iterable: + key = header.lower() + if key not in self._set: + self._headers.append(header) + self._set.add(key) + inserted_any = True + if inserted_any and self.on_update is not None: + self.on_update(self) + + def discard(self, header): + """Like :meth:`remove` but ignores errors. + + :param header: the header to be discarded. + """ + try: + self.remove(header) + except KeyError: + pass + + def find(self, header): + """Return the index of the header in the set or return -1 if not found. + + :param header: the header to be looked up. + """ + header = header.lower() + for idx, item in enumerate(self._headers): + if item.lower() == header: + return idx + return -1 + + def index(self, header): + """Return the index of the header in the set or raise an + :exc:`IndexError`. + + :param header: the header to be looked up. + """ + rv = self.find(header) + if rv < 0: + raise IndexError(header) + return rv + + def clear(self): + """Clear the set.""" + self._set.clear() + del self._headers[:] + if self.on_update is not None: + self.on_update(self) + + def as_set(self, preserve_casing=False): + """Return the set as real python set type. When calling this, all + the items are converted to lowercase and the ordering is lost. + + :param preserve_casing: if set to `True` the items in the set returned + will have the original case like in the + :class:`HeaderSet`, otherwise they will + be lowercase. + """ + if preserve_casing: + return set(self._headers) + return set(self._set) + + def to_header(self): + """Convert the header set into an HTTP header string.""" + return ", ".join(map(http.quote_header_value, self._headers)) + + def __getitem__(self, idx): + return self._headers[idx] + + def __delitem__(self, idx): + rv = self._headers.pop(idx) + self._set.remove(rv.lower()) + if self.on_update is not None: + self.on_update(self) + + def __setitem__(self, idx, value): + old = self._headers[idx] + self._set.remove(old.lower()) + self._headers[idx] = value + self._set.add(value.lower()) + if self.on_update is not None: + self.on_update(self) + + def __contains__(self, header): + return header.lower() in self._set + + def __len__(self): + return len(self._set) + + def __iter__(self): + return iter(self._headers) + + def __bool__(self): + return bool(self._set) + + def __str__(self): + return self.to_header() + + def __repr__(self): + return f"{type(self).__name__}({self._headers!r})" + + +# circular dependencies +from .. import http diff --git a/contrib/python/Werkzeug/py3/werkzeug/datastructures/structures.pyi b/contrib/python/Werkzeug/py3/werkzeug/datastructures/structures.pyi new file mode 100644 index 00000000000..2e7af35bec1 --- /dev/null +++ b/contrib/python/Werkzeug/py3/werkzeug/datastructures/structures.pyi @@ -0,0 +1,208 @@ +from collections.abc import Callable +from collections.abc import Iterable +from collections.abc import Iterator +from collections.abc import Mapping +from typing import Any +from typing import Generic +from typing import Literal +from typing import NoReturn +from typing import overload +from typing import TypeVar + +from .mixins import ( + ImmutableDictMixin, + ImmutableListMixin, + ImmutableMultiDictMixin, + UpdateDictMixin, +) + +D = TypeVar("D") +K = TypeVar("K") +T = TypeVar("T") +V = TypeVar("V") +_CD = TypeVar("_CD", bound="CallbackDict") + +def is_immutable(self: object) -> NoReturn: ... +def iter_multi_items( + mapping: Mapping[K, V | Iterable[V]] | Iterable[tuple[K, V]] +) -> Iterator[tuple[K, V]]: ... + +class ImmutableList(ImmutableListMixin[V]): ... + +class TypeConversionDict(dict[K, V]): + @overload + def get(self, key: K, default: None = ..., type: None = ...) -> V | None: ... + @overload + def get(self, key: K, default: D, type: None = ...) -> D | V: ... + @overload + def get(self, key: K, default: D, type: Callable[[V], T]) -> D | T: ... + @overload + def get(self, key: K, type: Callable[[V], T]) -> T | None: ... + +class ImmutableTypeConversionDict(ImmutableDictMixin[K, V], TypeConversionDict[K, V]): + def copy(self) -> TypeConversionDict[K, V]: ... + def __copy__(self) -> ImmutableTypeConversionDict: ... + +class MultiDict(TypeConversionDict[K, V]): + def __init__( + self, + mapping: Mapping[K, Iterable[V] | V] | Iterable[tuple[K, V]] | None = None, + ) -> None: ... + def __getitem__(self, item: K) -> V: ... + def __setitem__(self, key: K, value: V) -> None: ... + def add(self, key: K, value: V) -> None: ... + @overload + def getlist(self, key: K) -> list[V]: ... + @overload + def getlist(self, key: K, type: Callable[[V], T] = ...) -> list[T]: ... + def setlist(self, key: K, new_list: Iterable[V]) -> None: ... + def setdefault(self, key: K, default: V | None = None) -> V: ... + def setlistdefault( + self, key: K, default_list: Iterable[V] | None = None + ) -> list[V]: ... + def items(self, multi: bool = False) -> Iterator[tuple[K, V]]: ... # type: ignore + def lists(self) -> Iterator[tuple[K, list[V]]]: ... + def values(self) -> Iterator[V]: ... # type: ignore + def listvalues(self) -> Iterator[list[V]]: ... + def copy(self) -> MultiDict[K, V]: ... + def deepcopy(self, memo: Any = None) -> MultiDict[K, V]: ... + @overload + def to_dict(self) -> dict[K, V]: ... + @overload + def to_dict(self, flat: Literal[False]) -> dict[K, list[V]]: ... + def update( # type: ignore + self, mapping: Mapping[K, Iterable[V] | V] | Iterable[tuple[K, V]] + ) -> None: ... + @overload + def pop(self, key: K) -> V: ... + @overload + def pop(self, key: K, default: V | T = ...) -> V | T: ... + def popitem(self) -> tuple[K, V]: ... + def poplist(self, key: K) -> list[V]: ... + def popitemlist(self) -> tuple[K, list[V]]: ... + def __copy__(self) -> MultiDict[K, V]: ... + def __deepcopy__(self, memo: Any) -> MultiDict[K, V]: ... + +class _omd_bucket(Generic[K, V]): + prev: _omd_bucket | None + next: _omd_bucket | None + key: K + value: V + def __init__(self, omd: OrderedMultiDict, key: K, value: V) -> None: ... + def unlink(self, omd: OrderedMultiDict) -> None: ... + +class OrderedMultiDict(MultiDict[K, V]): + _first_bucket: _omd_bucket | None + _last_bucket: _omd_bucket | None + def __init__(self, mapping: Mapping[K, V] | None = None) -> None: ... + def __eq__(self, other: object) -> bool: ... + def __getitem__(self, key: K) -> V: ... + def __setitem__(self, key: K, value: V) -> None: ... + def __delitem__(self, key: K) -> None: ... + def keys(self) -> Iterator[K]: ... # type: ignore + def __iter__(self) -> Iterator[K]: ... + def values(self) -> Iterator[V]: ... # type: ignore + def items(self, multi: bool = False) -> Iterator[tuple[K, V]]: ... # type: ignore + def lists(self) -> Iterator[tuple[K, list[V]]]: ... + def listvalues(self) -> Iterator[list[V]]: ... + def add(self, key: K, value: V) -> None: ... + @overload + def getlist(self, key: K) -> list[V]: ... + @overload + def getlist(self, key: K, type: Callable[[V], T] = ...) -> list[T]: ... + def setlist(self, key: K, new_list: Iterable[V]) -> None: ... + def setlistdefault( + self, key: K, default_list: Iterable[V] | None = None + ) -> list[V]: ... + def update( # type: ignore + self, mapping: Mapping[K, V] | Iterable[tuple[K, V]] + ) -> None: ... + def poplist(self, key: K) -> list[V]: ... + @overload + def pop(self, key: K) -> V: ... + @overload + def pop(self, key: K, default: V | T = ...) -> V | T: ... + def popitem(self) -> tuple[K, V]: ... + def popitemlist(self) -> tuple[K, list[V]]: ... + +class CombinedMultiDict(ImmutableMultiDictMixin[K, V], MultiDict[K, V]): # type: ignore + dicts: list[MultiDict[K, V]] + def __init__(self, dicts: Iterable[MultiDict[K, V]] | None) -> None: ... + @classmethod + def fromkeys(cls, keys: Any, value: Any = None) -> NoReturn: ... + def __getitem__(self, key: K) -> V: ... + @overload # type: ignore + def get(self, key: K) -> V | None: ... + @overload + def get(self, key: K, default: V | T = ...) -> V | T: ... + @overload + def get( + self, key: K, default: T | None = None, type: Callable[[V], T] = ... + ) -> T | None: ... + @overload + def getlist(self, key: K) -> list[V]: ... + @overload + def getlist(self, key: K, type: Callable[[V], T] = ...) -> list[T]: ... + def _keys_impl(self) -> set[K]: ... + def keys(self) -> set[K]: ... # type: ignore + def __iter__(self) -> set[K]: ... # type: ignore + def items(self, multi: bool = False) -> Iterator[tuple[K, V]]: ... # type: ignore + def values(self) -> Iterator[V]: ... # type: ignore + def lists(self) -> Iterator[tuple[K, list[V]]]: ... + def listvalues(self) -> Iterator[list[V]]: ... + def copy(self) -> MultiDict[K, V]: ... + @overload + def to_dict(self) -> dict[K, V]: ... + @overload + def to_dict(self, flat: Literal[False]) -> dict[K, list[V]]: ... + def __contains__(self, key: K) -> bool: ... # type: ignore + def has_key(self, key: K) -> bool: ... + +class ImmutableDict(ImmutableDictMixin[K, V], dict[K, V]): + def copy(self) -> dict[K, V]: ... + def __copy__(self) -> ImmutableDict[K, V]: ... + +class ImmutableMultiDict( # type: ignore + ImmutableMultiDictMixin[K, V], MultiDict[K, V] +): + def copy(self) -> MultiDict[K, V]: ... + def __copy__(self) -> ImmutableMultiDict[K, V]: ... + +class ImmutableOrderedMultiDict( # type: ignore + ImmutableMultiDictMixin[K, V], OrderedMultiDict[K, V] +): + def _iter_hashitems(self) -> Iterator[tuple[int, tuple[K, V]]]: ... + def copy(self) -> OrderedMultiDict[K, V]: ... + def __copy__(self) -> ImmutableOrderedMultiDict[K, V]: ... + +class CallbackDict(UpdateDictMixin[K, V], dict[K, V]): + def __init__( + self, + initial: Mapping[K, V] | Iterable[tuple[K, V]] | None = None, + on_update: Callable[[_CD], None] | None = None, + ) -> None: ... + +class HeaderSet(set[str]): + _headers: list[str] + _set: set[str] + on_update: Callable[[HeaderSet], None] | None + def __init__( + self, + headers: Iterable[str] | None = None, + on_update: Callable[[HeaderSet], None] | None = None, + ) -> None: ... + def add(self, header: str) -> None: ... + def remove(self, header: str) -> None: ... + def update(self, iterable: Iterable[str]) -> None: ... # type: ignore + def discard(self, header: str) -> None: ... + def find(self, header: str) -> int: ... + def index(self, header: str) -> int: ... + def clear(self) -> None: ... + def as_set(self, preserve_casing: bool = False) -> set[str]: ... + def to_header(self) -> str: ... + def __getitem__(self, idx: int) -> str: ... + def __delitem__(self, idx: int) -> None: ... + def __setitem__(self, idx: int, value: str) -> None: ... + def __contains__(self, header: str) -> bool: ... # type: ignore + def __len__(self) -> int: ... + def __iter__(self) -> Iterator[str]: ... diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py b/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py index 24d19bbdadf..3b04b534ecc 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import getpass import hashlib import json @@ -9,7 +11,6 @@ import time import typing as t import uuid from contextlib import ExitStack -from contextlib import nullcontext from io import BytesIO from itertools import chain from os.path import basename @@ -41,16 +42,16 @@ def hash_pin(pin: str) -> str: return hashlib.sha1(f"{pin} added salt".encode("utf-8", "replace")).hexdigest()[:12] -_machine_id: t.Optional[t.Union[str, bytes]] = None +_machine_id: str | bytes | None = None -def get_machine_id() -> t.Optional[t.Union[str, bytes]]: +def get_machine_id() -> str | bytes | None: global _machine_id if _machine_id is not None: return _machine_id - def _generate() -> t.Optional[t.Union[str, bytes]]: + def _generate() -> str | bytes | None: linux = b"" # machine-id is stable across boots, boot_id is not. @@ -104,7 +105,7 @@ def get_machine_id() -> t.Optional[t.Union[str, bytes]]: 0, winreg.KEY_READ | winreg.KEY_WOW64_64KEY, ) as rk: - guid: t.Union[str, bytes] + guid: str | bytes guid_type: int guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid") @@ -126,7 +127,7 @@ class _ConsoleFrame: standalone console. """ - def __init__(self, namespace: t.Dict[str, t.Any]): + def __init__(self, namespace: dict[str, t.Any]): self.console = Console(namespace) self.id = 0 @@ -135,8 +136,8 @@ class _ConsoleFrame: def get_pin_and_cookie_name( - app: "WSGIApplication", -) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]: + app: WSGIApplication, +) -> tuple[str, str] | tuple[None, None]: """Given an application object this returns a semi-stable 9 digit pin code and a random key. The hope is that this is stable between restarts to not make debugging particularly frustrating. If the pin @@ -161,7 +162,7 @@ def get_pin_and_cookie_name( num = pin modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__) - username: t.Optional[str] + username: str | None try: # getuser imports the pwd module, which does not exist in Google @@ -229,8 +230,8 @@ class DebuggedApplication: The ``evalex`` argument allows evaluating expressions in any frame of a traceback. This works by preserving each frame with its local - state. Some state, such as :doc:`local`, cannot be restored with the - frame by default. When ``evalex`` is enabled, + state. Some state, such as context globals, cannot be restored with + the frame by default. When ``evalex`` is enabled, ``environ["werkzeug.debug.preserve_context"]`` will be a callable that takes a context manager, and can be called multiple times. Each context manager will be entered before evaluating code in the @@ -262,11 +263,11 @@ class DebuggedApplication: def __init__( self, - app: "WSGIApplication", + app: WSGIApplication, evalex: bool = False, request_key: str = "werkzeug.request", console_path: str = "/console", - console_init_func: t.Optional[t.Callable[[], t.Dict[str, t.Any]]] = None, + console_init_func: t.Callable[[], dict[str, t.Any]] | None = None, show_hidden_frames: bool = False, pin_security: bool = True, pin_logging: bool = True, @@ -275,8 +276,8 @@ class DebuggedApplication: console_init_func = None self.app = app self.evalex = evalex - self.frames: t.Dict[int, t.Union[DebugFrameSummary, _ConsoleFrame]] = {} - self.frame_contexts: t.Dict[int, t.List[t.ContextManager[None]]] = {} + self.frames: dict[int, DebugFrameSummary | _ConsoleFrame] = {} + self.frame_contexts: dict[int, list[t.ContextManager[None]]] = {} self.request_key = request_key self.console_path = console_path self.console_init_func = console_init_func @@ -297,7 +298,7 @@ class DebuggedApplication: self.pin = None @property - def pin(self) -> t.Optional[str]: + def pin(self) -> str | None: if not hasattr(self, "_pin"): pin_cookie = get_pin_and_cookie_name(self.app) self._pin, self._pin_cookie = pin_cookie # type: ignore @@ -316,10 +317,10 @@ class DebuggedApplication: return self._pin_cookie def debug_application( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterator[bytes]: """Run the application and conserve the traceback frames.""" - contexts: t.List[t.ContextManager[t.Any]] = [] + contexts: list[t.ContextManager[t.Any]] = [] if self.evalex: environ["werkzeug.debug.preserve_context"] = contexts.append @@ -367,7 +368,7 @@ class DebuggedApplication: self, request: Request, command: str, - frame: t.Union[DebugFrameSummary, _ConsoleFrame], + frame: DebugFrameSummary | _ConsoleFrame, ) -> Response: """Execute a command in a console.""" contexts = self.frame_contexts.get(id(frame), []) @@ -410,7 +411,7 @@ class DebuggedApplication: BytesIO(data), request.environ, download_name=filename, etag=etag ) - def check_pin_trust(self, environ: "WSGIEnvironment") -> t.Optional[bool]: + def check_pin_trust(self, environ: WSGIEnvironment) -> bool | None: """Checks if the request passed the pin test. This returns `True` if the request is trusted on a pin/cookie basis and returns `False` if not. Additionally if the cookie's stored pin hash is wrong it will return @@ -497,7 +498,7 @@ class DebuggedApplication: return Response("") def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: """Dispatch the requests.""" # important: don't ever access a function here that reads the incoming diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/console.py b/contrib/python/Werkzeug/py3/werkzeug/debug/console.py index 69974d1235a..03ddc07f281 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/console.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/console.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import code import sys import typing as t @@ -10,10 +12,7 @@ from .repr import debug_repr from .repr import dump from .repr import helper -if t.TYPE_CHECKING: - import codeop # noqa: F401 - -_stream: ContextVar["HTMLStringO"] = ContextVar("werkzeug.debug.console.stream") +_stream: ContextVar[HTMLStringO] = ContextVar("werkzeug.debug.console.stream") _ipy: ContextVar = ContextVar("werkzeug.debug.console.ipy") @@ -21,7 +20,7 @@ class HTMLStringO: """A StringO version that HTML escapes on write.""" def __init__(self) -> None: - self._buffer: t.List[str] = [] + self._buffer: list[str] = [] def isatty(self) -> bool: return False @@ -48,8 +47,6 @@ class HTMLStringO: return val def _write(self, x: str) -> None: - if isinstance(x, bytes): - x = x.decode("utf-8", "replace") self._buffer.append(x) def write(self, x: str) -> None: @@ -94,7 +91,7 @@ class ThreadedStream: def __setattr__(self, name: str, value: t.Any) -> None: raise AttributeError(f"read only attribute {name}") - def __dir__(self) -> t.List[str]: + def __dir__(self) -> list[str]: return dir(sys.__stdout__) def __getattribute__(self, name: str) -> t.Any: @@ -116,7 +113,7 @@ sys.displayhook = ThreadedStream.displayhook class _ConsoleLoader: def __init__(self) -> None: - self._storage: t.Dict[int, str] = {} + self._storage: dict[int, str] = {} def register(self, code: CodeType, source: str) -> None: self._storage[id(code)] = source @@ -125,7 +122,7 @@ class _ConsoleLoader: if isinstance(var, CodeType): self._storage[id(var)] = source - def get_source_by_code(self, code: CodeType) -> t.Optional[str]: + def get_source_by_code(self, code: CodeType) -> str | None: try: return self._storage[id(code)] except KeyError: @@ -133,9 +130,9 @@ class _ConsoleLoader: class _InteractiveConsole(code.InteractiveInterpreter): - locals: t.Dict[str, t.Any] + locals: dict[str, t.Any] - def __init__(self, globals: t.Dict[str, t.Any], locals: t.Dict[str, t.Any]) -> None: + def __init__(self, globals: dict[str, t.Any], locals: dict[str, t.Any]) -> None: self.loader = _ConsoleLoader() locals = { **globals, @@ -147,7 +144,7 @@ class _InteractiveConsole(code.InteractiveInterpreter): super().__init__(locals) original_compile = self.compile - def compile(source: str, filename: str, symbol: str) -> t.Optional[CodeType]: + def compile(source: str, filename: str, symbol: str) -> CodeType | None: code = original_compile(source, filename, symbol) if code is not None: @@ -157,7 +154,7 @@ class _InteractiveConsole(code.InteractiveInterpreter): self.compile = compile # type: ignore[assignment] self.more = False - self.buffer: t.List[str] = [] + self.buffer: list[str] = [] def runsource(self, source: str, **kwargs: t.Any) -> str: # type: ignore source = f"{source.rstrip()}\n" @@ -188,7 +185,7 @@ class _InteractiveConsole(code.InteractiveInterpreter): te = DebugTraceback(exc, skip=1) sys.stdout._write(te.render_traceback_html()) # type: ignore - def showsyntaxerror(self, filename: t.Optional[str] = None) -> None: + def showsyntaxerror(self, filename: str | None = None) -> None: from .tbtools import DebugTraceback exc = t.cast(BaseException, sys.exc_info()[1]) @@ -204,8 +201,8 @@ class Console: def __init__( self, - globals: t.Optional[t.Dict[str, t.Any]] = None, - locals: t.Optional[t.Dict[str, t.Any]] = None, + globals: dict[str, t.Any] | None = None, + locals: dict[str, t.Any] | None = None, ) -> None: if locals is None: locals = {} diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py b/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py index d9c28da41b4..3bf15a77a19 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/repr.py @@ -4,6 +4,8 @@ repr, these expose more information and produce HTML instead of ASCII. Together with the CSS and JavaScript of the debugger this gives a colorful and more compact output. """ +from __future__ import annotations + import codecs import re import sys @@ -57,7 +59,7 @@ class _Helper: def __repr__(self) -> str: return "Type help(object) for help about object." - def __call__(self, topic: t.Optional[t.Any] = None) -> None: + def __call__(self, topic: t.Any | None = None) -> None: if topic is None: sys.stdout._write(f"<span class=help>{self!r}</span>") # type: ignore return @@ -65,8 +67,6 @@ class _Helper: pydoc.help(topic) rv = sys.stdout.reset() # type: ignore - if isinstance(rv, bytes): - rv = rv.decode("utf-8", "ignore") paragraphs = _paragraph_re.split(rv) if len(paragraphs) > 1: title = paragraphs[0] @@ -81,7 +81,7 @@ helper = _Helper() def _add_subclass_info( - inner: str, obj: object, base: t.Union[t.Type, t.Tuple[t.Type, ...]] + inner: str, obj: object, base: t.Type | tuple[t.Type, ...] ) -> str: if isinstance(base, tuple): for cls in base: @@ -97,8 +97,8 @@ def _add_subclass_info( def _sequence_repr_maker( left: str, right: str, base: t.Type, limit: int = 8 -) -> t.Callable[["DebugReprGenerator", t.Iterable, bool], str]: - def proxy(self: "DebugReprGenerator", obj: t.Iterable, recursive: bool) -> str: +) -> t.Callable[[DebugReprGenerator, t.Iterable, bool], str]: + def proxy(self: DebugReprGenerator, obj: t.Iterable, recursive: bool) -> str: if recursive: return _add_subclass_info(f"{left}...{right}", obj, base) buf = [left] @@ -120,7 +120,7 @@ def _sequence_repr_maker( class DebugReprGenerator: def __init__(self) -> None: - self._stack: t.List[t.Any] = [] + self._stack: list[t.Any] = [] list_repr = _sequence_repr_maker("[", "]", list) tuple_repr = _sequence_repr_maker("(", ")", tuple) @@ -136,7 +136,7 @@ class DebugReprGenerator: pattern = f"r{pattern}" return f're.compile(<span class="string regex">{pattern}</span>)' - def string_repr(self, obj: t.Union[str, bytes], limit: int = 70) -> str: + def string_repr(self, obj: str | bytes, limit: int = 70) -> str: buf = ['<span class="string">'] r = repr(obj) @@ -165,7 +165,7 @@ class DebugReprGenerator: def dict_repr( self, - d: t.Union[t.Dict[int, None], t.Dict[str, int], t.Dict[t.Union[str, int], int]], + d: dict[int, None] | dict[str, int] | dict[str | int, int], recursive: bool, limit: int = 5, ) -> str: @@ -188,9 +188,7 @@ class DebugReprGenerator: buf.append("}") return _add_subclass_info("".join(buf), d, dict) - def object_repr( - self, obj: t.Optional[t.Union[t.Type[dict], t.Callable, t.Type[list]]] - ) -> str: + def object_repr(self, obj: type[dict] | t.Callable | type[list] | None) -> str: r = repr(obj) return f'<span class="object">{escape(r)}</span>' @@ -244,7 +242,7 @@ class DebugReprGenerator: def dump_object(self, obj: object) -> str: repr = None - items: t.Optional[t.List[t.Tuple[str, str]]] = None + items: list[tuple[str, str]] | None = None if isinstance(obj, dict): title = "Contents of" @@ -266,12 +264,12 @@ class DebugReprGenerator: title += f" {object.__repr__(obj)[1:-1]}" return self.render_object_dump(items, title, repr) - def dump_locals(self, d: t.Dict[str, t.Any]) -> str: + def dump_locals(self, d: dict[str, t.Any]) -> str: items = [(key, self.repr(value)) for key, value in d.items()] return self.render_object_dump(items, "Local variables in frame") def render_object_dump( - self, items: t.List[t.Tuple[str, str]], title: str, repr: t.Optional[str] = None + self, items: list[tuple[str, str]], title: str, repr: str | None = None ) -> str: html_items = [] for key, value in items: diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/shared/debugger.js b/contrib/python/Werkzeug/py3/werkzeug/debug/shared/debugger.js index 2354f0300db..f463e9c77e1 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/shared/debugger.js +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/shared/debugger.js @@ -305,7 +305,8 @@ function handleConsoleSubmit(e, command, frameID) { wrapperSpan.append(spanToWrap); spanToWrap.hidden = true; - expansionButton.addEventListener("click", () => { + expansionButton.addEventListener("click", (event) => { + event.preventDefault(); spanToWrap.hidden = !spanToWrap.hidden; expansionButton.classList.toggle("open"); return false; diff --git a/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py b/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py index d56f7390a50..c45f56ef08a 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py +++ b/contrib/python/Werkzeug/py3/werkzeug/debug/tbtools.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import itertools import linecache import os @@ -123,7 +125,7 @@ FRAME_HTML = """\ def _process_traceback( exc: BaseException, - te: t.Optional[traceback.TracebackException] = None, + te: traceback.TracebackException | None = None, *, skip: int = 0, hide: bool = True, @@ -146,7 +148,7 @@ def _process_traceback( frame_gen = itertools.islice(frame_gen, skip, None) del te.stack[:skip] - new_stack: t.List[DebugFrameSummary] = [] + new_stack: list[DebugFrameSummary] = [] hidden = False # Match each frame with the FrameSummary that was generated. @@ -175,7 +177,7 @@ def _process_traceback( elif hide_value or hidden: continue - frame_args: t.Dict[str, t.Any] = { + frame_args: dict[str, t.Any] = { "filename": fs.filename, "lineno": fs.lineno, "name": fs.name, @@ -221,7 +223,7 @@ class DebugTraceback: def __init__( self, exc: BaseException, - te: t.Optional[traceback.TracebackException] = None, + te: traceback.TracebackException | None = None, *, skip: int = 0, hide: bool = True, @@ -234,7 +236,7 @@ class DebugTraceback: @cached_property def all_tracebacks( self, - ) -> t.List[t.Tuple[t.Optional[str], traceback.TracebackException]]: + ) -> list[tuple[str | None, traceback.TracebackException]]: out = [] current = self._te @@ -261,7 +263,7 @@ class DebugTraceback: return out @cached_property - def all_frames(self) -> t.List["DebugFrameSummary"]: + def all_frames(self) -> list[DebugFrameSummary]: return [ f for _, te in self.all_tracebacks for f in te.stack # type: ignore[misc] ] @@ -325,7 +327,7 @@ class DebugTraceback: "evalex": "true" if evalex else "false", "evalex_trusted": "true" if evalex_trusted else "false", "console": "false", - "title": exc_lines[0], + "title": escape(exc_lines[0]), "exception": escape("".join(exc_lines)), "exception_type": escape(self._te.exc_type.__name__), "summary": self.render_traceback_html(include_title=False), @@ -351,8 +353,8 @@ class DebugFrameSummary(traceback.FrameSummary): def __init__( self, *, - locals: t.Dict[str, t.Any], - globals: t.Dict[str, t.Any], + locals: dict[str, t.Any], + globals: dict[str, t.Any], **kwargs: t.Any, ) -> None: super().__init__(locals=None, **kwargs) @@ -360,7 +362,7 @@ class DebugFrameSummary(traceback.FrameSummary): self.global_ns = globals @cached_property - def info(self) -> t.Optional[str]: + def info(self) -> str | None: return self.local_ns.get("__traceback_info__") @cached_property diff --git a/contrib/python/Werkzeug/py3/werkzeug/exceptions.py b/contrib/python/Werkzeug/py3/werkzeug/exceptions.py index 739bd905ba1..2536129180e 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/exceptions.py +++ b/contrib/python/Werkzeug/py3/werkzeug/exceptions.py @@ -43,6 +43,8 @@ code, you can add a second except for a specific subclass of an error: return e """ +from __future__ import annotations + import typing as t from datetime import datetime @@ -52,13 +54,12 @@ from markupsafe import Markup from ._internal import _get_environ if t.TYPE_CHECKING: - import typing_extensions as te from _typeshed.wsgi import StartResponse from _typeshed.wsgi import WSGIEnvironment from .datastructures import WWWAuthenticate from .sansio.response import Response - from .wrappers.request import Request as WSGIRequest # noqa: F401 - from .wrappers.response import Response as WSGIResponse # noqa: F401 + from .wrappers.request import Request as WSGIRequest + from .wrappers.response import Response as WSGIResponse class HTTPException(Exception): @@ -70,13 +71,13 @@ class HTTPException(Exception): Removed the ``wrap`` class method. """ - code: t.Optional[int] = None - description: t.Optional[str] = None + code: int | None = None + description: str | None = None def __init__( self, - description: t.Optional[str] = None, - response: t.Optional["Response"] = None, + description: str | None = None, + response: Response | None = None, ) -> None: super().__init__() if description is not None: @@ -92,14 +93,12 @@ class HTTPException(Exception): def get_description( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, + environ: WSGIEnvironment | None = None, + scope: dict | None = None, ) -> str: """Get the description.""" if self.description is None: description = "" - elif not isinstance(self.description, str): - description = str(self.description) else: description = self.description @@ -108,8 +107,8 @@ class HTTPException(Exception): def get_body( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, + environ: WSGIEnvironment | None = None, + scope: dict | None = None, ) -> str: """Get the HTML body.""" return ( @@ -122,17 +121,17 @@ class HTTPException(Exception): def get_headers( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, - ) -> t.List[t.Tuple[str, str]]: + environ: WSGIEnvironment | None = None, + scope: dict | None = None, + ) -> list[tuple[str, str]]: """Get a list of headers.""" return [("Content-Type", "text/html; charset=utf-8")] def get_response( self, - environ: t.Optional[t.Union["WSGIEnvironment", "WSGIRequest"]] = None, - scope: t.Optional[dict] = None, - ) -> "Response": + environ: WSGIEnvironment | WSGIRequest | None = None, + scope: dict | None = None, + ) -> Response: """Get a response object. If one was passed to the exception it's returned directly. @@ -151,7 +150,7 @@ class HTTPException(Exception): return WSGIResponse(self.get_body(environ, scope), self.code, headers) def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: """Call the exception as WSGI application. @@ -196,7 +195,7 @@ class BadRequestKeyError(BadRequest, KeyError): #: useful in a debug mode. show_exception = False - def __init__(self, arg: t.Optional[str] = None, *args: t.Any, **kwargs: t.Any): + def __init__(self, arg: str | None = None, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) if arg is None: @@ -297,11 +296,9 @@ class Unauthorized(HTTPException): def __init__( self, - description: t.Optional[str] = None, - response: t.Optional["Response"] = None, - www_authenticate: t.Optional[ - t.Union["WWWAuthenticate", t.Iterable["WWWAuthenticate"]] - ] = None, + description: str | None = None, + response: Response | None = None, + www_authenticate: None | (WWWAuthenticate | t.Iterable[WWWAuthenticate]) = None, ) -> None: super().__init__(description, response) @@ -314,9 +311,9 @@ class Unauthorized(HTTPException): def get_headers( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, - ) -> t.List[t.Tuple[str, str]]: + environ: WSGIEnvironment | None = None, + scope: dict | None = None, + ) -> list[tuple[str, str]]: headers = super().get_headers(environ, scope) if self.www_authenticate: headers.extend(("WWW-Authenticate", str(x)) for x in self.www_authenticate) @@ -367,9 +364,9 @@ class MethodNotAllowed(HTTPException): def __init__( self, - valid_methods: t.Optional[t.Iterable[str]] = None, - description: t.Optional[str] = None, - response: t.Optional["Response"] = None, + valid_methods: t.Iterable[str] | None = None, + description: str | None = None, + response: Response | None = None, ) -> None: """Takes an optional list of valid http methods starting with werkzeug 0.3 the list will be mandatory.""" @@ -378,9 +375,9 @@ class MethodNotAllowed(HTTPException): def get_headers( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, - ) -> t.List[t.Tuple[str, str]]: + environ: WSGIEnvironment | None = None, + scope: dict | None = None, + ) -> list[tuple[str, str]]: headers = super().get_headers(environ, scope) if self.valid_methods: headers.append(("Allow", ", ".join(self.valid_methods))) @@ -524,10 +521,10 @@ class RequestedRangeNotSatisfiable(HTTPException): def __init__( self, - length: t.Optional[int] = None, + length: int | None = None, units: str = "bytes", - description: t.Optional[str] = None, - response: t.Optional["Response"] = None, + description: str | None = None, + response: Response | None = None, ) -> None: """Takes an optional `Content-Range` header value based on ``length`` parameter. @@ -538,9 +535,9 @@ class RequestedRangeNotSatisfiable(HTTPException): def get_headers( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, - ) -> t.List[t.Tuple[str, str]]: + environ: WSGIEnvironment | None = None, + scope: dict | None = None, + ) -> list[tuple[str, str]]: headers = super().get_headers(environ, scope) if self.length is not None: headers.append(("Content-Range", f"{self.units} */{self.length}")) @@ -638,18 +635,18 @@ class _RetryAfter(HTTPException): def __init__( self, - description: t.Optional[str] = None, - response: t.Optional["Response"] = None, - retry_after: t.Optional[t.Union[datetime, int]] = None, + description: str | None = None, + response: Response | None = None, + retry_after: datetime | int | None = None, ) -> None: super().__init__(description, response) self.retry_after = retry_after def get_headers( self, - environ: t.Optional["WSGIEnvironment"] = None, - scope: t.Optional[dict] = None, - ) -> t.List[t.Tuple[str, str]]: + environ: WSGIEnvironment | None = None, + scope: dict | None = None, + ) -> list[tuple[str, str]]: headers = super().get_headers(environ, scope) if self.retry_after: @@ -728,9 +725,9 @@ class InternalServerError(HTTPException): def __init__( self, - description: t.Optional[str] = None, - response: t.Optional["Response"] = None, - original_exception: t.Optional[BaseException] = None, + description: str | None = None, + response: Response | None = None, + original_exception: BaseException | None = None, ) -> None: #: The original exception that caused this 500 error. Can be #: used by frameworks to provide context when handling @@ -809,7 +806,7 @@ class HTTPVersionNotSupported(HTTPException): ) -default_exceptions: t.Dict[int, t.Type[HTTPException]] = {} +default_exceptions: dict[int, type[HTTPException]] = {} def _find_exceptions() -> None: @@ -841,8 +838,8 @@ class Aborter: def __init__( self, - mapping: t.Optional[t.Dict[int, t.Type[HTTPException]]] = None, - extra: t.Optional[t.Dict[int, t.Type[HTTPException]]] = None, + mapping: dict[int, type[HTTPException]] | None = None, + extra: dict[int, type[HTTPException]] | None = None, ) -> None: if mapping is None: mapping = default_exceptions @@ -851,8 +848,8 @@ class Aborter: self.mapping.update(extra) def __call__( - self, code: t.Union[int, "Response"], *args: t.Any, **kwargs: t.Any - ) -> "te.NoReturn": + self, code: int | Response, *args: t.Any, **kwargs: t.Any + ) -> t.NoReturn: from .sansio.response import Response if isinstance(code, Response): @@ -864,9 +861,7 @@ class Aborter: raise self.mapping[code](*args, **kwargs) -def abort( - status: t.Union[int, "Response"], *args: t.Any, **kwargs: t.Any -) -> "te.NoReturn": +def abort(status: int | Response, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: """Raises an :py:exc:`HTTPException` for the given status code or WSGI application. diff --git a/contrib/python/Werkzeug/py3/werkzeug/formparser.py b/contrib/python/Werkzeug/py3/werkzeug/formparser.py index bebb2fc8fe1..25ef0d61b13 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/formparser.py +++ b/contrib/python/Werkzeug/py3/werkzeug/formparser.py @@ -1,13 +1,15 @@ +from __future__ import annotations + import typing as t -from functools import update_wrapper +import warnings from io import BytesIO -from itertools import chain -from typing import Union +from urllib.parse import parse_qsl -from . import exceptions +from ._internal import _plain_int from .datastructures import FileStorage from .datastructures import Headers from .datastructures import MultiDict +from .exceptions import RequestEntityTooLarge from .http import parse_options_header from .sansio.multipart import Data from .sansio.multipart import Epilogue @@ -15,8 +17,6 @@ from .sansio.multipart import Field from .sansio.multipart import File from .sansio.multipart import MultipartDecoder from .sansio.multipart import NeedData -from .urls import url_decode_stream -from .wsgi import _make_chunk_iter from .wsgi import get_content_length from .wsgi import get_input_stream @@ -38,10 +38,10 @@ if t.TYPE_CHECKING: class TStreamFactory(te.Protocol): def __call__( self, - total_content_length: t.Optional[int], - content_type: t.Optional[str], - filename: t.Optional[str], - content_length: t.Optional[int] = None, + total_content_length: int | None, + content_type: str | None, + filename: str | None, + content_length: int | None = None, ) -> t.IO[bytes]: ... @@ -49,17 +49,11 @@ if t.TYPE_CHECKING: F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -def _exhaust(stream: t.IO[bytes]) -> None: - bts = stream.read(64 * 1024) - while bts: - bts = stream.read(64 * 1024) - - def default_stream_factory( - total_content_length: t.Optional[int], - content_type: t.Optional[str], - filename: t.Optional[str], - content_length: t.Optional[int] = None, + total_content_length: int | None, + content_type: str | None, + filename: str | None, + content_length: int | None = None, ) -> t.IO[bytes]: max_size = 1024 * 500 @@ -72,15 +66,17 @@ def default_stream_factory( def parse_form_data( - environ: "WSGIEnvironment", - stream_factory: t.Optional["TStreamFactory"] = None, - charset: str = "utf-8", - errors: str = "replace", - max_form_memory_size: t.Optional[int] = None, - max_content_length: t.Optional[int] = None, - cls: t.Optional[t.Type[MultiDict]] = None, + environ: WSGIEnvironment, + stream_factory: TStreamFactory | None = None, + charset: str | None = None, + errors: str | None = None, + max_form_memory_size: int | None = None, + max_content_length: int | None = None, + cls: type[MultiDict] | None = None, silent: bool = True, -) -> "t_parse_result": + *, + max_form_parts: int | None = None, +) -> t_parse_result: """Parse the form data in the environ and return it as tuple in the form ``(stream, form, files)``. You should only call this method if the transport method is `POST`, `PUT`, or `PATCH`. @@ -92,21 +88,10 @@ def parse_form_data( This is a shortcut for the common usage of :class:`FormDataParser`. - Have a look at :doc:`/request_data` for more details. - - .. versionadded:: 0.5 - The `max_form_memory_size`, `max_content_length` and - `cls` parameters were added. - - .. versionadded:: 0.5.1 - The optional `silent` flag was added. - :param environ: the WSGI environment to be used for parsing. :param stream_factory: An optional callable that returns a new read and writeable file descriptor. This callable works the same as :meth:`Response._get_file_stream`. - :param charset: The character set for URL and url encoded form data. - :param errors: The encoding error behavior. :param max_form_memory_size: the maximum number of bytes to be accepted for in-memory stored form data. If the data exceeds the value specified an @@ -119,38 +104,34 @@ def parse_form_data( :param cls: an optional dict class to use. If this is not specified or `None` the default :class:`MultiDict` is used. :param silent: If set to False parsing errors will not be caught. + :param max_form_parts: The maximum number of multipart parts to be parsed. If this + is exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised. :return: A tuple in the form ``(stream, form, files)``. - """ - return FormDataParser( - stream_factory, - charset, - errors, - max_form_memory_size, - max_content_length, - cls, - silent, - ).parse_from_environ(environ) - -def exhaust_stream(f: F) -> F: - """Helper decorator for methods that exhausts the stream on return.""" + .. versionchanged:: 2.3 + Added the ``max_form_parts`` parameter. - def wrapper(self, stream, *args, **kwargs): # type: ignore - try: - return f(self, stream, *args, **kwargs) - finally: - exhaust = getattr(stream, "exhaust", None) - - if exhaust is not None: - exhaust() - else: - while True: - chunk = stream.read(1024 * 64) + .. versionchanged:: 2.3 + The ``charset`` and ``errors`` parameters are deprecated and will be removed in + Werkzeug 3.0. - if not chunk: - break + .. versionadded:: 0.5.1 + Added the ``silent`` parameter. - return update_wrapper(t.cast(F, wrapper), f) + .. versionadded:: 0.5 + Added the ``max_form_memory_size``, ``max_content_length``, and ``cls`` + parameters. + """ + return FormDataParser( + stream_factory=stream_factory, + charset=charset, + errors=errors, + max_form_memory_size=max_form_memory_size, + max_content_length=max_content_length, + max_form_parts=max_form_parts, + silent=silent, + cls=cls, + ).parse_from_environ(environ) class FormDataParser: @@ -160,13 +141,9 @@ class FormDataParser: untouched stream and expose it as separate attributes on a request object. - .. versionadded:: 0.8 - :param stream_factory: An optional callable that returns a new read and writeable file descriptor. This callable works the same as :meth:`Response._get_file_stream`. - :param charset: The character set for URL and url encoded form data. - :param errors: The encoding error behavior. :param max_form_memory_size: the maximum number of bytes to be accepted for in-memory stored form data. If the data exceeds the value specified an @@ -179,27 +156,62 @@ class FormDataParser: :param cls: an optional dict class to use. If this is not specified or `None` the default :class:`MultiDict` is used. :param silent: If set to False parsing errors will not be caught. - :param max_form_parts: The maximum number of parts to be parsed. If this is - exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised. + :param max_form_parts: The maximum number of multipart parts to be parsed. If this + is exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised. + + .. versionchanged:: 2.3 + The ``charset`` and ``errors`` parameters are deprecated and will be removed in + Werkzeug 3.0. + + .. versionchanged:: 2.3 + The ``parse_functions`` attribute and ``get_parse_func`` methods are deprecated + and will be removed in Werkzeug 3.0. + + .. versionchanged:: 2.2.3 + Added the ``max_form_parts`` parameter. + + .. versionadded:: 0.8 """ def __init__( self, - stream_factory: t.Optional["TStreamFactory"] = None, - charset: str = "utf-8", - errors: str = "replace", - max_form_memory_size: t.Optional[int] = None, - max_content_length: t.Optional[int] = None, - cls: t.Optional[t.Type[MultiDict]] = None, + stream_factory: TStreamFactory | None = None, + charset: str | None = None, + errors: str | None = None, + max_form_memory_size: int | None = None, + max_content_length: int | None = None, + cls: type[MultiDict] | None = None, silent: bool = True, *, - max_form_parts: t.Optional[int] = None, + max_form_parts: int | None = None, ) -> None: if stream_factory is None: stream_factory = default_stream_factory self.stream_factory = stream_factory + + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + self.charset = charset + + if errors is not None: + warnings.warn( + "The 'errors' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + errors = "replace" + self.errors = errors self.max_form_memory_size = max_form_memory_size self.max_content_length = max_content_length @@ -212,33 +224,66 @@ class FormDataParser: self.silent = silent def get_parse_func( - self, mimetype: str, options: t.Dict[str, str] - ) -> t.Optional[ + self, mimetype: str, options: dict[str, str] + ) -> None | ( t.Callable[ - ["FormDataParser", t.IO[bytes], str, t.Optional[int], t.Dict[str, str]], - "t_parse_result", + [FormDataParser, t.IO[bytes], str, int | None, dict[str, str]], + t_parse_result, ] - ]: - return self.parse_functions.get(mimetype) + ): + warnings.warn( + "The 'get_parse_func' method is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + + if mimetype == "multipart/form-data": + return type(self)._parse_multipart + elif mimetype == "application/x-www-form-urlencoded": + return type(self)._parse_urlencoded + elif mimetype == "application/x-url-encoded": + warnings.warn( + "The 'application/x-url-encoded' mimetype is invalid, and will not be" + " treated as 'application/x-www-form-urlencoded' in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + return type(self)._parse_urlencoded + elif mimetype in self.parse_functions: + warnings.warn( + "The 'parse_functions' attribute is deprecated and will be removed in" + " Werkzeug 3.0. Override 'parse' instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.parse_functions[mimetype] - def parse_from_environ(self, environ: "WSGIEnvironment") -> "t_parse_result": + return None + + def parse_from_environ(self, environ: WSGIEnvironment) -> t_parse_result: """Parses the information from the environment as form data. :param environ: the WSGI environment to be used for parsing. :return: A tuple in the form ``(stream, form, files)``. """ - content_type = environ.get("CONTENT_TYPE", "") + stream = get_input_stream(environ, max_content_length=self.max_content_length) content_length = get_content_length(environ) - mimetype, options = parse_options_header(content_type) - return self.parse(get_input_stream(environ), mimetype, content_length, options) + mimetype, options = parse_options_header(environ.get("CONTENT_TYPE")) + return self.parse( + stream, + content_length=content_length, + mimetype=mimetype, + options=options, + ) def parse( self, stream: t.IO[bytes], mimetype: str, - content_length: t.Optional[int], - options: t.Optional[t.Dict[str, str]] = None, - ) -> "t_parse_result": + content_length: int | None, + options: dict[str, str] | None = None, + ) -> t_parse_result: """Parses the information from the given stream, mimetype, content length and mimetype parameters. @@ -248,45 +293,61 @@ class FormDataParser: :param options: optional mimetype parameters (used for the multipart boundary for instance) :return: A tuple in the form ``(stream, form, files)``. + + .. versionchanged:: 2.3 + The ``application/x-url-encoded`` content type is deprecated and will not be + treated as ``application/x-www-form-urlencoded`` in Werkzeug 3.0. """ - if ( - self.max_content_length is not None - and content_length is not None - and content_length > self.max_content_length - ): - # if the input stream is not exhausted, firefox reports Connection Reset - _exhaust(stream) - raise exceptions.RequestEntityTooLarge() + if mimetype == "multipart/form-data": + parse_func = self._parse_multipart + elif mimetype == "application/x-www-form-urlencoded": + parse_func = self._parse_urlencoded + elif mimetype == "application/x-url-encoded": + warnings.warn( + "The 'application/x-url-encoded' mimetype is invalid, and will not be" + " treated as 'application/x-www-form-urlencoded' in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + parse_func = self._parse_urlencoded + elif mimetype in self.parse_functions: + warnings.warn( + "The 'parse_functions' attribute is deprecated and will be removed in" + " Werkzeug 3.0. Override 'parse' instead.", + DeprecationWarning, + stacklevel=2, + ) + parse_func = self.parse_functions[mimetype].__get__(self, type(self)) + else: + return stream, self.cls(), self.cls() if options is None: options = {} - parse_func = self.get_parse_func(mimetype, options) - - if parse_func is not None: - try: - return parse_func(self, stream, mimetype, content_length, options) - except ValueError: - if not self.silent: - raise + try: + return parse_func(stream, mimetype, content_length, options) + except ValueError: + if not self.silent: + raise return stream, self.cls(), self.cls() - @exhaust_stream def _parse_multipart( self, stream: t.IO[bytes], mimetype: str, - content_length: t.Optional[int], - options: t.Dict[str, str], - ) -> "t_parse_result": + content_length: int | None, + options: dict[str, str], + ) -> t_parse_result: + charset = self.charset if self.charset != "utf-8" else None + errors = self.errors if self.errors != "replace" else None parser = MultiPartParser( - self.stream_factory, - self.charset, - self.errors, + stream_factory=self.stream_factory, + charset=charset, + errors=errors, max_form_memory_size=self.max_form_memory_size, - cls=self.cls, max_form_parts=self.max_form_parts, + cls=self.cls, ) boundary = options.get("boundary", "").encode("ascii") @@ -296,65 +357,74 @@ class FormDataParser: form, files = parser.parse(stream, boundary, content_length) return stream, form, files - @exhaust_stream def _parse_urlencoded( self, stream: t.IO[bytes], mimetype: str, - content_length: t.Optional[int], - options: t.Dict[str, str], - ) -> "t_parse_result": + content_length: int | None, + options: dict[str, str], + ) -> t_parse_result: if ( self.max_form_memory_size is not None and content_length is not None and content_length > self.max_form_memory_size ): - # if the input stream is not exhausted, firefox reports Connection Reset - _exhaust(stream) - raise exceptions.RequestEntityTooLarge() + raise RequestEntityTooLarge() + + try: + items = parse_qsl( + stream.read().decode(), + keep_blank_values=True, + encoding=self.charset, + errors="werkzeug.url_quote", + ) + except ValueError as e: + raise RequestEntityTooLarge() from e - form = url_decode_stream(stream, self.charset, errors=self.errors, cls=self.cls) - return stream, form, self.cls() + return stream, self.cls(items), self.cls() - #: mapping of mimetypes to parsing functions - parse_functions: t.Dict[ + parse_functions: dict[ str, t.Callable[ - ["FormDataParser", t.IO[bytes], str, t.Optional[int], t.Dict[str, str]], - "t_parse_result", + [FormDataParser, t.IO[bytes], str, int | None, dict[str, str]], + t_parse_result, ], - ] = { - "multipart/form-data": _parse_multipart, - "application/x-www-form-urlencoded": _parse_urlencoded, - "application/x-url-encoded": _parse_urlencoded, - } - - -def _line_parse(line: str) -> t.Tuple[str, bool]: - """Removes line ending characters and returns a tuple (`stripped_line`, - `is_terminated`). - """ - if line[-2:] == "\r\n": - return line[:-2], True - - elif line[-1:] in {"\r", "\n"}: - return line[:-1], True - - return line, False + ] = {} class MultiPartParser: def __init__( self, - stream_factory: t.Optional["TStreamFactory"] = None, - charset: str = "utf-8", - errors: str = "replace", - max_form_memory_size: t.Optional[int] = None, - cls: t.Optional[t.Type[MultiDict]] = None, + stream_factory: TStreamFactory | None = None, + charset: str | None = None, + errors: str | None = None, + max_form_memory_size: int | None = None, + cls: type[MultiDict] | None = None, buffer_size: int = 64 * 1024, - max_form_parts: t.Optional[int] = None, + max_form_parts: int | None = None, ) -> None: + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + self.charset = charset + + if errors is not None: + warnings.warn( + "The 'errors' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + errors = "replace" + self.errors = errors self.max_form_memory_size = max_form_memory_size self.max_form_parts = max_form_parts @@ -368,10 +438,9 @@ class MultiPartParser: cls = MultiDict self.cls = cls - self.buffer_size = buffer_size - def fail(self, message: str) -> "te.NoReturn": + def fail(self, message: str) -> te.NoReturn: raise ValueError(message) def get_part_charset(self, headers: Headers) -> str: @@ -379,18 +448,23 @@ class MultiPartParser: content_type = headers.get("content-type") if content_type: - mimetype, ct_params = parse_options_header(content_type) - return ct_params.get("charset", self.charset) + parameters = parse_options_header(content_type)[1] + ct_charset = parameters.get("charset", "").lower() + + # A safe list of encodings. Modern clients should only send ASCII or UTF-8. + # This list will not be extended further. + if ct_charset in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: + return ct_charset return self.charset def start_file_streaming( - self, event: File, total_content_length: t.Optional[int] + self, event: File, total_content_length: int | None ) -> t.IO[bytes]: content_type = event.headers.get("content-type") try: - content_length = int(event.headers["content-length"]) + content_length = _plain_int(event.headers["content-length"]) except (KeyError, ValueError): content_length = 0 @@ -403,29 +477,22 @@ class MultiPartParser: return container def parse( - self, stream: t.IO[bytes], boundary: bytes, content_length: t.Optional[int] - ) -> t.Tuple[MultiDict, MultiDict]: - container: t.Union[t.IO[bytes], t.List[bytes]] + self, stream: t.IO[bytes], boundary: bytes, content_length: int | None + ) -> tuple[MultiDict, MultiDict]: + current_part: Field | File + container: t.IO[bytes] | list[bytes] _write: t.Callable[[bytes], t.Any] - iterator = chain( - _make_chunk_iter( - stream, - limit=content_length, - buffer_size=self.buffer_size, - ), - [None], - ) - parser = MultipartDecoder( - boundary, self.max_form_memory_size, max_parts=self.max_form_parts + boundary, + max_form_memory_size=self.max_form_memory_size, + max_parts=self.max_form_parts, ) fields = [] files = [] - current_part: Union[Field, File] - for data in iterator: + for data in _chunk_iter(stream.read, self.buffer_size): parser.receive_data(data) event = parser.next_event() while not isinstance(event, (Epilogue, NeedData)): @@ -463,3 +530,18 @@ class MultiPartParser: event = parser.next_event() return self.cls(fields), self.cls(files) + + +def _chunk_iter(read: t.Callable[[int], bytes], size: int) -> t.Iterator[bytes | None]: + """Read data in chunks for multipart/form-data parsing. Stop if no data is read. + Yield ``None`` at the end to signal end of parsing. + """ + while True: + data = read(size) + + if not data: + break + + yield data + + yield None diff --git a/contrib/python/Werkzeug/py3/werkzeug/http.py b/contrib/python/Werkzeug/py3/werkzeug/http.py index 0a7bc739c54..07d1fd49692 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/http.py +++ b/contrib/python/Werkzeug/py3/werkzeug/http.py @@ -1,7 +1,7 @@ -import base64 +from __future__ import annotations + import email.utils import re -import typing import typing as t import warnings from datetime import date @@ -13,74 +13,20 @@ from enum import Enum from hashlib import sha1 from time import mktime from time import struct_time -from urllib.parse import unquote_to_bytes as _unquote +from urllib.parse import quote +from urllib.parse import unquote from urllib.request import parse_http_list as _parse_list_header -from ._internal import _cookie_quote from ._internal import _dt_as_utc -from ._internal import _make_cookie_domain -from ._internal import _to_bytes -from ._internal import _to_str -from ._internal import _wsgi_decoding_dance +from ._internal import _plain_int if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIEnvironment -# for explanation of "media-range", etc. see Sections 5.3.{1,2} of RFC 7231 -_accept_re = re.compile( - r""" - ( # media-range capturing-parenthesis - [^\s;,]+ # type/subtype - (?:[ \t]*;[ \t]* # ";" - (?: # parameter non-capturing-parenthesis - [^\s;,q][^\s;,]* # token that doesn't start with "q" - | # or - q[^\s;,=][^\s;,]* # token that is more than just "q" - ) - )* # zero or more parameters - ) # end of media-range - (?:[ \t]*;[ \t]*q= # weight is a "q" parameter - (\d*(?:\.\d+)?) # qvalue capturing-parentheses - [^,]* # "extension" accept params: who cares? - )? # accept params are optional - """, - re.VERBOSE, -) _token_chars = frozenset( "!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ^_`abcdefghijklmnopqrstuvwxyz|~" ) _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') -_option_header_piece_re = re.compile( - r""" - ;\s*,?\s* # newlines were replaced with commas - (?P<key> - "[^"\\]*(?:\\.[^"\\]*)*" # quoted string - | - [^\s;,=*]+ # token - ) - (?:\*(?P<count>\d+))? # *1, optional continuation index - \s* - (?: # optionally followed by =value - (?: # equals sign, possibly with encoding - \*\s*=\s* # * indicates extended notation - (?: # optional encoding - (?P<encoding>[^\s]+?) - '(?P<language>[^\s]*?)' - )? - | - =\s* # basic notation - ) - (?P<value> - "[^"\\]*(?:\\.[^"\\]*)*" # quoted string - | - [^;,]+ # token - )? - )? - \s* - """, - flags=re.VERBOSE, -) -_option_header_start_mime_type = re.compile(r",\s*([^;,\s]+)([;,]\s*.+)?") _entity_headers = frozenset( [ "allow", @@ -190,110 +136,191 @@ class COOP(Enum): SAME_ORIGIN = "same-origin" -def _is_extended_parameter(key: str) -> bool: - """Per RFC 5987/8187, "extended" values may *not* be quoted. - This is in keeping with browser implementations. So we test - using this function to see if the key indicates this parameter - follows the `ext-parameter` syntax (using a trailing '*'). - """ - return key.strip().endswith("*") - - def quote_header_value( - value: t.Union[str, int], extra_chars: str = "", allow_token: bool = True + value: t.Any, + extra_chars: str | None = None, + allow_token: bool = True, ) -> str: - """Quote a header value if necessary. + """Add double quotes around a header value. If the header contains only ASCII token + characters, it will be returned unchanged. If the header contains ``"`` or ``\\`` + characters, they will be escaped with an additional ``\\`` character. - .. versionadded:: 0.5 + This is the reverse of :func:`unquote_header_value`. + + :param value: The value to quote. Will be converted to a string. + :param allow_token: Disable to quote the value even if it only has token characters. + + .. versionchanged:: 2.3 + The value is quoted if it is the empty string. + + .. versionchanged:: 2.3 + Passing bytes is deprecated and will not be supported in Werkzeug 3.0. - :param value: the value to quote. - :param extra_chars: a list of extra characters to skip quoting. - :param allow_token: if this is enabled token values are returned - unchanged. + .. versionchanged:: 2.3 + The ``extra_chars`` parameter is deprecated and will be removed in Werkzeug 3.0. + + .. versionadded:: 0.5 """ if isinstance(value, bytes): + warnings.warn( + "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) value = value.decode("latin1") + + if extra_chars is not None: + warnings.warn( + "The 'extra_chars' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + value = str(value) + + if not value: + return '""' + if allow_token: - token_chars = _token_chars | set(extra_chars) - if set(value).issubset(token_chars): + token_chars = _token_chars + + if extra_chars: + token_chars |= set(extra_chars) + + if token_chars.issuperset(value): return value + value = value.replace("\\", "\\\\").replace('"', '\\"') return f'"{value}"' -def unquote_header_value(value: str, is_filename: bool = False) -> str: - r"""Unquotes a header value. (Reversal of :func:`quote_header_value`). - This does not use the real unquoting but what browsers are actually - using for quoting. +def unquote_header_value(value: str, is_filename: bool | None = None) -> str: + """Remove double quotes and decode slash-escaped ``"`` and ``\\`` characters in a + header value. - .. versionadded:: 0.5 + This is the reverse of :func:`quote_header_value`. + + :param value: The header value to unquote. - :param value: the header value to unquote. - :param is_filename: The value represents a filename or path. + .. versionchanged:: 2.3 + The ``is_filename`` parameter is deprecated and will be removed in Werkzeug 3.0. """ - if value and value[0] == value[-1] == '"': - # this is not the real unquoting, but fixing this so that the - # RFC is met will result in bugs with internet explorer and - # probably some other browsers as well. IE for example is - # uploading files with "C:\foo\bar.txt" as filename + if is_filename is not None: + warnings.warn( + "The 'is_filename' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + + if len(value) >= 2 and value[0] == value[-1] == '"': value = value[1:-1] - # if this is a filename and the starting characters look like - # a UNC path, then just return the value without quotes. Using the - # replace sequence below on a UNC path has the effect of turning - # the leading double slash into a single slash and then - # _fix_ie_filename() doesn't work correctly. See #458. - if not is_filename or value[:2] != "\\\\": + if not is_filename: return value.replace("\\\\", "\\").replace('\\"', '"') + return value -def dump_options_header( - header: t.Optional[str], options: t.Mapping[str, t.Optional[t.Union[str, int]]] -) -> str: - """The reverse function to :func:`parse_options_header`. +def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> str: + """Produce a header value and ``key=value`` parameters separated by semicolons + ``;``. For example, the ``Content-Type`` header. + + .. code-block:: python + + dump_options_header("text/html", {"charset": "UTF-8"}) + 'text/html; charset=UTF-8' + + This is the reverse of :func:`parse_options_header`. + + If a value contains non-token characters, it will be quoted. + + If a value is ``None``, the parameter is skipped. + + In some keys for some headers, a UTF-8 value can be encoded using a special + ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will + not produce that format automatically, but if a given key ends with an asterisk + ``*``, the value is assumed to have that form and will not be quoted further. + + :param header: The primary header value. + :param options: Parameters to encode as ``key=value`` pairs. + + .. versionchanged:: 2.3 + Keys with ``None`` values are skipped rather than treated as a bare key. - :param header: the header to dump - :param options: a dict of options to append. + .. versionchanged:: 2.2.3 + If a key ends with ``*``, its value will not be quoted. """ segments = [] + if header is not None: segments.append(header) + for key, value in options.items(): if value is None: - segments.append(key) - elif _is_extended_parameter(key): + continue + + if key[-1] == "*": segments.append(f"{key}={value}") else: segments.append(f"{key}={quote_header_value(value)}") + return "; ".join(segments) def dump_header( - iterable: t.Union[t.Dict[str, t.Union[str, int]], t.Iterable[str]], - allow_token: bool = True, + iterable: dict[str, t.Any] | t.Iterable[t.Any], + allow_token: bool | None = None, ) -> str: - """Dump an HTTP header again. This is the reversal of - :func:`parse_list_header`, :func:`parse_set_header` and - :func:`parse_dict_header`. This also quotes strings that include an - equals sign unless you pass it as dict of key, value pairs. + """Produce a header value from a list of items or ``key=value`` pairs, separated by + commas ``,``. + + This is the reverse of :func:`parse_list_header`, :func:`parse_dict_header`, and + :func:`parse_set_header`. + + If a value contains non-token characters, it will be quoted. + + If a value is ``None``, the key is output alone. + + In some keys for some headers, a UTF-8 value can be encoded using a special + ``key*=UTF-8''value`` form, where ``value`` is percent encoded. This function will + not produce that format automatically, but if a given key ends with an asterisk + ``*``, the value is assumed to have that form and will not be quoted further. + + .. code-block:: python + + dump_header(["foo", "bar baz"]) + 'foo, "bar baz"' + + dump_header({"foo": "bar baz"}) + 'foo="bar baz"' - >>> dump_header({'foo': 'bar baz'}) - 'foo="bar baz"' - >>> dump_header(('foo', 'bar baz')) - 'foo, "bar baz"' + :param iterable: The items to create a header from. - :param iterable: the iterable or dict of values to quote. - :param allow_token: if set to `False` tokens as values are disallowed. - See :func:`quote_header_value` for more details. + .. versionchanged:: 2.3 + The ``allow_token`` parameter is deprecated and will be removed in Werkzeug 3.0. + + .. versionchanged:: 2.2.3 + If a key ends with ``*``, its value will not be quoted. """ + if allow_token is not None: + warnings.warn( + "'The 'allow_token' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + allow_token = True + if isinstance(iterable, dict): items = [] + for key, value in iterable.items(): if value is None: items.append(key) - elif _is_extended_parameter(key): + elif key[-1] == "*": items.append(f"{key}={value}") else: items.append( @@ -301,10 +328,11 @@ def dump_header( ) else: items = [quote_header_value(x, allow_token=allow_token) for x in iterable] + return ", ".join(items) -def dump_csp_header(header: "ds.ContentSecurityPolicy") -> str: +def dump_csp_header(header: ds.ContentSecurityPolicy) -> str: """Dump a Content Security Policy header. These are structured into policies such as "default-src 'self'; @@ -317,187 +345,304 @@ def dump_csp_header(header: "ds.ContentSecurityPolicy") -> str: return "; ".join(f"{key} {value}" for key, value in header.items()) -def parse_list_header(value: str) -> t.List[str]: - """Parse lists as described by RFC 2068 Section 2. - - In particular, parse comma-separated lists where the elements of - the list may include quoted-strings. A quoted-string could - contain a comma. A non-quoted string could have quotes in the - middle. Quotes are removed automatically after parsing. +def parse_list_header(value: str) -> list[str]: + """Parse a header value that consists of a list of comma separated items according + to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__. - It basically works like :func:`parse_set_header` just that items - may appear multiple times and case sensitivity is preserved. + This extends :func:`urllib.request.parse_http_list` to remove surrounding quotes + from values. - The return value is a standard :class:`list`: + .. code-block:: python - >>> parse_list_header('token, "quoted value"') - ['token', 'quoted value'] + parse_list_header('token, "quoted value"') + ['token', 'quoted value'] - To create a header from the :class:`list` again, use the - :func:`dump_header` function. + This is the reverse of :func:`dump_header`. - :param value: a string with a list header. - :return: :class:`list` + :param value: The header value to parse. """ result = [] + for item in _parse_list_header(value): - if item[:1] == item[-1:] == '"': - item = unquote_header_value(item[1:-1]) + if len(item) >= 2 and item[0] == item[-1] == '"': + item = item[1:-1] + result.append(item) + return result -def parse_dict_header(value: str, cls: t.Type[dict] = dict) -> t.Dict[str, str]: - """Parse lists of key, value pairs as described by RFC 2068 Section 2 and - convert them into a python dict (or any other mapping object created from - the type with a dict like interface provided by the `cls` argument): +def parse_dict_header(value: str, cls: type[dict] | None = None) -> dict[str, str]: + """Parse a list header using :func:`parse_list_header`, then parse each item as a + ``key=value`` pair. - >>> d = parse_dict_header('foo="is a fish", bar="as well"') - >>> type(d) is dict - True - >>> sorted(d.items()) - [('bar', 'as well'), ('foo', 'is a fish')] + .. code-block:: python - If there is no value for a key it will be `None`: + parse_dict_header('a=b, c="d, e", f') + {"a": "b", "c": "d, e", "f": None} - >>> parse_dict_header('key_without_value') - {'key_without_value': None} + This is the reverse of :func:`dump_header`. - To create a header from the :class:`dict` again, use the - :func:`dump_header` function. + If a key does not have a value, it is ``None``. - .. versionchanged:: 0.9 - Added support for `cls` argument. + This handles charsets for values as described in + `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8, + and ISO-8859-1 charsets are accepted, otherwise the value remains quoted. + + :param value: The header value to parse. + + .. versionchanged:: 2.3 + Added support for ``key*=charset''value`` encoded items. - :param value: a string with a dict header. - :param cls: callable to use for storage of parsed results. - :return: an instance of `cls` + .. versionchanged:: 2.3 + Passing bytes is deprecated, support will be removed in Werkzeug 3.0. + + .. versionchanged:: 2.3 + The ``cls`` argument is deprecated and will be removed in Werkzeug 3.0. + + .. versionchanged:: 0.9 + The ``cls`` argument was added. """ + if cls is None: + cls = dict + else: + warnings.warn( + "The 'cls' parameter is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + result = cls() + if isinstance(value, bytes): + warnings.warn( + "Passing bytes is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) value = value.decode("latin1") - for item in _parse_list_header(value): - if "=" not in item: - result[item] = None + + for item in parse_list_header(value): + key, has_value, value = item.partition("=") + key = key.strip() + + if not has_value: + result[key] = None continue - name, value = item.split("=", 1) - if value[:1] == value[-1:] == '"': - value = unquote_header_value(value[1:-1]) - result[name] = value + + value = value.strip() + encoding: str | None = None + + if key[-1] == "*": + # key*=charset''value becomes key=value, where value is percent encoded + # adapted from parse_options_header, without the continuation handling + key = key[:-1] + match = _charset_value_re.match(value) + + if match: + # If there is a charset marker in the value, split it off. + encoding, value = match.groups() + encoding = encoding.lower() + + # A safe list of encodings. Modern clients should only send ASCII or UTF-8. + # This list will not be extended further. An invalid encoding will leave the + # value quoted. + if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: + # invalid bytes are replaced during unquoting + value = unquote(value, encoding=encoding) + + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + + result[key] = value + return result -def parse_options_header(value: t.Optional[str]) -> t.Tuple[str, t.Dict[str, str]]: - """Parse a ``Content-Type``-like header into a tuple with the - value and any options: +# https://httpwg.org/specs/rfc9110.html#parameter +_parameter_re = re.compile( + r""" + # don't match multiple empty parts, that causes backtracking + \s*;\s* # find the part delimiter + (?: + ([\w!#$%&'*+\-.^`|~]+) # key, one or more token chars + = # equals, with no space on either side + ( # value, token or quoted string + [\w!#$%&'*+\-.^`|~]+ # one or more token chars + | + "(?:\\\\|\\"|.)*?" # quoted string, consuming slash escapes + ) + )? # optionally match key=value, to account for empty parts + """, + re.ASCII | re.VERBOSE, +) +# https://www.rfc-editor.org/rfc/rfc2231#section-4 +_charset_value_re = re.compile( + r""" + ([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty + [\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty + ([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding + """, + re.ASCII | re.VERBOSE, +) +# https://www.rfc-editor.org/rfc/rfc2231#section-3 +_continuation_re = re.compile(r"\*(\d+)$", re.ASCII) + + +def parse_options_header(value: str | None) -> tuple[str, dict[str, str]]: + """Parse a header that consists of a value with ``key=value`` parameters separated + by semicolons ``;``. For example, the ``Content-Type`` header. + + .. code-block:: python + + parse_options_header("text/html; charset=UTF-8") + ('text/html', {'charset': 'UTF-8'}) + + parse_options_header("") + ("", {}) + + This is the reverse of :func:`dump_options_header`. + + This parses valid parameter parts as described in + `RFC 9110 <https://httpwg.org/specs/rfc9110.html#parameter>`__. Invalid parts are + skipped. + + This handles continuations and charsets as described in + `RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__, although not as + strictly as the RFC. Only ASCII, UTF-8, and ISO-8859-1 charsets are accepted, + otherwise the value remains quoted. - >>> parse_options_header('text/html; charset=utf8') - ('text/html', {'charset': 'utf8'}) + Clients may not be consistent in how they handle a quote character within a quoted + value. The `HTML Standard <https://html.spec.whatwg.org/#multipart-form-data>`__ + replaces it with ``%22`` in multipart form data. + `RFC 9110 <https://httpwg.org/specs/rfc9110.html#quoted.strings>`__ uses backslash + escapes in HTTP headers. Both are decoded to the ``"`` character. - This should is not for ``Cache-Control``-like headers, which use a - different format. For those, use :func:`parse_dict_header`. + Clients may not be consistent in how they handle non-ASCII characters. HTML + documents must declare ``<meta charset=UTF-8>``, otherwise browsers may replace with + HTML character references, which can be decoded using :func:`html.unescape`. :param value: The header value to parse. + :return: ``(value, options)``, where ``options`` is a dict + + .. versionchanged:: 2.3 + Invalid parts, such as keys with no value, quoted keys, and incorrectly quoted + values, are discarded instead of treating as ``None``. + + .. versionchanged:: 2.3 + Only ASCII, UTF-8, and ISO-8859-1 are accepted for charset values. + + .. versionchanged:: 2.3 + Escaped quotes in quoted values, like ``%22`` and ``\\"``, are handled. .. versionchanged:: 2.2 Option names are always converted to lowercase. - .. versionchanged:: 2.1 - The ``multiple`` parameter is deprecated and will be removed in - Werkzeug 2.2. + .. versionchanged:: 2.2 + The ``multiple`` parameter was removed. .. versionchanged:: 0.15 :rfc:`2231` parameter continuations are handled. .. versionadded:: 0.5 """ - if not value: + if value is None: return "", {} - result: t.List[t.Any] = [] + value, _, rest = value.partition(";") + value = value.strip() + rest = rest.strip() - value = "," + value.replace("\n", ",") - while value: - match = _option_header_start_mime_type.match(value) - if not match: - break - result.append(match.group(1)) # mimetype - options: t.Dict[str, str] = {} - # Parse options - rest = match.group(2) - encoding: t.Optional[str] - continued_encoding: t.Optional[str] = None - while rest: - optmatch = _option_header_piece_re.match(rest) - if not optmatch: - break - option, count, encoding, language, option_value = optmatch.groups() - # Continuations don't have to supply the encoding after the - # first line. If we're in a continuation, track the current - # encoding to use for subsequent lines. Reset it when the - # continuation ends. - if not count: - continued_encoding = None - else: - if not encoding: - encoding = continued_encoding - continued_encoding = encoding - option = unquote_header_value(option).lower() + if not value or not rest: + # empty (invalid) value, or value without options + return value, {} - if option_value is not None: - option_value = unquote_header_value(option_value, option == "filename") + rest = f";{rest}" + options: dict[str, str] = {} + encoding: str | None = None + continued_encoding: str | None = None - if encoding is not None: - option_value = _unquote(option_value).decode(encoding) + for pk, pv in _parameter_re.findall(rest): + if not pk: + # empty or invalid part + continue - if count: - # Continuations append to the existing value. For - # simplicity, this ignores the possibility of - # out-of-order indices, which shouldn't happen anyway. - if option_value is not None: - options[option] = options.get(option, "") + option_value - else: - options[option] = option_value # type: ignore[assignment] + pk = pk.lower() + + if pk[-1] == "*": + # key*=charset''value becomes key=value, where value is percent encoded + pk = pk[:-1] + match = _charset_value_re.match(pv) + + if match: + # If there is a valid charset marker in the value, split it off. + encoding, pv = match.groups() + # This might be the empty string, handled next. + encoding = encoding.lower() - rest = rest[optmatch.end() :] - result.append(options) - return tuple(result) # type: ignore[return-value] + # No charset marker, or marker with empty charset value. + if not encoding: + encoding = continued_encoding + + # A safe list of encodings. Modern clients should only send ASCII or UTF-8. + # This list will not be extended further. An invalid encoding will leave the + # value quoted. + if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}: + # Continuation parts don't require their own charset marker. This is + # looser than the RFC, it will persist across different keys and allows + # changing the charset during a continuation. But this implementation is + # much simpler than tracking the full state. + continued_encoding = encoding + # invalid bytes are replaced during unquoting + pv = unquote(pv, encoding=encoding) + + # Remove quotes. At this point the value cannot be empty or a single quote. + if pv[0] == pv[-1] == '"': + # HTTP headers use slash, multipart form data uses percent + pv = pv[1:-1].replace("\\\\", "\\").replace('\\"', '"').replace("%22", '"') + + match = _continuation_re.search(pk) + + if match: + # key*0=a; key*1=b becomes key=ab + pk = pk[: match.start()] + options[pk] = options.get(pk, "") + pv + else: + options[pk] = pv - return tuple(result) if result else ("", {}) # type: ignore[return-value] + return value, options +_q_value_re = re.compile(r"-?\d+(\.\d+)?", re.ASCII) _TAnyAccept = t.TypeVar("_TAnyAccept", bound="ds.Accept") -def parse_accept_header(value: t.Optional[str]) -> "ds.Accept": +def parse_accept_header(value: str | None) -> ds.Accept: ... -def parse_accept_header( - value: t.Optional[str], cls: t.Type[_TAnyAccept] -) -> _TAnyAccept: +def parse_accept_header(value: str | None, cls: type[_TAnyAccept]) -> _TAnyAccept: ... def parse_accept_header( - value: t.Optional[str], cls: t.Optional[t.Type[_TAnyAccept]] = None + value: str | None, cls: type[_TAnyAccept] | None = None ) -> _TAnyAccept: - """Parses an HTTP Accept-* header. This does not implement a complete - valid algorithm but one that supports at least value and quality - extraction. + """Parse an ``Accept`` header according to + `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.accept>`__. - Returns a new :class:`Accept` object (basically a list of ``(value, quality)`` - tuples sorted by the quality with some additional accessor methods). + Returns an :class:`.Accept` instance, which can sort and inspect items based on + their quality parameter. When parsing ``Accept-Charset``, ``Accept-Encoding``, or + ``Accept-Language``, pass the appropriate :class:`.Accept` subclass. - The second parameter can be a subclass of :class:`Accept` that is created - with the parsed values and returned. + :param value: The header value to parse. + :param cls: The :class:`.Accept` class to wrap the result in. + :return: An instance of ``cls``. - :param value: the accept header string to be parsed. - :param cls: the wrapper class for the return value (can be - :class:`Accept` or a subclass thereof) - :return: an instance of `cls`. + .. versionchanged:: 2.3 + Parse according to RFC 9110. Items with invalid ``q`` values are skipped. """ if cls is None: cls = t.cast(t.Type[_TAnyAccept], ds.Accept) @@ -506,38 +651,57 @@ def parse_accept_header( return cls(None) result = [] - for match in _accept_re.finditer(value): - quality_match = match.group(2) - if not quality_match: - quality: float = 1 + + for item in parse_list_header(value): + item, options = parse_options_header(item) + + if "q" in options: + # pop q, remaining options are reconstructed + q_str = options.pop("q").strip() + + if _q_value_re.fullmatch(q_str) is None: + # ignore an invalid q + continue + + q = float(q_str) + + if q < 0 or q > 1: + # ignore an invalid q + continue else: - quality = max(min(float(quality_match), 1), 0) - result.append((match.group(1), quality)) + q = 1 + + if options: + # reconstruct the media type with any options + item = dump_options_header(item, options) + + result.append((item, q)) + return cls(result) -_TAnyCC = t.TypeVar("_TAnyCC", bound="ds._CacheControl") +_TAnyCC = t.TypeVar("_TAnyCC", bound="ds.cache_control._CacheControl") _t_cc_update = t.Optional[t.Callable[[_TAnyCC], None]] def parse_cache_control_header( - value: t.Optional[str], on_update: _t_cc_update, cls: None = None -) -> "ds.RequestCacheControl": + value: str | None, on_update: _t_cc_update, cls: None = None +) -> ds.RequestCacheControl: ... def parse_cache_control_header( - value: t.Optional[str], on_update: _t_cc_update, cls: t.Type[_TAnyCC] + value: str | None, on_update: _t_cc_update, cls: type[_TAnyCC] ) -> _TAnyCC: ... def parse_cache_control_header( - value: t.Optional[str], + value: str | None, on_update: _t_cc_update = None, - cls: t.Optional[t.Type[_TAnyCC]] = None, + cls: type[_TAnyCC] | None = None, ) -> _TAnyCC: """Parse a cache control header. The RFC differs between response and request cache control, this method does not. It's your responsibility @@ -568,24 +732,24 @@ _TAnyCSP = t.TypeVar("_TAnyCSP", bound="ds.ContentSecurityPolicy") _t_csp_update = t.Optional[t.Callable[[_TAnyCSP], None]] def parse_csp_header( - value: t.Optional[str], on_update: _t_csp_update, cls: None = None -) -> "ds.ContentSecurityPolicy": + value: str | None, on_update: _t_csp_update, cls: None = None +) -> ds.ContentSecurityPolicy: ... def parse_csp_header( - value: t.Optional[str], on_update: _t_csp_update, cls: t.Type[_TAnyCSP] + value: str | None, on_update: _t_csp_update, cls: type[_TAnyCSP] ) -> _TAnyCSP: ... def parse_csp_header( - value: t.Optional[str], + value: str | None, on_update: _t_csp_update = None, - cls: t.Optional[t.Type[_TAnyCSP]] = None, + cls: type[_TAnyCSP] | None = None, ) -> _TAnyCSP: """Parse a Content Security Policy header. @@ -619,9 +783,9 @@ def parse_csp_header( def parse_set_header( - value: t.Optional[str], - on_update: t.Optional[t.Callable[["ds.HeaderSet"], None]] = None, -) -> "ds.HeaderSet": + value: str | None, + on_update: t.Callable[[ds.HeaderSet], None] | None = None, +) -> ds.HeaderSet: """Parse a set-like header and return a :class:`~werkzeug.datastructures.HeaderSet` object: @@ -652,8 +816,8 @@ def parse_set_header( def parse_authorization_header( - value: t.Optional[str], -) -> t.Optional["ds.Authorization"]: + value: str | None, +) -> ds.Authorization | None: """Parse an HTTP basic/digest authorization header transmitted by the web browser. The return value is either `None` if the header was invalid or not given, otherwise an :class:`~werkzeug.datastructures.Authorization` @@ -661,46 +825,25 @@ def parse_authorization_header( :param value: the authorization header to parse. :return: a :class:`~werkzeug.datastructures.Authorization` object or `None`. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use :meth:`.Authorization.from_header` instead. """ - if not value: - return None - value = _wsgi_decoding_dance(value) - try: - auth_type, auth_info = value.split(None, 1) - auth_type = auth_type.lower() - except ValueError: - return None - if auth_type == "basic": - try: - username, password = base64.b64decode(auth_info).split(b":", 1) - except Exception: - return None - try: - return ds.Authorization( - "basic", - { - "username": _to_str(username, "utf-8"), - "password": _to_str(password, "utf-8"), - }, - ) - except UnicodeDecodeError: - return None - elif auth_type == "digest": - auth_map = parse_dict_header(auth_info) - for key in "username", "realm", "nonce", "uri", "response": - if key not in auth_map: - return None - if "qop" in auth_map: - if not auth_map.get("nc") or not auth_map.get("cnonce"): - return None - return ds.Authorization("digest", auth_map) - return None + from .datastructures import Authorization + + warnings.warn( + "'parse_authorization_header' is deprecated and will be removed in Werkzeug" + " 2.4. Use 'Authorization.from_header' instead.", + DeprecationWarning, + stacklevel=2, + ) + return Authorization.from_header(value) def parse_www_authenticate_header( - value: t.Optional[str], - on_update: t.Optional[t.Callable[["ds.WWWAuthenticate"], None]] = None, -) -> "ds.WWWAuthenticate": + value: str | None, + on_update: t.Callable[[ds.WWWAuthenticate], None] | None = None, +) -> ds.WWWAuthenticate: """Parse an HTTP WWW-Authenticate header into a :class:`~werkzeug.datastructures.WWWAuthenticate` object. @@ -709,18 +852,29 @@ def parse_www_authenticate_header( on the :class:`~werkzeug.datastructures.WWWAuthenticate` object is changed. :return: a :class:`~werkzeug.datastructures.WWWAuthenticate` object. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use :meth:`.WWWAuthenticate.from_header` + instead. """ - if not value: - return ds.WWWAuthenticate(on_update=on_update) - try: - auth_type, auth_info = value.split(None, 1) - auth_type = auth_type.lower() - except (ValueError, AttributeError): - return ds.WWWAuthenticate(value.strip().lower(), on_update=on_update) - return ds.WWWAuthenticate(auth_type, parse_dict_header(auth_info), on_update) + from .datastructures.auth import WWWAuthenticate + warnings.warn( + "'parse_www_authenticate_header' is deprecated and will be removed in Werkzeug" + " 2.4. Use 'WWWAuthenticate.from_header' instead.", + DeprecationWarning, + stacklevel=2, + ) + rv = WWWAuthenticate.from_header(value) + + if rv is None: + rv = WWWAuthenticate("basic") + + rv._on_update = on_update + return rv -def parse_if_range_header(value: t.Optional[str]) -> "ds.IfRange": + +def parse_if_range_header(value: str | None) -> ds.IfRange: """Parses an if-range header which can be an etag or a date. Returns a :class:`~werkzeug.datastructures.IfRange` object. @@ -739,8 +893,8 @@ def parse_if_range_header(value: t.Optional[str]) -> "ds.IfRange": def parse_range_header( - value: t.Optional[str], make_inclusive: bool = True -) -> t.Optional["ds.Range"]: + value: str | None, make_inclusive: bool = True +) -> ds.Range | None: """Parses a range header into a :class:`~werkzeug.datastructures.Range` object. If the header is missing or malformed `None` is returned. `ranges` is a list of ``(start, stop)`` tuples where the ranges are @@ -764,7 +918,7 @@ def parse_range_header( if last_end < 0: return None try: - begin = int(item) + begin = _plain_int(item) except ValueError: return None end = None @@ -775,7 +929,7 @@ def parse_range_header( end_str = end_str.strip() try: - begin = int(begin_str) + begin = _plain_int(begin_str) except ValueError: return None @@ -783,7 +937,7 @@ def parse_range_header( return None if end_str: try: - end = int(end_str) + 1 + end = _plain_int(end_str) + 1 except ValueError: return None @@ -798,9 +952,9 @@ def parse_range_header( def parse_content_range_header( - value: t.Optional[str], - on_update: t.Optional[t.Callable[["ds.ContentRange"], None]] = None, -) -> t.Optional["ds.ContentRange"]: + value: str | None, + on_update: t.Callable[[ds.ContentRange], None] | None = None, +) -> ds.ContentRange | None: """Parses a range header into a :class:`~werkzeug.datastructures.ContentRange` object or `None` if parsing is not possible. @@ -826,7 +980,7 @@ def parse_content_range_header( length = None else: try: - length = int(length_str) + length = _plain_int(length_str) except ValueError: return None @@ -840,8 +994,8 @@ def parse_content_range_header( start_str, stop_str = rng.split("-", 1) try: - start = int(start_str) - stop = int(stop_str) + 1 + start = _plain_int(start_str) + stop = _plain_int(stop_str) + 1 except ValueError: return None @@ -866,8 +1020,8 @@ def quote_etag(etag: str, weak: bool = False) -> str: def unquote_etag( - etag: t.Optional[str], -) -> t.Union[t.Tuple[str, bool], t.Tuple[None, None]]: + etag: str | None, +) -> tuple[str, bool] | tuple[None, None]: """Unquote a single etag: >>> unquote_etag('W/"bar"') @@ -890,7 +1044,7 @@ def unquote_etag( return etag, weak -def parse_etags(value: t.Optional[str]) -> "ds.ETags": +def parse_etags(value: str | None) -> ds.ETags: """Parse an etag header. :param value: the tag header to parse @@ -928,7 +1082,7 @@ def generate_etag(data: bytes) -> str: return sha1(data).hexdigest() -def parse_date(value: t.Optional[str]) -> t.Optional[datetime]: +def parse_date(value: str | None) -> datetime | None: """Parse an :rfc:`2822` date into a timezone-aware :class:`datetime.datetime` object, or ``None`` if parsing fails. @@ -958,7 +1112,7 @@ def parse_date(value: t.Optional[str]) -> t.Optional[datetime]: def http_date( - timestamp: t.Optional[t.Union[datetime, date, int, float, struct_time]] = None + timestamp: datetime | date | int | float | struct_time | None = None, ) -> str: """Format a datetime object or timestamp into an :rfc:`2822` date string. @@ -989,7 +1143,7 @@ def http_date( return email.utils.formatdate(timestamp, usegmt=True) -def parse_age(value: t.Optional[str] = None) -> t.Optional[timedelta]: +def parse_age(value: str | None = None) -> timedelta | None: """Parses a base-10 integer count of seconds into a timedelta. If parsing fails, the return value is `None`. @@ -1011,7 +1165,7 @@ def parse_age(value: t.Optional[str] = None) -> t.Optional[timedelta]: return None -def dump_age(age: t.Optional[t.Union[timedelta, int]] = None) -> t.Optional[str]: +def dump_age(age: timedelta | int | None = None) -> str | None: """Formats the duration as a base-10 integer. :param age: should be an integer number of seconds, @@ -1032,10 +1186,10 @@ def dump_age(age: t.Optional[t.Union[timedelta, int]] = None) -> t.Optional[str] def is_resource_modified( - environ: "WSGIEnvironment", - etag: t.Optional[str] = None, - data: t.Optional[bytes] = None, - last_modified: t.Optional[t.Union[datetime, str]] = None, + environ: WSGIEnvironment, + etag: str | None = None, + data: bytes | None = None, + last_modified: datetime | str | None = None, ignore_if_range: bool = True, ) -> bool: """Convenience method for conditional requests. @@ -1070,7 +1224,7 @@ def is_resource_modified( def remove_entity_headers( - headers: t.Union["ds.Headers", t.List[t.Tuple[str, str]]], + headers: ds.Headers | list[tuple[str, str]], allowed: t.Iterable[str] = ("expires", "content-location"), ) -> None: """Remove all entity headers from a list or :class:`Headers` object. This @@ -1093,9 +1247,7 @@ def remove_entity_headers( ] -def remove_hop_by_hop_headers( - headers: t.Union["ds.Headers", t.List[t.Tuple[str, str]]] -) -> None: +def remove_hop_by_hop_headers(headers: ds.Headers | list[tuple[str, str]]) -> None: """Remove all HTTP/1.1 "Hop-by-Hop" headers from a list or :class:`Headers` object. This operation works in-place. @@ -1131,11 +1283,11 @@ def is_hop_by_hop_header(header: str) -> bool: def parse_cookie( - header: t.Union["WSGIEnvironment", str, bytes, None], - charset: str = "utf-8", - errors: str = "replace", - cls: t.Optional[t.Type["ds.MultiDict"]] = None, -) -> "ds.MultiDict[str, str]": + header: WSGIEnvironment | str | None, + charset: str | None = None, + errors: str | None = None, + cls: type[ds.MultiDict] | None = None, +) -> ds.MultiDict[str, str]: """Parse a cookie from a string or WSGI environ. The same key can be provided multiple times, the values are stored @@ -1145,44 +1297,62 @@ def parse_cookie( :param header: The cookie header as a string, or a WSGI environ dict with a ``HTTP_COOKIE`` key. - :param charset: The charset for the cookie values. - :param errors: The error behavior for the charset decoding. :param cls: A dict-like class to store the parsed cookies in. Defaults to :class:`MultiDict`. - .. versionchanged:: 1.0.0 - Returns a :class:`MultiDict` instead of a - ``TypeConversionDict``. + .. versionchanged:: 2.3 + Passing bytes, and the ``charset`` and ``errors`` parameters, are deprecated and + will be removed in Werkzeug 3.0. + + .. versionchanged:: 1.0 + Returns a :class:`MultiDict` instead of a ``TypeConversionDict``. .. versionchanged:: 0.5 - Returns a :class:`TypeConversionDict` instead of a regular dict. - The ``cls`` parameter was added. + Returns a :class:`TypeConversionDict` instead of a regular dict. The ``cls`` + parameter was added. """ if isinstance(header, dict): - cookie = header.get("HTTP_COOKIE", "") - elif header is None: - cookie = "" + cookie = header.get("HTTP_COOKIE") + elif isinstance(header, bytes): + warnings.warn( + "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + cookie = header.decode() else: cookie = header + if cookie: + cookie = cookie.encode("latin1").decode() + return _sansio_http.parse_cookie( cookie=cookie, charset=charset, errors=errors, cls=cls ) +_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A) +_cookie_slash_re = re.compile(rb"[\x00-\x19\",;\\\x7f-\xff]", re.A) +_cookie_slash_map = {b'"': b'\\"', b"\\": b"\\\\"} +_cookie_slash_map.update( + (v.to_bytes(1, "big"), b"\\%03o" % v) + for v in [*range(0x20), *b",;", *range(0x7F, 256)] +) + + def dump_cookie( key: str, - value: t.Union[bytes, str] = "", - max_age: t.Optional[t.Union[timedelta, int]] = None, - expires: t.Optional[t.Union[str, datetime, int, float]] = None, - path: t.Optional[str] = "/", - domain: t.Optional[str] = None, + value: str = "", + max_age: timedelta | int | None = None, + expires: str | datetime | int | float | None = None, + path: str | None = "/", + domain: str | None = None, secure: bool = False, httponly: bool = False, - charset: str = "utf-8", + charset: str | None = None, sync_expires: bool = True, max_size: int = 4093, - samesite: t.Optional[str] = None, + samesite: str | None = None, ) -> str: """Create a Set-Cookie header without the ``Set-Cookie`` prefix. @@ -1203,7 +1373,7 @@ def dump_cookie( :param path: limits the cookie to a given path, per default it will span the whole domain. :param domain: Use this if you want to set a cross-domain cookie. For - example, ``domain=".example.com"`` will set a cookie + example, ``domain="example.com"`` will set a cookie that is readable by the domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a cookie will only be readable by the domain that set it. @@ -1222,18 +1392,62 @@ def dump_cookie( .. _`cookie`: http://browsercookielimits.squawky.net/ + .. versionchanged:: 2.3.3 + The ``path`` parameter is ``/`` by default. + + .. versionchanged:: 2.3.1 + The value allows more characters without quoting. + + .. versionchanged:: 2.3 + ``localhost`` and other names without a dot are allowed for the domain. A + leading dot is ignored. + + .. versionchanged:: 2.3 + The ``path`` parameter is ``None`` by default. + + .. versionchanged:: 2.3 + Passing bytes, and the ``charset`` parameter, are deprecated and will be removed + in Werkzeug 3.0. + .. versionchanged:: 1.0.0 The string ``'None'`` is accepted for ``samesite``. """ - key = _to_bytes(key, charset) - value = _to_bytes(value, charset) + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be removed" + " in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" - if path is not None: - from .urls import iri_to_uri + if isinstance(key, bytes): + warnings.warn( + "The 'key' parameter must be a string. Bytes are deprecated" + " and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + key = key.decode() + + if isinstance(value, bytes): + warnings.warn( + "The 'value' parameter must be a string. Bytes are" + " deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + value = value.decode() - path = iri_to_uri(path, charset) + if path is not None: + # safe = https://url.spec.whatwg.org/#url-path-segment-string + # as well as percent for things that are already quoted + # excluding semicolon since it's part of the header syntax + path = quote(path, safe="%!$&'()*+,/:=@", encoding=charset) - domain = _make_cookie_domain(domain) + if domain: + domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii") if isinstance(max_age, timedelta): max_age = int(max_age.total_seconds()) @@ -1250,54 +1464,51 @@ def dump_cookie( if samesite not in {"Strict", "Lax", "None"}: raise ValueError("SameSite must be 'Strict', 'Lax', or 'None'.") - buf = [key + b"=" + _cookie_quote(value)] + # Quote value if it contains characters not allowed by RFC 6265. Slash-escape with + # three octal digits, which matches http.cookies, although the RFC suggests base64. + if not _cookie_no_quote_re.fullmatch(value): + # Work with bytes here, since a UTF-8 character could be multiple bytes. + value = _cookie_slash_re.sub( + lambda m: _cookie_slash_map[m.group()], value.encode(charset) + ).decode("ascii") + value = f'"{value}"' + + # Send a non-ASCII key as mojibake. Everything else should already be ASCII. + # TODO Remove encoding dance, it seems like clients accept UTF-8 keys + buf = [f"{key.encode().decode('latin1')}={value}"] - # XXX: In theory all of these parameters that are not marked with `None` - # should be quoted. Because stdlib did not quote it before I did not - # want to introduce quoting there now. - for k, v, q in ( - (b"Domain", domain, True), - (b"Expires", expires, False), - (b"Max-Age", max_age, False), - (b"Secure", secure, None), - (b"HttpOnly", httponly, None), - (b"Path", path, False), - (b"SameSite", samesite, False), + for k, v in ( + ("Domain", domain), + ("Expires", expires), + ("Max-Age", max_age), + ("Secure", secure), + ("HttpOnly", httponly), + ("Path", path), + ("SameSite", samesite), ): - if q is None: - if v: - buf.append(k) + if v is None or v is False: continue - if v is None: + if v is True: + buf.append(k) continue - tmp = bytearray(k) - if not isinstance(v, (bytes, bytearray)): - v = _to_bytes(str(v), charset) - if q: - v = _cookie_quote(v) - tmp += b"=" + v - buf.append(bytes(tmp)) + buf.append(f"{k}={v}") - # The return value will be an incorrectly encoded latin1 header for - # consistency with the headers object. - rv = b"; ".join(buf) - rv = rv.decode("latin1") + rv = "; ".join(buf) - # Warn if the final value of the cookie is larger than the limit. If the - # cookie is too large, then it may be silently ignored by the browser, - # which can be quite hard to debug. + # Warn if the final value of the cookie is larger than the limit. If the cookie is + # too large, then it may be silently ignored by the browser, which can be quite hard + # to debug. cookie_size = len(rv) if max_size and cookie_size > max_size: value_size = len(value) warnings.warn( - f"The {key.decode(charset)!r} cookie is too large: the value was" - f" {value_size} bytes but the" + f"The '{key}' cookie is too large: the value was {value_size} bytes but the" f" header required {cookie_size - value_size} extra bytes. The final size" f" was {cookie_size} bytes but the limit is {max_size} bytes. Browsers may" - f" silently ignore cookies larger than this.", + " silently ignore cookies larger than this.", stacklevel=2, ) @@ -1305,7 +1516,7 @@ def dump_cookie( def is_byte_range_valid( - start: t.Optional[int], stop: t.Optional[int], length: t.Optional[int] + start: int | None, stop: int | None, length: int | None ) -> bool: """Checks if a given byte content range is valid for the given length. diff --git a/contrib/python/Werkzeug/py3/werkzeug/local.py b/contrib/python/Werkzeug/py3/werkzeug/local.py index 9927a0a1f31..fba80e974ad 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/local.py +++ b/contrib/python/Werkzeug/py3/werkzeug/local.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy import math import operator @@ -18,7 +20,7 @@ T = t.TypeVar("T") F = t.TypeVar("F", bound=t.Callable[..., t.Any]) -def release_local(local: t.Union["Local", "LocalStack"]) -> None: +def release_local(local: Local | LocalStack) -> None: """Release the data for the current context in a :class:`Local` or :class:`LocalStack` without using a :class:`LocalManager`. @@ -49,9 +51,7 @@ class Local: __slots__ = ("__storage",) - def __init__( - self, context_var: t.Optional[ContextVar[t.Dict[str, t.Any]]] = None - ) -> None: + def __init__(self, context_var: ContextVar[dict[str, t.Any]] | None = None) -> None: if context_var is None: # A ContextVar not created at global scope interferes with # Python's garbage collection. However, a local only makes @@ -61,12 +61,10 @@ class Local: object.__setattr__(self, "_Local__storage", context_var) - def __iter__(self) -> t.Iterator[t.Tuple[str, t.Any]]: + def __iter__(self) -> t.Iterator[tuple[str, t.Any]]: return iter(self.__storage.get({}).items()) - def __call__( - self, name: str, *, unbound_message: t.Optional[str] = None - ) -> "LocalProxy": + def __call__(self, name: str, *, unbound_message: str | None = None) -> LocalProxy: """Create a :class:`LocalProxy` that access an attribute on this local namespace. @@ -124,7 +122,7 @@ class LocalStack(t.Generic[T]): __slots__ = ("_storage",) - def __init__(self, context_var: t.Optional[ContextVar[t.List[T]]] = None) -> None: + def __init__(self, context_var: ContextVar[list[T]] | None = None) -> None: if context_var is None: # A ContextVar not created at global scope interferes with # Python's garbage collection. However, a local only makes @@ -137,14 +135,14 @@ class LocalStack(t.Generic[T]): def __release_local__(self) -> None: self._storage.set([]) - def push(self, obj: T) -> t.List[T]: + def push(self, obj: T) -> list[T]: """Add a new item to the top of the stack.""" stack = self._storage.get([]).copy() stack.append(obj) self._storage.set(stack) return stack - def pop(self) -> t.Optional[T]: + def pop(self) -> T | None: """Remove the top item from the stack and return it. If the stack is empty, return ``None``. """ @@ -158,7 +156,7 @@ class LocalStack(t.Generic[T]): return rv @property - def top(self) -> t.Optional[T]: + def top(self) -> T | None: """The topmost item on the stack. If the stack is empty, `None` is returned. """ @@ -170,8 +168,8 @@ class LocalStack(t.Generic[T]): return stack[-1] def __call__( - self, name: t.Optional[str] = None, *, unbound_message: t.Optional[str] = None - ) -> "LocalProxy": + self, name: str | None = None, *, unbound_message: str | None = None + ) -> LocalProxy: """Create a :class:`LocalProxy` that accesses the top of this local stack. @@ -192,9 +190,8 @@ class LocalManager: :param locals: A local or list of locals to manage. - .. versionchanged:: 2.0 - ``ident_func`` is deprecated and will be removed in Werkzeug - 2.1. + .. versionchanged:: 2.1 + The ``ident_func`` was removed. .. versionchanged:: 0.7 The ``ident_func`` parameter was added. @@ -208,9 +205,7 @@ class LocalManager: def __init__( self, - locals: t.Optional[ - t.Union[Local, LocalStack, t.Iterable[t.Union[Local, LocalStack]]] - ] = None, + locals: None | (Local | LocalStack | t.Iterable[Local | LocalStack]) = None, ) -> None: if locals is None: self.locals = [] @@ -226,19 +221,19 @@ class LocalManager: for local in self.locals: release_local(local) - def make_middleware(self, app: "WSGIApplication") -> "WSGIApplication": + def make_middleware(self, app: WSGIApplication) -> WSGIApplication: """Wrap a WSGI application so that local data is released automatically after the response has been sent for a request. """ def application( - environ: "WSGIEnvironment", start_response: "StartResponse" + environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: return ClosingIterator(app(environ, start_response), self.cleanup) return application - def middleware(self, func: "WSGIApplication") -> "WSGIApplication": + def middleware(self, func: WSGIApplication) -> WSGIApplication: """Like :meth:`make_middleware` but used as a decorator on the WSGI application function. @@ -274,23 +269,23 @@ class _ProxyLookup: def __init__( self, - f: t.Optional[t.Callable] = None, - fallback: t.Optional[t.Callable] = None, - class_value: t.Optional[t.Any] = None, + f: t.Callable | None = None, + fallback: t.Callable | None = None, + class_value: t.Any | None = None, is_attr: bool = False, ) -> None: - bind_f: t.Optional[t.Callable[["LocalProxy", t.Any], t.Callable]] + bind_f: t.Callable[[LocalProxy, t.Any], t.Callable] | None if hasattr(f, "__get__"): # A Python function, can be turned into a bound method. - def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: + def bind_f(instance: LocalProxy, obj: t.Any) -> t.Callable: return f.__get__(obj, type(obj)) # type: ignore elif f is not None: # A C function, use partial to bind the first argument. - def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: + def bind_f(instance: LocalProxy, obj: t.Any) -> t.Callable: return partial(f, obj) else: @@ -302,10 +297,10 @@ class _ProxyLookup: self.class_value = class_value self.is_attr = is_attr - def __set_name__(self, owner: "LocalProxy", name: str) -> None: + def __set_name__(self, owner: LocalProxy, name: str) -> None: self.name = name - def __get__(self, instance: "LocalProxy", owner: t.Optional[type] = None) -> t.Any: + def __get__(self, instance: LocalProxy, owner: type | None = None) -> t.Any: if instance is None: if self.class_value is not None: return self.class_value @@ -335,7 +330,7 @@ class _ProxyLookup: def __repr__(self) -> str: return f"proxy {self.name}" - def __call__(self, instance: "LocalProxy", *args: t.Any, **kwargs: t.Any) -> t.Any: + def __call__(self, instance: LocalProxy, *args: t.Any, **kwargs: t.Any) -> t.Any: """Support calling unbound methods from the class. For example, this happens with ``copy.copy``, which does ``type(x).__copy__(x)``. ``type(x)`` can't be proxied, so it @@ -352,12 +347,12 @@ class _ProxyIOp(_ProxyLookup): __slots__ = () def __init__( - self, f: t.Optional[t.Callable] = None, fallback: t.Optional[t.Callable] = None + self, f: t.Callable | None = None, fallback: t.Callable | None = None ) -> None: super().__init__(f, fallback) - def bind_f(instance: "LocalProxy", obj: t.Any) -> t.Callable: - def i_op(self: t.Any, other: t.Any) -> "LocalProxy": + def bind_f(instance: LocalProxy, obj: t.Any) -> t.Callable: + def i_op(self: t.Any, other: t.Any) -> LocalProxy: f(self, other) # type: ignore return instance @@ -471,10 +466,10 @@ class LocalProxy(t.Generic[T]): def __init__( self, - local: t.Union[ContextVar[T], Local, LocalStack[T], t.Callable[[], T]], - name: t.Optional[str] = None, + local: ContextVar[T] | Local | LocalStack[T] | t.Callable[[], T], + name: str | None = None, *, - unbound_message: t.Optional[str] = None, + unbound_message: str | None = None, ) -> None: if name is None: get_name = _identity @@ -497,7 +492,7 @@ class LocalProxy(t.Generic[T]): elif isinstance(local, LocalStack): def _get_current_object() -> T: - obj = local.top # type: ignore[union-attr] + obj = local.top if obj is None: raise RuntimeError(unbound_message) @@ -508,7 +503,7 @@ class LocalProxy(t.Generic[T]): def _get_current_object() -> T: try: - obj = local.get() # type: ignore[union-attr] + obj = local.get() except LookupError: raise RuntimeError(unbound_message) from None @@ -517,7 +512,7 @@ class LocalProxy(t.Generic[T]): elif callable(local): def _get_current_object() -> T: - return get_name(local()) # type: ignore + return get_name(local()) else: raise TypeError(f"Don't know how to proxy '{type(local)}'.") diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/__init__.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/__init__.py index 6ddcf7f5c15..e69de29bb2d 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/__init__.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/__init__.py @@ -1,22 +0,0 @@ -""" -Middleware -========== - -A WSGI middleware is a WSGI application that wraps another application -in order to observe or change its behavior. Werkzeug provides some -middleware for common use cases. - -.. toctree:: - :maxdepth: 1 - - proxy_fix - shared_data - dispatcher - http_proxy - lint - profiler - -The :doc:`interactive debugger </debug>` is also a middleware that can -be applied manually, although it is typically used automatically with -the :doc:`development server </serving>`. -""" diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/dispatcher.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/dispatcher.py index ace1c7504e9..559fea585b0 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/dispatcher.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/dispatcher.py @@ -30,6 +30,8 @@ and the static files would be served directly by the HTTP server. :copyright: 2007 Pallets :license: BSD-3-Clause """ +from __future__ import annotations + import typing as t if t.TYPE_CHECKING: @@ -50,14 +52,14 @@ class DispatcherMiddleware: def __init__( self, - app: "WSGIApplication", - mounts: t.Optional[t.Dict[str, "WSGIApplication"]] = None, + app: WSGIApplication, + mounts: dict[str, WSGIApplication] | None = None, ) -> None: self.app = app self.mounts = mounts or {} def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: script = environ.get("PATH_INFO", "") path_info = "" diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/http_proxy.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/http_proxy.py index 1cde458df33..59ba9b32472 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/http_proxy.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/http_proxy.py @@ -7,13 +7,15 @@ Basic HTTP Proxy :copyright: 2007 Pallets :license: BSD-3-Clause """ +from __future__ import annotations + import typing as t from http import client +from urllib.parse import quote +from urllib.parse import urlsplit from ..datastructures import EnvironHeaders from ..http import is_hop_by_hop_header -from ..urls import url_parse -from ..urls import url_quote from ..wsgi import get_input_stream if t.TYPE_CHECKING: @@ -78,12 +80,12 @@ class ProxyMiddleware: def __init__( self, - app: "WSGIApplication", - targets: t.Mapping[str, t.Dict[str, t.Any]], + app: WSGIApplication, + targets: t.Mapping[str, dict[str, t.Any]], chunk_size: int = 2 << 13, timeout: int = 10, ) -> None: - def _set_defaults(opts: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + def _set_defaults(opts: dict[str, t.Any]) -> dict[str, t.Any]: opts.setdefault("remove_prefix", False) opts.setdefault("host", "<auto>") opts.setdefault("headers", {}) @@ -98,13 +100,14 @@ class ProxyMiddleware: self.timeout = timeout def proxy_to( - self, opts: t.Dict[str, t.Any], path: str, prefix: str - ) -> "WSGIApplication": - target = url_parse(opts["target"]) - host = t.cast(str, target.ascii_host) + self, opts: dict[str, t.Any], path: str, prefix: str + ) -> WSGIApplication: + target = urlsplit(opts["target"]) + # socket can handle unicode host, but header must be ascii + host = target.hostname.encode("idna").decode("ascii") def application( - environ: "WSGIEnvironment", start_response: "StartResponse" + environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: headers = list(EnvironHeaders(environ).items()) headers[:] = [ @@ -157,7 +160,9 @@ class ProxyMiddleware: ) con.connect() - remote_url = url_quote(remote_path) + # safe = https://url.spec.whatwg.org/#url-path-segment-string + # as well as percent for things that are already quoted + remote_url = quote(remote_path, safe="!$&'()*+,/:;=@%") querystring = environ["QUERY_STRING"] if querystring: @@ -217,7 +222,7 @@ class ProxyMiddleware: return application def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: path = environ["PATH_INFO"] app = self.app diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py index fcf3b413147..462959943ba 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/lint.py @@ -12,6 +12,8 @@ common HTTP errors such as non-empty responses for 304 status codes. :copyright: 2007 Pallets :license: BSD-3-Clause """ +from __future__ import annotations + import typing as t from types import TracebackType from urllib.parse import urlparse @@ -117,7 +119,7 @@ class ErrorStream: class GuardedWrite: - def __init__(self, write: t.Callable[[bytes], object], chunks: t.List[int]) -> None: + def __init__(self, write: t.Callable[[bytes], object], chunks: list[int]) -> None: self._write = write self._chunks = chunks @@ -131,8 +133,8 @@ class GuardedIterator: def __init__( self, iterator: t.Iterable[bytes], - headers_set: t.Tuple[int, Headers], - chunks: t.List[int], + headers_set: tuple[int, Headers], + chunks: list[int], ) -> None: self._iterator = iterator self._next = iter(iterator).__next__ @@ -140,7 +142,7 @@ class GuardedIterator: self.headers_set = headers_set self.chunks = chunks - def __iter__(self) -> "GuardedIterator": + def __iter__(self) -> GuardedIterator: return self def __next__(self) -> bytes: @@ -230,10 +232,10 @@ class LintMiddleware: app = LintMiddleware(app) """ - def __init__(self, app: "WSGIApplication") -> None: + def __init__(self, app: WSGIApplication) -> None: self.app = app - def check_environ(self, environ: "WSGIEnvironment") -> None: + def check_environ(self, environ: WSGIEnvironment) -> None: if type(environ) is not dict: warn( "WSGI environment is not a standard Python dict.", @@ -280,11 +282,9 @@ class LintMiddleware: def check_start_response( self, status: str, - headers: t.List[t.Tuple[str, str]], - exc_info: t.Optional[ - t.Tuple[t.Type[BaseException], BaseException, TracebackType] - ], - ) -> t.Tuple[int, Headers]: + headers: list[tuple[str, str]], + exc_info: None | (tuple[type[BaseException], BaseException, TracebackType]), + ) -> tuple[int, Headers]: check_type("status", status, str) status_code_str = status.split(None, 1)[0] @@ -359,9 +359,9 @@ class LintMiddleware: ) def check_iterator(self, app_iter: t.Iterable[bytes]) -> None: - if isinstance(app_iter, bytes): + if isinstance(app_iter, str): warn( - "The application returned a bytestring. The response will send one" + "The application returned a string. The response will send one" " character at a time to the client, which will kill performance." " Return a list or iterable instead.", WSGIWarning, @@ -377,8 +377,8 @@ class LintMiddleware: "A WSGI app does not take keyword arguments.", WSGIWarning, stacklevel=2 ) - environ: "WSGIEnvironment" = args[0] - start_response: "StartResponse" = args[1] + environ: WSGIEnvironment = args[0] + start_response: StartResponse = args[1] self.check_environ(environ) environ["wsgi.input"] = InputStream(environ["wsgi.input"]) @@ -388,8 +388,8 @@ class LintMiddleware: # iterate to the end and we can check the content length. environ["wsgi.file_wrapper"] = FileWrapper - headers_set: t.List[t.Any] = [] - chunks: t.List[int] = [] + headers_set: list[t.Any] = [] + chunks: list[int] = [] def checking_start_response( *args: t.Any, **kwargs: t.Any @@ -405,10 +405,10 @@ class LintMiddleware: warn("'start_response' does not take keyword arguments.", WSGIWarning) status: str = args[0] - headers: t.List[t.Tuple[str, str]] = args[1] - exc_info: t.Optional[ - t.Tuple[t.Type[BaseException], BaseException, TracebackType] - ] = (args[2] if len(args) == 3 else None) + headers: list[tuple[str, str]] = args[1] + exc_info: None | ( + tuple[type[BaseException], BaseException, TracebackType] + ) = (args[2] if len(args) == 3 else None) headers_set[:] = self.check_start_response(status, headers, exc_info) return GuardedWrite(start_response(status, headers, exc_info), chunks) diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py index f91e33b8257..2d806154c46 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/profiler.py @@ -11,6 +11,8 @@ that may be slowing down your application. :copyright: 2007 Pallets :license: BSD-3-Clause """ +from __future__ import annotations + import os.path import sys import time @@ -76,11 +78,11 @@ class ProfilerMiddleware: def __init__( self, - app: "WSGIApplication", - stream: t.IO[str] = sys.stdout, + app: WSGIApplication, + stream: t.IO[str] | None = sys.stdout, sort_by: t.Iterable[str] = ("time", "calls"), - restrictions: t.Iterable[t.Union[str, int, float]] = (), - profile_dir: t.Optional[str] = None, + restrictions: t.Iterable[str | int | float] = (), + profile_dir: str | None = None, filename_format: str = "{method}.{path}.{elapsed:.0f}ms.{time:.0f}.prof", ) -> None: self._app = app @@ -91,9 +93,9 @@ class ProfilerMiddleware: self._filename_format = filename_format def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: - response_body: t.List[bytes] = [] + response_body: list[bytes] = [] def catching_start_response(status, headers, exc_info=None): # type: ignore start_response(status, headers, exc_info) diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/proxy_fix.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/proxy_fix.py index 4cef7cc8050..8dfbb36c0b2 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/proxy_fix.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/proxy_fix.py @@ -21,6 +21,8 @@ setting each header so the middleware knows what to trust. :copyright: 2007 Pallets :license: BSD-3-Clause """ +from __future__ import annotations + import typing as t from ..http import parse_list_header @@ -64,23 +66,16 @@ class ProxyFix: app = ProxyFix(app, x_for=1, x_host=1) .. versionchanged:: 1.0 - Deprecated code has been removed: - - * The ``num_proxies`` argument and attribute. - * The ``get_remote_addr`` method. - * The environ keys ``orig_remote_addr``, - ``orig_wsgi_url_scheme``, and ``orig_http_host``. + The ``num_proxies`` argument and attribute; the ``get_remote_addr`` method; and + the environ keys ``orig_remote_addr``, ``orig_wsgi_url_scheme``, and + ``orig_http_host`` were removed. .. versionchanged:: 0.15 - All headers support multiple values. The ``num_proxies`` - argument is deprecated. Each header is configured with a - separate number of trusted proxies. + All headers support multiple values. Each header is configured with a separate + number of trusted proxies. .. versionchanged:: 0.15 - Original WSGI environ values are stored in the - ``werkzeug.proxy_fix.orig`` dict. ``orig_remote_addr``, - ``orig_wsgi_url_scheme``, and ``orig_http_host`` are deprecated - and will be removed in 1.0. + Original WSGI environ values are stored in the ``werkzeug.proxy_fix.orig`` dict. .. versionchanged:: 0.15 Support ``X-Forwarded-Port`` and ``X-Forwarded-Prefix``. @@ -92,7 +87,7 @@ class ProxyFix: def __init__( self, - app: "WSGIApplication", + app: WSGIApplication, x_for: int = 1, x_proto: int = 1, x_host: int = 0, @@ -106,7 +101,7 @@ class ProxyFix: self.x_port = x_port self.x_prefix = x_prefix - def _get_real_value(self, trusted: int, value: t.Optional[str]) -> t.Optional[str]: + def _get_real_value(self, trusted: int, value: str | None) -> str | None: """Get the real value from a list header based on the configured number of trusted proxies. @@ -128,7 +123,7 @@ class ProxyFix: return None def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: """Modify the WSGI environ based on the various ``Forwarded`` headers before calling the wrapped application. Store the diff --git a/contrib/python/Werkzeug/py3/werkzeug/middleware/shared_data.py b/contrib/python/Werkzeug/py3/werkzeug/middleware/shared_data.py index 2ec396c5331..e3ec7cab86f 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/middleware/shared_data.py +++ b/contrib/python/Werkzeug/py3/werkzeug/middleware/shared_data.py @@ -8,9 +8,11 @@ Serve Shared Static Files :copyright: 2007 Pallets :license: BSD-3-Clause """ +from __future__ import annotations + +import importlib.util import mimetypes import os -import pkgutil import posixpath import typing as t from datetime import datetime @@ -99,18 +101,18 @@ class SharedDataMiddleware: def __init__( self, - app: "WSGIApplication", - exports: t.Union[ - t.Dict[str, t.Union[str, t.Tuple[str, str]]], - t.Iterable[t.Tuple[str, t.Union[str, t.Tuple[str, str]]]], - ], + app: WSGIApplication, + exports: ( + dict[str, str | tuple[str, str]] + | t.Iterable[tuple[str, str | tuple[str, str]]] + ), disallow: None = None, cache: bool = True, cache_timeout: int = 60 * 60 * 12, fallback_mimetype: str = "application/octet-stream", ) -> None: self.app = app - self.exports: t.List[t.Tuple[str, _TLoader]] = [] + self.exports: list[tuple[str, _TLoader]] = [] self.cache = cache self.cache_timeout = cache_timeout @@ -156,12 +158,12 @@ class SharedDataMiddleware: def get_package_loader(self, package: str, package_path: str) -> _TLoader: load_time = datetime.now(timezone.utc) - provider = pkgutil.get_loader(package) - reader = provider.get_resource_reader(package) # type: ignore + spec = importlib.util.find_spec(package) + reader = spec.loader.get_resource_reader(package) # type: ignore[union-attr] def loader( - path: t.Optional[str], - ) -> t.Tuple[t.Optional[str], t.Optional[_TOpener]]: + path: str | None, + ) -> tuple[str | None, _TOpener | None]: if path is None: return None, None @@ -198,8 +200,8 @@ class SharedDataMiddleware: def get_directory_loader(self, directory: str) -> _TLoader: def loader( - path: t.Optional[str], - ) -> t.Tuple[t.Optional[str], t.Optional[_TOpener]]: + path: str | None, + ) -> tuple[str | None, _TOpener | None]: if path is not None: path = safe_join(directory, path) @@ -222,7 +224,7 @@ class SharedDataMiddleware: return f"wzsdm-{timestamp}-{file_size}-{checksum}" def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: path = get_path_info(environ) file_loader = None diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/converters.py b/contrib/python/Werkzeug/py3/werkzeug/routing/converters.py index bbad29d7ad4..c59e2abcb4c 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/converters.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/converters.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import re import typing as t import uuid - -from ..urls import _fast_url_quote +import warnings +from urllib.parse import quote if t.TYPE_CHECKING: from .map import Map @@ -15,13 +17,25 @@ class ValidationError(ValueError): class BaseConverter: - """Base class for all converters.""" + """Base class for all converters. + + .. versionchanged:: 2.3 + ``part_isolating`` defaults to ``False`` if ``regex`` contains a ``/``. + """ regex = "[^/]+" weight = 100 part_isolating = True - def __init__(self, map: "Map", *args: t.Any, **kwargs: t.Any) -> None: + def __init_subclass__(cls, **kwargs: t.Any) -> None: + super().__init_subclass__(**kwargs) + + # If the converter isn't inheriting its regex, disable part_isolating by default + # if the regex contains a / character. + if "regex" in cls.__dict__ and "part_isolating" not in cls.__dict__: + cls.part_isolating = "/" not in cls.regex + + def __init__(self, map: Map, *args: t.Any, **kwargs: t.Any) -> None: self.map = map def to_python(self, value: str) -> t.Any: @@ -29,8 +43,16 @@ class BaseConverter: def to_url(self, value: t.Any) -> str: if isinstance(value, (bytes, bytearray)): - return _fast_url_quote(value) - return _fast_url_quote(str(value).encode(self.map.charset)) + warnings.warn( + "Passing bytes as a URL value is deprecated and will not be supported" + " in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=7, + ) + return quote(value, safe="!$&'()*+,/:;=@") + + # safe = https://url.spec.whatwg.org/#url-path-segment-string + return quote(str(value), encoding=self.map.charset, safe="!$&'()*+,/:;=@") class UnicodeConverter(BaseConverter): @@ -51,14 +73,12 @@ class UnicodeConverter(BaseConverter): :param length: the exact length of the string. """ - part_isolating = True - def __init__( self, - map: "Map", + map: Map, minlength: int = 1, - maxlength: t.Optional[int] = None, - length: t.Optional[int] = None, + maxlength: int | None = None, + length: int | None = None, ) -> None: super().__init__(map) if length is not None: @@ -86,9 +106,7 @@ class AnyConverter(BaseConverter): Value is validated when building a URL. """ - part_isolating = True - - def __init__(self, map: "Map", *items: str) -> None: + def __init__(self, map: Map, *items: str) -> None: super().__init__(map) self.items = set(items) self.regex = f"(?:{'|'.join([re.escape(x) for x in items])})" @@ -113,7 +131,6 @@ class PathConverter(BaseConverter): regex = "[^/].*?" weight = 200 - part_isolating = False class NumberConverter(BaseConverter): @@ -124,14 +141,13 @@ class NumberConverter(BaseConverter): weight = 50 num_convert: t.Callable = int - part_isolating = True def __init__( self, - map: "Map", + map: Map, fixed_digits: int = 0, - min: t.Optional[int] = None, - max: t.Optional[int] = None, + min: int | None = None, + max: int | None = None, signed: bool = False, ) -> None: if signed: @@ -186,7 +202,6 @@ class IntegerConverter(NumberConverter): """ regex = r"\d+" - part_isolating = True class FloatConverter(NumberConverter): @@ -210,13 +225,12 @@ class FloatConverter(NumberConverter): regex = r"\d+\.\d+" num_convert = float - part_isolating = True def __init__( self, - map: "Map", - min: t.Optional[float] = None, - max: t.Optional[float] = None, + map: Map, + min: float | None = None, + max: float | None = None, signed: bool = False, ) -> None: super().__init__(map, min=min, max=max, signed=signed) # type: ignore @@ -236,7 +250,6 @@ class UUIDConverter(BaseConverter): r"[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-" r"[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}" ) - part_isolating = True def to_python(self, value: str) -> uuid.UUID: return uuid.UUID(value) @@ -246,7 +259,7 @@ class UUIDConverter(BaseConverter): #: the default converter mapping for the map. -DEFAULT_CONVERTERS: t.Mapping[str, t.Type[BaseConverter]] = { +DEFAULT_CONVERTERS: t.Mapping[str, type[BaseConverter]] = { "default": UnicodeConverter, "string": UnicodeConverter, "any": AnyConverter, diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/exceptions.py b/contrib/python/Werkzeug/py3/werkzeug/routing/exceptions.py index 7cbe6e91319..9d0a5281b8c 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/exceptions.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import difflib import typing as t @@ -9,7 +11,7 @@ from ..utils import redirect if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIEnvironment from .map import MapAdapter - from .rules import Rule # noqa: F401 + from .rules import Rule from ..wrappers.request import Request from ..wrappers.response import Response @@ -37,9 +39,9 @@ class RequestRedirect(HTTPException, RoutingException): def get_response( self, - environ: t.Optional[t.Union["WSGIEnvironment", "Request"]] = None, - scope: t.Optional[dict] = None, - ) -> "Response": + environ: WSGIEnvironment | Request | None = None, + scope: dict | None = None, + ) -> Response: return redirect(self.new_url, self.code) @@ -71,8 +73,8 @@ class BuildError(RoutingException, LookupError): self, endpoint: str, values: t.Mapping[str, t.Any], - method: t.Optional[str], - adapter: t.Optional["MapAdapter"] = None, + method: str | None, + adapter: MapAdapter | None = None, ) -> None: super().__init__(endpoint, values, method) self.endpoint = endpoint @@ -81,11 +83,11 @@ class BuildError(RoutingException, LookupError): self.adapter = adapter @cached_property - def suggested(self) -> t.Optional["Rule"]: + def suggested(self) -> Rule | None: return self.closest_rule(self.adapter) - def closest_rule(self, adapter: t.Optional["MapAdapter"]) -> t.Optional["Rule"]: - def _score_rule(rule: "Rule") -> float: + def closest_rule(self, adapter: MapAdapter | None) -> Rule | None: + def _score_rule(rule: Rule) -> float: return sum( [ 0.98 @@ -141,6 +143,6 @@ class WebsocketMismatch(BadRequest): class NoMatch(Exception): __slots__ = ("have_match_for", "websocket_mismatch") - def __init__(self, have_match_for: t.Set[str], websocket_mismatch: bool) -> None: + def __init__(self, have_match_for: set[str], websocket_mismatch: bool) -> None: self.have_match_for = have_match_for self.websocket_mismatch = websocket_mismatch diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/map.py b/contrib/python/Werkzeug/py3/werkzeug/routing/map.py index daf94b6a1c1..0d02bb8b72d 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/map.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/map.py @@ -1,12 +1,14 @@ -import posixpath +from __future__ import annotations + import typing as t import warnings from pprint import pformat from threading import Lock +from urllib.parse import quote +from urllib.parse import urljoin +from urllib.parse import urlunsplit -from .._internal import _encode_idna from .._internal import _get_environ -from .._internal import _to_str from .._internal import _wsgi_decoding_dance from ..datastructures import ImmutableDict from ..datastructures import MultiDict @@ -14,9 +16,7 @@ from ..exceptions import BadHost from ..exceptions import HTTPException from ..exceptions import MethodNotAllowed from ..exceptions import NotFound -from ..urls import url_encode -from ..urls import url_join -from ..urls import url_quote +from ..urls import _urlencode from ..wsgi import get_host from .converters import DEFAULT_CONVERTERS from .exceptions import BuildError @@ -30,7 +30,6 @@ from .rules import _simple_rule_re from .rules import Rule if t.TYPE_CHECKING: - import typing_extensions as te from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment from .converters import BaseConverter @@ -69,18 +68,21 @@ class Map: enabled the `host` parameter to rules is used instead of the `subdomain` one. + .. versionchanged:: 2.3 + The ``charset`` and ``encoding_errors`` parameters are deprecated and will be + removed in Werkzeug 3.0. + .. versionchanged:: 1.0 - If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules - will match. + If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match. .. versionchanged:: 1.0 - Added ``merge_slashes``. + The ``merge_slashes`` parameter was added. .. versionchanged:: 0.7 - Added ``encoding_errors`` and ``host_matching``. + The ``encoding_errors`` and ``host_matching`` parameters were added. .. versionchanged:: 0.5 - Added ``sort_parameters`` and ``sort_key``. + The ``sort_parameters`` and ``sort_key`` paramters were added. """ #: A dict of default converters to be used. @@ -93,25 +95,47 @@ class Map: def __init__( self, - rules: t.Optional[t.Iterable["RuleFactory"]] = None, + rules: t.Iterable[RuleFactory] | None = None, default_subdomain: str = "", - charset: str = "utf-8", + charset: str | None = None, strict_slashes: bool = True, merge_slashes: bool = True, redirect_defaults: bool = True, - converters: t.Optional[t.Mapping[str, t.Type["BaseConverter"]]] = None, + converters: t.Mapping[str, type[BaseConverter]] | None = None, sort_parameters: bool = False, - sort_key: t.Optional[t.Callable[[t.Any], t.Any]] = None, - encoding_errors: str = "replace", + sort_key: t.Callable[[t.Any], t.Any] | None = None, + encoding_errors: str | None = None, host_matching: bool = False, ) -> None: self._matcher = StateMachineMatcher(merge_slashes) - self._rules_by_endpoint: t.Dict[str, t.List[Rule]] = {} + self._rules_by_endpoint: dict[str, list[Rule]] = {} self._remap = True self._remap_lock = self.lock_class() self.default_subdomain = default_subdomain + + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + self.charset = charset + + if encoding_errors is not None: + warnings.warn( + "The 'encoding_errors' parameter is deprecated and will be" + " removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + encoding_errors = "replace" + self.encoding_errors = encoding_errors self.strict_slashes = strict_slashes self.merge_slashes = merge_slashes @@ -149,10 +173,10 @@ class Map: return False @property - def _rules(self) -> t.List[Rule]: + def _rules(self) -> list[Rule]: return [rule for rules in self._rules_by_endpoint.values() for rule in rules] - def iter_rules(self, endpoint: t.Optional[str] = None) -> t.Iterator[Rule]: + def iter_rules(self, endpoint: str | None = None) -> t.Iterator[Rule]: """Iterate over all rules or the rules of an endpoint. :param endpoint: if provided only the rules for that endpoint @@ -164,7 +188,7 @@ class Map: return iter(self._rules_by_endpoint[endpoint]) return iter(self._rules) - def add(self, rulefactory: "RuleFactory") -> None: + def add(self, rulefactory: RuleFactory) -> None: """Add a new rule or factory to the map and bind it. Requires that the rule is not bound to another map. @@ -180,13 +204,13 @@ class Map: def bind( self, server_name: str, - script_name: t.Optional[str] = None, - subdomain: t.Optional[str] = None, + script_name: str | None = None, + subdomain: str | None = None, url_scheme: str = "http", default_method: str = "GET", - path_info: t.Optional[str] = None, - query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, - ) -> "MapAdapter": + path_info: str | None = None, + query_args: t.Mapping[str, t.Any] | str | None = None, + ) -> MapAdapter: """Return a new :class:`MapAdapter` with the details specified to the call. Note that `script_name` will default to ``'/'`` if not further specified or `None`. The `server_name` at least is a requirement @@ -227,14 +251,17 @@ class Map: if path_info is None: path_info = "/" + # Port isn't part of IDNA, and might push a name over the 63 octet limit. + server_name, port_sep, port = server_name.partition(":") + try: - server_name = _encode_idna(server_name) # type: ignore + server_name = server_name.encode("idna").decode("ascii") except UnicodeError as e: raise BadHost() from e return MapAdapter( self, - server_name, + f"{server_name}{port_sep}{port}", script_name, subdomain, url_scheme, @@ -245,10 +272,10 @@ class Map: def bind_to_environ( self, - environ: t.Union["WSGIEnvironment", "Request"], - server_name: t.Optional[str] = None, - subdomain: t.Optional[str] = None, - ) -> "MapAdapter": + environ: WSGIEnvironment | Request, + server_name: str | None = None, + subdomain: str | None = None, + ) -> MapAdapter: """Like :meth:`bind` but you can pass it an WSGI environment and it will fetch the information from that dictionary. Note that because of limitations in the protocol there is no way to get the current @@ -332,7 +359,7 @@ class Map: else: subdomain = ".".join(filter(None, cur_server_name[:offset])) - def _get_wsgi_string(name: str) -> t.Optional[str]: + def _get_wsgi_string(name: str) -> str | None: val = env.get(name) if val is not None: return _wsgi_decoding_dance(val, self.charset) @@ -384,32 +411,33 @@ class MapAdapter: map: Map, server_name: str, script_name: str, - subdomain: t.Optional[str], + subdomain: str | None, url_scheme: str, path_info: str, default_method: str, - query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, + query_args: t.Mapping[str, t.Any] | str | None = None, ): self.map = map - self.server_name = _to_str(server_name) - script_name = _to_str(script_name) + self.server_name = server_name + if not script_name.endswith("/"): script_name += "/" + self.script_name = script_name - self.subdomain = _to_str(subdomain) - self.url_scheme = _to_str(url_scheme) - self.path_info = _to_str(path_info) - self.default_method = _to_str(default_method) + self.subdomain = subdomain + self.url_scheme = url_scheme + self.path_info = path_info + self.default_method = default_method self.query_args = query_args self.websocket = self.url_scheme in {"ws", "wss"} def dispatch( self, - view_func: t.Callable[[str, t.Mapping[str, t.Any]], "WSGIApplication"], - path_info: t.Optional[str] = None, - method: t.Optional[str] = None, + view_func: t.Callable[[str, t.Mapping[str, t.Any]], WSGIApplication], + path_info: str | None = None, + method: str | None = None, catch_http_exceptions: bool = False, - ) -> "WSGIApplication": + ) -> WSGIApplication: """Does the complete dispatching process. `view_func` is called with the endpoint and a dict with the values for the view. It should look up the view function, call it, and return a response object @@ -466,33 +494,33 @@ class MapAdapter: @t.overload def match( # type: ignore self, - path_info: t.Optional[str] = None, - method: t.Optional[str] = None, - return_rule: "te.Literal[False]" = False, - query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, - websocket: t.Optional[bool] = None, - ) -> t.Tuple[str, t.Mapping[str, t.Any]]: + path_info: str | None = None, + method: str | None = None, + return_rule: t.Literal[False] = False, + query_args: t.Mapping[str, t.Any] | str | None = None, + websocket: bool | None = None, + ) -> tuple[str, t.Mapping[str, t.Any]]: ... @t.overload def match( self, - path_info: t.Optional[str] = None, - method: t.Optional[str] = None, - return_rule: "te.Literal[True]" = True, - query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, - websocket: t.Optional[bool] = None, - ) -> t.Tuple[Rule, t.Mapping[str, t.Any]]: + path_info: str | None = None, + method: str | None = None, + return_rule: t.Literal[True] = True, + query_args: t.Mapping[str, t.Any] | str | None = None, + websocket: bool | None = None, + ) -> tuple[Rule, t.Mapping[str, t.Any]]: ... def match( self, - path_info: t.Optional[str] = None, - method: t.Optional[str] = None, + path_info: str | None = None, + method: str | None = None, return_rule: bool = False, - query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, - websocket: t.Optional[bool] = None, - ) -> t.Tuple[t.Union[str, Rule], t.Mapping[str, t.Any]]: + query_args: t.Mapping[str, t.Any] | str | None = None, + websocket: bool | None = None, + ) -> tuple[str | Rule, t.Mapping[str, t.Any]]: """The usage is simple: you just pass the match method the current path info as well as the method (which defaults to `GET`). The following things can then happen: @@ -583,8 +611,6 @@ class MapAdapter: self.map.update() if path_info is None: path_info = self.path_info - else: - path_info = _to_str(path_info, self.map.charset) if query_args is None: query_args = self.query_args or {} method = (method or self.default_method).upper() @@ -592,17 +618,22 @@ class MapAdapter: if websocket is None: websocket = self.websocket - domain_part = self.server_name if self.map.host_matching else self.subdomain + domain_part = self.server_name + + if not self.map.host_matching and self.subdomain is not None: + domain_part = self.subdomain + path_part = f"/{path_info.lstrip('/')}" if path_info else "" try: result = self.map._matcher.match(domain_part, path_part, method, websocket) except RequestPath as e: + # safe = https://url.spec.whatwg.org/#url-path-segment-string + new_path = quote( + e.path_info, safe="!$&'()*+,/:;=@", encoding=self.map.charset + ) raise RequestRedirect( - self.make_redirect_url( - url_quote(e.path_info, self.map.charset, safe="/:|+"), - query_args, - ) + self.make_redirect_url(new_path, query_args) ) from None except RequestAliasRedirect as e: raise RequestRedirect( @@ -647,7 +678,7 @@ class MapAdapter: netloc = self.server_name raise RequestRedirect( - url_join( + urljoin( f"{self.url_scheme or 'http'}://{netloc}{self.script_name}", redirect_url, ) @@ -658,9 +689,7 @@ class MapAdapter: else: return rule.endpoint, rv - def test( - self, path_info: t.Optional[str] = None, method: t.Optional[str] = None - ) -> bool: + def test(self, path_info: str | None = None, method: str | None = None) -> bool: """Test if a rule would match. Works like `match` but returns `True` if the URL matches, or `False` if it does not exist. @@ -677,7 +706,7 @@ class MapAdapter: return False return True - def allowed_methods(self, path_info: t.Optional[str] = None) -> t.Iterable[str]: + def allowed_methods(self, path_info: str | None = None) -> t.Iterable[str]: """Returns the valid methods that match for a given path. .. versionadded:: 0.7 @@ -690,7 +719,7 @@ class MapAdapter: pass return [] - def get_host(self, domain_part: t.Optional[str]) -> str: + def get_host(self, domain_part: str | None) -> str: """Figures out the full host name for the given domain part. The domain part is a subdomain in case host matching is disabled or a full host name. @@ -698,12 +727,13 @@ class MapAdapter: if self.map.host_matching: if domain_part is None: return self.server_name - return _to_str(domain_part, "ascii") - subdomain = domain_part - if subdomain is None: + + return domain_part + + if domain_part is None: subdomain = self.subdomain else: - subdomain = _to_str(subdomain, "ascii") + subdomain = domain_part if subdomain: return f"{subdomain}.{self.server_name}" @@ -715,8 +745,8 @@ class MapAdapter: rule: Rule, method: str, values: t.MutableMapping[str, t.Any], - query_args: t.Union[t.Mapping[str, t.Any], str], - ) -> t.Optional[str]: + query_args: t.Mapping[str, t.Any] | str, + ) -> str | None: """A helper that returns the URL to redirect to if it finds one. This is used for default redirecting only. @@ -735,30 +765,33 @@ class MapAdapter: return self.make_redirect_url(path, query_args, domain_part=domain_part) return None - def encode_query_args(self, query_args: t.Union[t.Mapping[str, t.Any], str]) -> str: + def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str: if not isinstance(query_args, str): - return url_encode(query_args, self.map.charset) + return _urlencode(query_args, encoding=self.map.charset) return query_args def make_redirect_url( self, path_info: str, - query_args: t.Optional[t.Union[t.Mapping[str, t.Any], str]] = None, - domain_part: t.Optional[str] = None, + query_args: t.Mapping[str, t.Any] | str | None = None, + domain_part: str | None = None, ) -> str: """Creates a redirect URL. :internal: """ + if query_args is None: + query_args = self.query_args + if query_args: - suffix = f"?{self.encode_query_args(query_args)}" + query_str = self.encode_query_args(query_args) else: - suffix = "" + query_str = None scheme = self.url_scheme or "http" host = self.get_host(domain_part) - path = posixpath.join(self.script_name.strip("/"), path_info.lstrip("/")) - return f"{scheme}://{host}/{path}{suffix}" + path = "/".join((self.script_name.strip("/"), path_info.lstrip("/"))) + return urlunsplit((scheme, host, path, query_str, None)) def make_alias_redirect_url( self, @@ -766,7 +799,7 @@ class MapAdapter: endpoint: str, values: t.Mapping[str, t.Any], method: str, - query_args: t.Union[t.Mapping[str, t.Any], str], + query_args: t.Mapping[str, t.Any] | str, ) -> str: """Internally called to make an alias redirect URL.""" url = self.build( @@ -781,9 +814,9 @@ class MapAdapter: self, endpoint: str, values: t.Mapping[str, t.Any], - method: t.Optional[str], + method: str | None, append_unknown: bool, - ) -> t.Optional[t.Tuple[str, str, bool]]: + ) -> tuple[str, str, bool] | None: """Helper for :meth:`build`. Returns subdomain and path for the rule that accepts this endpoint, values and method. @@ -821,11 +854,11 @@ class MapAdapter: def build( self, endpoint: str, - values: t.Optional[t.Mapping[str, t.Any]] = None, - method: t.Optional[str] = None, + values: t.Mapping[str, t.Any] | None = None, + method: str | None = None, force_external: bool = False, append_unknown: bool = True, - url_scheme: t.Optional[str] = None, + url_scheme: str | None = None, ) -> str: """Building URLs works pretty much the other way round. Instead of `match` you call `build` and pass it the endpoint and a dict of diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py b/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py index 05370c3e073..0d1210a67da 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/matcher.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import typing as t from dataclasses import dataclass @@ -23,9 +25,9 @@ class State: possible *static* and *dynamic* transitions to the next state. """ - dynamic: t.List[t.Tuple[RulePart, "State"]] = field(default_factory=list) - rules: t.List[Rule] = field(default_factory=list) - static: t.Dict[str, "State"] = field(default_factory=dict) + dynamic: list[tuple[RulePart, State]] = field(default_factory=list) + rules: list[Rule] = field(default_factory=list) + static: dict[str, State] = field(default_factory=dict) class StateMachineMatcher: @@ -66,7 +68,7 @@ class StateMachineMatcher: def match( self, domain: str, path: str, method: str, websocket: bool - ) -> t.Tuple[Rule, t.MutableMapping[str, t.Any]]: + ) -> tuple[Rule, t.MutableMapping[str, t.Any]]: # To match to a rule we need to start at the root state and # try to follow the transitions until we find a match, or find # there is no transition to follow. @@ -75,8 +77,8 @@ class StateMachineMatcher: websocket_mismatch = False def _match( - state: State, parts: t.List[str], values: t.List[str] - ) -> t.Optional[t.Tuple[Rule, t.List[str]]]: + state: State, parts: list[str], values: list[str] + ) -> tuple[Rule, list[str]] | None: # This function is meant to be called recursively, and will attempt # to match the head part to the state's transitions. nonlocal have_match_for, websocket_mismatch @@ -127,13 +129,21 @@ class StateMachineMatcher: remaining = [] match = re.compile(test_part.content).match(target) if match is not None: - groups = list(match.groups()) if test_part.suffixed: # If a part_isolating=False part has a slash suffix, remove the # suffix from the match and check for the slash redirect next. - suffix = groups.pop() + suffix = match.groups()[-1] if suffix == "/": remaining = [""] + + converter_groups = sorted( + match.groupdict().items(), key=lambda entry: entry[0] + ) + groups = [ + value + for key, value in converter_groups + if key[:11] == "__werkzeug_" + ] rv = _match(new_state, remaining, values + groups) if rv is not None: return rv diff --git a/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py b/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py index 7b37890abde..904a0225847 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py +++ b/contrib/python/Werkzeug/py3/werkzeug/routing/rules.py @@ -1,13 +1,15 @@ +from __future__ import annotations + import ast import re import typing as t from dataclasses import dataclass from string import Template from types import CodeType +from urllib.parse import quote -from .._internal import _to_bytes -from ..urls import url_encode -from ..urls import url_quote +from ..datastructures import iter_multi_items +from ..urls import _urlencode from .converters import ValidationError if t.TYPE_CHECKING: @@ -17,9 +19,9 @@ if t.TYPE_CHECKING: class Weighting(t.NamedTuple): number_static_weights: int - static_weights: t.List[t.Tuple[int, int]] + static_weights: list[tuple[int, int]] number_argument_weights: int - argument_weights: t.List[int] + argument_weights: list[int] @dataclass @@ -43,16 +45,16 @@ class RulePart: _part_re = re.compile( r""" (?: - (?P<slash>\/) # a slash + (?P<slash>/) # a slash | - (?P<static>[^<\/]+) # static rule data + (?P<static>[^</]+) # static rule data | (?: < (?: (?P<converter>[a-zA-Z_][a-zA-Z0-9_]*) # converter name (?:\((?P<arguments>.*?)\))? # converter arguments - \: # variable delimiter + : # variable delimiter )? (?P<variable>[a-zA-Z_][a-zA-Z0-9_]*) # variable name > @@ -93,7 +95,7 @@ def _find(value: str, target: str, pos: int) -> int: return len(value) -def _pythonize(value: str) -> t.Union[None, bool, int, float, str]: +def _pythonize(value: str) -> None | bool | int | float | str: if value in _PYTHON_CONSTANTS: return _PYTHON_CONSTANTS[value] for convert in int, float: @@ -106,7 +108,7 @@ def _pythonize(value: str) -> t.Union[None, bool, int, float, str]: return str(value) -def parse_converter_args(argstr: str) -> t.Tuple[t.Tuple, t.Dict[str, t.Any]]: +def parse_converter_args(argstr: str) -> tuple[t.Tuple, dict[str, t.Any]]: argstr += "," args = [] kwargs = {} @@ -131,7 +133,7 @@ class RuleFactory: be added by subclassing `RuleFactory` and overriding `get_rules`. """ - def get_rules(self, map: "Map") -> t.Iterable["Rule"]: + def get_rules(self, map: Map) -> t.Iterable[Rule]: """Subclasses of `RuleFactory` have to override this method and return an iterable of rules.""" raise NotImplementedError() @@ -160,7 +162,7 @@ class Subdomain(RuleFactory): self.subdomain = subdomain self.rules = rules - def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + def get_rules(self, map: Map) -> t.Iterator[Rule]: for rulefactory in self.rules: for rule in rulefactory.get_rules(map): rule = rule.empty() @@ -186,7 +188,7 @@ class Submount(RuleFactory): self.path = path.rstrip("/") self.rules = rules - def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + def get_rules(self, map: Map) -> t.Iterator[Rule]: for rulefactory in self.rules: for rule in rulefactory.get_rules(map): rule = rule.empty() @@ -211,7 +213,7 @@ class EndpointPrefix(RuleFactory): self.prefix = prefix self.rules = rules - def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + def get_rules(self, map: Map) -> t.Iterator[Rule]: for rulefactory in self.rules: for rule in rulefactory.get_rules(map): rule = rule.empty() @@ -238,10 +240,10 @@ class RuleTemplate: replace the placeholders in all the string parameters. """ - def __init__(self, rules: t.Iterable["Rule"]) -> None: + def __init__(self, rules: t.Iterable[Rule]) -> None: self.rules = list(rules) - def __call__(self, *args: t.Any, **kwargs: t.Any) -> "RuleTemplateFactory": + def __call__(self, *args: t.Any, **kwargs: t.Any) -> RuleTemplateFactory: return RuleTemplateFactory(self.rules, dict(*args, **kwargs)) @@ -253,12 +255,12 @@ class RuleTemplateFactory(RuleFactory): """ def __init__( - self, rules: t.Iterable[RuleFactory], context: t.Dict[str, t.Any] + self, rules: t.Iterable[RuleFactory], context: dict[str, t.Any] ) -> None: self.rules = rules self.context = context - def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + def get_rules(self, map: Map) -> t.Iterator[Rule]: for rulefactory in self.rules: for rule in rulefactory.get_rules(map): new_defaults = subdomain = None @@ -439,25 +441,26 @@ class Rule(RuleFactory): def __init__( self, string: str, - defaults: t.Optional[t.Mapping[str, t.Any]] = None, - subdomain: t.Optional[str] = None, - methods: t.Optional[t.Iterable[str]] = None, + defaults: t.Mapping[str, t.Any] | None = None, + subdomain: str | None = None, + methods: t.Iterable[str] | None = None, build_only: bool = False, - endpoint: t.Optional[str] = None, - strict_slashes: t.Optional[bool] = None, - merge_slashes: t.Optional[bool] = None, - redirect_to: t.Optional[t.Union[str, t.Callable[..., str]]] = None, + endpoint: str | None = None, + strict_slashes: bool | None = None, + merge_slashes: bool | None = None, + redirect_to: str | t.Callable[..., str] | None = None, alias: bool = False, - host: t.Optional[str] = None, + host: str | None = None, websocket: bool = False, ) -> None: if not string.startswith("/"): - raise ValueError("urls must start with a leading slash") + raise ValueError(f"URL rule '{string}' must start with a slash.") + self.rule = string self.is_leaf = not string.endswith("/") self.is_branch = string.endswith("/") - self.map: "Map" = None # type: ignore + self.map: Map = None # type: ignore self.strict_slashes = strict_slashes self.merge_slashes = merge_slashes self.subdomain = subdomain @@ -490,11 +493,11 @@ class Rule(RuleFactory): else: self.arguments = set() - self._converters: t.Dict[str, "BaseConverter"] = {} - self._trace: t.List[t.Tuple[bool, str]] = [] - self._parts: t.List[RulePart] = [] + self._converters: dict[str, BaseConverter] = {} + self._trace: list[tuple[bool, str]] = [] + self._parts: list[RulePart] = [] - def empty(self) -> "Rule": + def empty(self) -> Rule: """ Return an unbound copy of this rule. @@ -531,7 +534,7 @@ class Rule(RuleFactory): host=self.host, ) - def get_rules(self, map: "Map") -> t.Iterator["Rule"]: + def get_rules(self, map: Map) -> t.Iterator[Rule]: yield self def refresh(self) -> None: @@ -542,7 +545,7 @@ class Rule(RuleFactory): """ self.bind(self.map, rebind=True) - def bind(self, map: "Map", rebind: bool = False) -> None: + def bind(self, map: Map, rebind: bool = False) -> None: """Bind the url to a map and create a regular expression based on the information from the rule itself and the defaults from the map. @@ -565,7 +568,7 @@ class Rule(RuleFactory): converter_name: str, args: t.Tuple, kwargs: t.Mapping[str, t.Any], - ) -> "BaseConverter": + ) -> BaseConverter: """Looks up the converter for the given parameter. .. versionadded:: 0.9 @@ -575,19 +578,20 @@ class Rule(RuleFactory): return self.map.converters[converter_name](self.map, *args, **kwargs) def _encode_query_vars(self, query_vars: t.Mapping[str, t.Any]) -> str: - return url_encode( - query_vars, - charset=self.map.charset, - sort=self.map.sort_parameters, - key=self.map.sort_key, - ) + items: t.Iterable[tuple[str, str]] = iter_multi_items(query_vars) + + if self.map.sort_parameters: + items = sorted(items, key=self.map.sort_key) + + return _urlencode(items, encoding=self.map.charset) def _parse_rule(self, rule: str) -> t.Iterable[RulePart]: content = "" static = True argument_weights = [] - static_weights: t.List[t.Tuple[int, int]] = [] + static_weights: list[tuple[int, int]] = [] final = False + convertor_number = 0 pos = 0 while pos < len(rule): @@ -614,7 +618,8 @@ class Rule(RuleFactory): self.arguments.add(data["variable"]) if not convobj.part_isolating: final = True - content += f"({convobj.regex})" + content += f"(?P<__werkzeug_{convertor_number}>{convobj.regex})" + convertor_number += 1 argument_weights.append(convobj.weight) self._trace.append((True, data["variable"])) @@ -643,6 +648,7 @@ class Rule(RuleFactory): argument_weights = [] static_weights = [] final = False + convertor_number = 0 pos = match.end() @@ -701,24 +707,24 @@ class Rule(RuleFactory): rule = re.sub("/{2,}?", "/", self.rule) self._parts.extend(self._parse_rule(rule)) - self._build: t.Callable[..., t.Tuple[str, str]] + self._build: t.Callable[..., tuple[str, str]] self._build = self._compile_builder(False).__get__(self, None) - self._build_unknown: t.Callable[..., t.Tuple[str, str]] + self._build_unknown: t.Callable[..., tuple[str, str]] self._build_unknown = self._compile_builder(True).__get__(self, None) @staticmethod - def _get_func_code(code: CodeType, name: str) -> t.Callable[..., t.Tuple[str, str]]: - globs: t.Dict[str, t.Any] = {} - locs: t.Dict[str, t.Any] = {} + def _get_func_code(code: CodeType, name: str) -> t.Callable[..., tuple[str, str]]: + globs: dict[str, t.Any] = {} + locs: dict[str, t.Any] = {} exec(code, globs, locs) return locs[name] # type: ignore def _compile_builder( self, append_unknown: bool = True - ) -> t.Callable[..., t.Tuple[str, str]]: + ) -> t.Callable[..., tuple[str, str]]: defaults = self.defaults or {} - dom_ops: t.List[t.Tuple[bool, str]] = [] - url_ops: t.List[t.Tuple[bool, str]] = [] + dom_ops: list[tuple[bool, str]] = [] + url_ops: list[tuple[bool, str]] = [] opl = dom_ops for is_dynamic, data in self._trace: @@ -732,8 +738,12 @@ class Rule(RuleFactory): data = self._converters[data].to_url(defaults[data]) opl.append((False, data)) elif not is_dynamic: + # safe = https://url.spec.whatwg.org/#url-path-segment-string opl.append( - (False, url_quote(_to_bytes(data, self.map.charset), safe="/:|+")) + ( + False, + quote(data, safe="!$&'()*+,/:;=@", encoding=self.map.charset), + ) ) else: opl.append((True, data)) @@ -743,17 +753,17 @@ class Rule(RuleFactory): ret.args = [ast.Name(str(elem), ast.Load())] # type: ignore # str for py2 return ret - def _parts(ops: t.List[t.Tuple[bool, str]]) -> t.List[ast.AST]: + def _parts(ops: list[tuple[bool, str]]) -> list[ast.AST]: parts = [ - _convert(elem) if is_dynamic else ast.Str(s=elem) + _convert(elem) if is_dynamic else ast.Constant(elem) for is_dynamic, elem in ops ] - parts = parts or [ast.Str("")] + parts = parts or [ast.Constant("")] # constant fold ret = [parts[0]] for p in parts[1:]: - if isinstance(p, ast.Str) and isinstance(ret[-1], ast.Str): - ret[-1] = ast.Str(ret[-1].s + p.s) + if isinstance(p, ast.Constant) and isinstance(ret[-1], ast.Constant): + ret[-1] = ast.Constant(ret[-1].value + p.value) else: ret.append(p) return ret @@ -766,7 +776,7 @@ class Rule(RuleFactory): body = [_IF_KWARGS_URL_ENCODE_AST] url_parts.extend(_URL_ENCODE_AST_NAMES) - def _join(parts: t.List[ast.AST]) -> ast.AST: + def _join(parts: list[ast.AST]) -> ast.AST: if len(parts) == 1: # shortcut return parts[0] return ast.JoinedStr(parts) @@ -789,11 +799,11 @@ class Rule(RuleFactory): func_ast.args.args.append(ast.arg(arg, None)) func_ast.args.kwarg = ast.arg(".kwargs", None) for _ in kargs: - func_ast.args.defaults.append(ast.Str("")) + func_ast.args.defaults.append(ast.Constant("")) func_ast.body = body - # use `ast.parse` instead of `ast.Module` for better portability - # Python 3.8 changes the signature of `ast.Module` + # Use `ast.parse` instead of `ast.Module` for better portability, since the + # signature of `ast.Module` can change. module = ast.parse("") module.body = [func_ast] @@ -804,18 +814,18 @@ class Rule(RuleFactory): if "lineno" in node._attributes: node.lineno = 1 if "end_lineno" in node._attributes: - node.end_lineno = node.lineno # type: ignore[attr-defined] + node.end_lineno = node.lineno if "col_offset" in node._attributes: node.col_offset = 0 if "end_col_offset" in node._attributes: - node.end_col_offset = node.col_offset # type: ignore[attr-defined] + node.end_col_offset = node.col_offset code = compile(module, "<werkzeug routing>", "exec") return self._get_func_code(code, func_ast.name) def build( self, values: t.Mapping[str, t.Any], append_unknown: bool = True - ) -> t.Optional[t.Tuple[str, str]]: + ) -> tuple[str, str] | None: """Assembles the relative url for that rule and the subdomain. If building doesn't work for some reasons `None` is returned. @@ -829,7 +839,7 @@ class Rule(RuleFactory): except ValidationError: return None - def provides_defaults_for(self, rule: "Rule") -> bool: + def provides_defaults_for(self, rule: Rule) -> bool: """Check if this rule has defaults for a given rule. :internal: @@ -843,7 +853,7 @@ class Rule(RuleFactory): ) def suitable_for( - self, values: t.Mapping[str, t.Any], method: t.Optional[str] = None + self, values: t.Mapping[str, t.Any], method: str | None = None ) -> bool: """Check if the dict of values has enough data for url generation. @@ -875,7 +885,7 @@ class Rule(RuleFactory): return True - def build_compare_key(self) -> t.Tuple[int, int, int]: + def build_compare_key(self) -> tuple[int, int, int]: """The build compare key for sorting. :internal: diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py index 6b227383284..21a61972038 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/http.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import re import typing as t +import warnings from datetime import datetime -from .._internal import _cookie_parse_impl from .._internal import _dt_as_utc -from .._internal import _to_str from ..http import generate_etag from ..http import parse_date from ..http import parse_etags @@ -15,14 +16,14 @@ _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') def is_resource_modified( - http_range: t.Optional[str] = None, - http_if_range: t.Optional[str] = None, - http_if_modified_since: t.Optional[str] = None, - http_if_none_match: t.Optional[str] = None, - http_if_match: t.Optional[str] = None, - etag: t.Optional[str] = None, - data: t.Optional[bytes] = None, - last_modified: t.Optional[t.Union[datetime, str]] = None, + http_range: str | None = None, + http_if_range: str | None = None, + http_if_modified_since: str | None = None, + http_if_none_match: str | None = None, + http_if_match: str | None = None, + etag: str | None = None, + data: bytes | None = None, + last_modified: datetime | str | None = None, ignore_if_range: bool = True, ) -> bool: """Convenience method for conditional requests. @@ -63,7 +64,7 @@ def is_resource_modified( if_range = parse_if_range_header(http_if_range) if if_range is not None and if_range.date is not None: - modified_since: t.Optional[datetime] = if_range.date + modified_since: datetime | None = if_range.date else: modified_since = parse_date(http_if_modified_since) @@ -94,12 +95,38 @@ def is_resource_modified( return not unmodified +_cookie_re = re.compile( + r""" + ([^=;]*) + (?:\s*=\s* + ( + "(?:[^\\"]|\\.)*" + | + .*? + ) + )? + \s*;\s* + """, + flags=re.ASCII | re.VERBOSE, +) +_cookie_unslash_re = re.compile(rb"\\([0-3][0-7]{2}|.)") + + +def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes: + v = m.group(1) + + if len(v) == 1: + return v + + return int(v, 8).to_bytes(1, "big") + + def parse_cookie( - cookie: t.Union[bytes, str, None] = "", - charset: str = "utf-8", - errors: str = "replace", - cls: t.Optional[t.Type["ds.MultiDict"]] = None, -) -> "ds.MultiDict[str, str]": + cookie: str | None = None, + charset: str | None = None, + errors: str | None = None, + cls: type[ds.MultiDict] | None = None, +) -> ds.MultiDict[str, str]: """Parse a cookie from a string. The same key can be provided multiple times, the values are stored @@ -108,28 +135,67 @@ def parse_cookie( :meth:`MultiDict.getlist`. :param cookie: The cookie header as a string. - :param charset: The charset for the cookie values. - :param errors: The error behavior for the charset decoding. :param cls: A dict-like class to store the parsed cookies in. Defaults to :class:`MultiDict`. + .. versionchanged:: 2.3 + Passing bytes, and the ``charset`` and ``errors`` parameters, are deprecated and + will be removed in Werkzeug 3.0. + .. versionadded:: 2.2 """ - # PEP 3333 sends headers through the environ as latin1 decoded - # strings. Encode strings back to bytes for parsing. - if isinstance(cookie, str): - cookie = cookie.encode("latin1", "replace") - if cls is None: cls = ds.MultiDict - def _parse_pairs() -> t.Iterator[t.Tuple[str, str]]: - for key, val in _cookie_parse_impl(cookie): # type: ignore - key_str = _to_str(key, charset, errors, allow_none_charset=True) - val_str = _to_str(val, charset, errors, allow_none_charset=True) - yield key_str, val_str + if isinstance(cookie, bytes): + warnings.warn( + "The 'cookie' parameter must be a string. Passing bytes is deprecated and" + " will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + cookie = cookie.decode() + + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be removed in Werkzeug 3.0", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + + if errors is not None: + warnings.warn( + "The 'errors' parameter is deprecated and will be removed in Werkzeug 3.0", + DeprecationWarning, + stacklevel=2, + ) + else: + errors = "replace" + + if not cookie: + return cls() + + cookie = f"{cookie};" + out = [] + + for ck, cv in _cookie_re.findall(cookie): + ck = ck.strip() + cv = cv.strip() + + if not ck: + continue + + if len(cv) >= 2 and cv[0] == cv[-1] == '"': + # Work with bytes here, since a UTF-8 character could be multiple bytes. + cv = _cookie_unslash_re.sub( + _cookie_unslash_replace, cv[1:-1].encode() + ).decode(charset, errors) + + out.append((ck, cv)) - return cls(_parse_pairs()) + return cls(out) # circular dependencies diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py index 2684e5dd644..fc873537877 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/multipart.py @@ -1,14 +1,11 @@ +from __future__ import annotations + import re +import typing as t from dataclasses import dataclass from enum import auto from enum import Enum -from typing import cast -from typing import List -from typing import Optional -from typing import Tuple -from .._internal import _to_bytes -from .._internal import _to_str from ..datastructures import Headers from ..exceptions import RequestEntityTooLarge from ..http import parse_options_header @@ -58,6 +55,7 @@ class State(Enum): PREAMBLE = auto() PART = auto() DATA = auto() + DATA_START = auto() EPILOGUE = auto() COMPLETE = auto() @@ -86,9 +84,9 @@ class MultipartDecoder: def __init__( self, boundary: bytes, - max_form_memory_size: Optional[int] = None, + max_form_memory_size: int | None = None, *, - max_parts: Optional[int] = None, + max_parts: int | None = None, ) -> None: self.buffer = bytearray() self.complete = False @@ -123,19 +121,19 @@ class MultipartDecoder: self._search_position = 0 self._parts_decoded = 0 - def last_newline(self) -> int: + def last_newline(self, data: bytes) -> int: try: - last_nl = self.buffer.rindex(b"\n") + last_nl = data.rindex(b"\n") except ValueError: - last_nl = len(self.buffer) + last_nl = len(data) try: - last_cr = self.buffer.rindex(b"\r") + last_cr = data.rindex(b"\r") except ValueError: - last_cr = len(self.buffer) + last_cr = len(data) return min(last_nl, last_cr) - def receive_data(self, data: Optional[bytes]) -> None: + def receive_data(self, data: bytes | None) -> None: if data is None: self.complete = True elif ( @@ -172,7 +170,11 @@ class MultipartDecoder: match = BLANK_LINE_RE.search(self.buffer, self._search_position) if match is not None: headers = self._parse_headers(self.buffer[: match.start()]) - del self.buffer[: match.end()] + # The final header ends with a single CRLF, however a + # blank line indicates the start of the + # body. Therefore the end is after the first CRLF. + headers_end = (match.start() + match.end()) // 2 + del self.buffer[:headers_end] if "content-disposition" not in headers: raise ValueError("Missing Content-Disposition header") @@ -180,7 +182,7 @@ class MultipartDecoder: disposition, extra = parse_options_header( headers["content-disposition"] ) - name = cast(str, extra.get("name")) + name = t.cast(str, extra.get("name")) filename = extra.get("filename") if filename is not None: event = File( @@ -193,7 +195,7 @@ class MultipartDecoder: headers=headers, name=name, ) - self.state = State.DATA + self.state = State.DATA_START self._search_position = 0 self._parts_decoded += 1 @@ -205,28 +207,15 @@ class MultipartDecoder: # safe buffer for part of the search target. self._search_position = max(0, len(self.buffer) - SEARCH_EXTRA_LENGTH) - elif self.state == State.DATA: - if self.buffer.find(b"--" + self.boundary) == -1: - # No complete boundary in the buffer, but there may be - # a partial boundary at the end. As the boundary - # starts with either a nl or cr find the earliest and - # return up to that as data. - data_length = del_index = self.last_newline() - more_data = True - else: - match = self.boundary_re.search(self.buffer) - if match is not None: - if match.group(1).startswith(b"--"): - self.state = State.EPILOGUE - else: - self.state = State.PART - data_length = match.start() - del_index = match.end() - else: - data_length = del_index = self.last_newline() - more_data = match is None + elif self.state == State.DATA_START: + data, del_index, more_data = self._parse_data(self.buffer, start=True) + del self.buffer[:del_index] + event = Data(data=data, more_data=more_data) + if more_data: + self.state = State.DATA - data = bytes(self.buffer[:data_length]) + elif self.state == State.DATA: + data, del_index, more_data = self._parse_data(self.buffer, start=False) del self.buffer[:del_index] if data or not more_data: event = Data(data=data, more_data=more_data) @@ -242,16 +231,56 @@ class MultipartDecoder: return event def _parse_headers(self, data: bytes) -> Headers: - headers: List[Tuple[str, str]] = [] + headers: list[tuple[str, str]] = [] # Merge the continued headers into one line data = HEADER_CONTINUATION_RE.sub(b" ", data) # Now there is one header per line for line in data.splitlines(): - if line.strip() != b"": - name, value = _to_str(line).strip().split(":", 1) + line = line.strip() + + if line != b"": + name, _, value = line.decode().partition(":") headers.append((name.strip(), value.strip())) return Headers(headers) + def _parse_data(self, data: bytes, *, start: bool) -> tuple[bytes, int, bool]: + # Body parts must start with CRLF (or CR or LF) + if start: + match = LINE_BREAK_RE.match(data) + data_start = t.cast(t.Match[bytes], match).end() + else: + data_start = 0 + + boundary = b"--" + self.boundary + + if self.buffer.find(boundary) == -1: + # No complete boundary in the buffer, but there may be + # a partial boundary at the end. As the boundary + # starts with either a nl or cr find the earliest and + # return up to that as data. + data_end = del_index = self.last_newline(data[data_start:]) + data_start + # If amount of data after last newline is far from + # possible length of partial boundary, we should + # assume that there is no partial boundary in the buffer + # and return all pending data. + if (len(data) - data_end) > len(b"\n" + boundary): + data_end = del_index = len(data) + more_data = True + else: + match = self.boundary_re.search(data) + if match is not None: + if match.group(1).startswith(b"--"): + self.state = State.EPILOGUE + else: + self.state = State.PART + data_end = match.start() + del_index = match.end() + else: + data_end = del_index = self.last_newline(data[data_start:]) + data_start + more_data = match is None + + return bytes(data[data_start:data_end]), del_index, more_data + class MultipartEncoder: def __init__(self, boundary: bytes) -> None: @@ -267,17 +296,22 @@ class MultipartEncoder: State.PART, State.DATA, }: - self.state = State.DATA data = b"\r\n--" + self.boundary + b"\r\n" - data += b'Content-Disposition: form-data; name="%s"' % _to_bytes(event.name) + data += b'Content-Disposition: form-data; name="%s"' % event.name.encode() if isinstance(event, File): - data += b'; filename="%s"' % _to_bytes(event.filename) + data += b'; filename="%s"' % event.filename.encode() data += b"\r\n" - for name, value in cast(Field, event).headers: + for name, value in t.cast(Field, event).headers: if name.lower() != "content-disposition": - data += _to_bytes(f"{name}: {value}\r\n") - data += b"\r\n" + data += f"{name}: {value}\r\n".encode() + self.state = State.DATA_START return data + elif isinstance(event, Data) and self.state == State.DATA_START: + self.state = State.DATA + if len(event.data) > 0: + return b"\r\n" + event.data + else: + return event.data elif isinstance(event, Data) and self.state == State.DATA: return event.data elif isinstance(event, Epilogue): diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/request.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/request.py index e100a1f27c5..f5368fc1e70 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/request.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/request.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import typing as t +import warnings from datetime import datetime +from urllib.parse import parse_qsl -from .._internal import _to_str from ..datastructures import Accept from ..datastructures import Authorization from ..datastructures import CharsetAccept @@ -17,7 +20,6 @@ from ..datastructures import MultiDict from ..datastructures import Range from ..datastructures import RequestCacheControl from ..http import parse_accept_header -from ..http import parse_authorization_header from ..http import parse_cache_control_header from ..http import parse_date from ..http import parse_etags @@ -26,11 +28,11 @@ from ..http import parse_list_header from ..http import parse_options_header from ..http import parse_range_header from ..http import parse_set_header -from ..urls import url_decode from ..user_agent import UserAgent from ..utils import cached_property from ..utils import header_property from .http import parse_cookie +from .utils import get_content_length from .utils import get_current_url from .utils import get_host @@ -60,11 +62,91 @@ class Request: .. versionadded:: 2.0 """ - #: The charset used to decode most data in the request. - charset = "utf-8" + _charset: str + + @property + def charset(self) -> str: + """The charset used to decode body, form, and cookie data. Defaults to UTF-8. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Request data must always be UTF-8. + """ + warnings.warn( + "The 'charset' attribute is deprecated and will not be used in Werkzeug" + " 2.4. Interpreting bytes as text in body, form, and cookie data will" + " always use UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + return self._charset + + @charset.setter + def charset(self, value: str) -> None: + warnings.warn( + "The 'charset' attribute is deprecated and will not be used in Werkzeug" + " 2.4. Interpreting bytes as text in body, form, and cookie data will" + " always use UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + self._charset = value + + _encoding_errors: str + + @property + def encoding_errors(self) -> str: + """How errors when decoding bytes are handled. Defaults to "replace". + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. + """ + warnings.warn( + "The 'encoding_errors' attribute is deprecated and will not be used in" + " Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + return self._encoding_errors + + @encoding_errors.setter + def encoding_errors(self, value: str) -> None: + warnings.warn( + "The 'encoding_errors' attribute is deprecated and will not be used in" + " Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + self._encoding_errors = value + + _url_charset: str + + @property + def url_charset(self) -> str: + """The charset to use when decoding percent-encoded bytes in :attr:`args`. + Defaults to the value of :attr:`charset`, which defaults to UTF-8. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Percent-encoded bytes must always be UTF-8. + + .. versionadded:: 0.6 + """ + warnings.warn( + "The 'url_charset' attribute is deprecated and will not be used in" + " Werkzeug 3.0. Percent-encoded bytes must always be UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + return self._url_charset - #: the error handling procedure for errors, defaults to 'replace' - encoding_errors = "replace" + @url_charset.setter + def url_charset(self, value: str) -> None: + warnings.warn( + "The 'url_charset' attribute is deprecated and will not be used in" + " Werkzeug 3.0. Percent-encoded bytes must always be UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + self._url_charset = value #: the class to use for `args` and `form`. The default is an #: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports @@ -75,7 +157,7 @@ class Request: #: possible to use mutable structures, but this is not recommended. #: #: .. versionadded:: 0.6 - parameter_storage_class: t.Type[MultiDict] = ImmutableMultiDict + parameter_storage_class: type[MultiDict] = ImmutableMultiDict #: The type to be used for dict values from the incoming WSGI #: environment. (For example for :attr:`cookies`.) By default an @@ -85,16 +167,16 @@ class Request: #: Changed to ``ImmutableMultiDict`` to support multiple values. #: #: .. versionadded:: 0.6 - dict_storage_class: t.Type[MultiDict] = ImmutableMultiDict + dict_storage_class: type[MultiDict] = ImmutableMultiDict #: the type to be used for list values from the incoming WSGI environment. #: By default an :class:`~werkzeug.datastructures.ImmutableList` is used #: (for example for :attr:`access_list`). #: #: .. versionadded:: 0.6 - list_storage_class: t.Type[t.List] = ImmutableList + list_storage_class: type[t.List] = ImmutableList - user_agent_class: t.Type[UserAgent] = UserAgent + user_agent_class: type[UserAgent] = UserAgent """The class used and returned by the :attr:`user_agent` property to parse the header. Defaults to :class:`~werkzeug.user_agent.UserAgent`, which does no parsing. An @@ -114,19 +196,53 @@ class Request: #: the application is being run behind one). #: #: .. versionadded:: 0.9 - trusted_hosts: t.Optional[t.List[str]] = None + trusted_hosts: list[str] | None = None def __init__( self, method: str, scheme: str, - server: t.Optional[t.Tuple[str, t.Optional[int]]], + server: tuple[str, int | None] | None, root_path: str, path: str, query_string: bytes, headers: Headers, - remote_addr: t.Optional[str], + remote_addr: str | None, ) -> None: + if not isinstance(type(self).charset, property): + warnings.warn( + "The 'charset' attribute is deprecated and will not be used in Werkzeug" + " 2.4. Interpreting bytes as text in body, form, and cookie data will" + " always use UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + self._charset = self.charset + else: + self._charset = "utf-8" + + if not isinstance(type(self).encoding_errors, property): + warnings.warn( + "The 'encoding_errors' attribute is deprecated and will not be used in" + " Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + self._encoding_errors = self.encoding_errors + else: + self._encoding_errors = "replace" + + if not isinstance(type(self).url_charset, property): + warnings.warn( + "The 'url_charset' attribute is deprecated and will not be used in" + " Werkzeug 3.0. Percent-encoded bytes must always be UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + self._url_charset = self.url_charset + else: + self._url_charset = self._charset + #: The method the request was made with, such as ``GET``. self.method = method.upper() #: The URL scheme of the protocol the request used, such as @@ -157,17 +273,8 @@ class Request: return f"<{type(self).__name__} {url!r} [{self.method}]>" - @property - def url_charset(self) -> str: - """The charset that is assumed for URLs. Defaults to the value - of :attr:`charset`. - - .. versionadded:: 0.6 - """ - return self.charset - @cached_property - def args(self) -> "MultiDict[str, str]": + def args(self) -> MultiDict[str, str]: """The parsed URL parameters (the part in the URL after the question mark). @@ -176,16 +283,21 @@ class Request: is returned from this function. This can be changed by setting :attr:`parameter_storage_class` to a different type. This might be necessary if the order of the form data is important. + + .. versionchanged:: 2.3 + Invalid bytes remain percent encoded. """ - return url_decode( - self.query_string, - self.url_charset, - errors=self.encoding_errors, - cls=self.parameter_storage_class, + return self.parameter_storage_class( + parse_qsl( + self.query_string.decode(), + keep_blank_values=True, + encoding=self._url_charset, + errors="werkzeug.url_quote", + ) ) @cached_property - def access_route(self) -> t.List[str]: + def access_route(self) -> list[str]: """If a forwarded header exists this is a list of all ip addresses from the client ip to the last proxy server. """ @@ -200,7 +312,7 @@ class Request: @cached_property def full_path(self) -> str: """Requested path, including the query string.""" - return f"{self.path}?{_to_str(self.query_string, self.url_charset)}" + return f"{self.path}?{self.query_string.decode()}" @property def is_xhr(self): @@ -266,14 +378,16 @@ class Request: ) @cached_property - def cookies(self) -> "ImmutableMultiDict[str, str]": + def cookies(self) -> ImmutableMultiDict[str, str]: """A :class:`dict` with the contents of all cookies transmitted with the request.""" wsgi_combined_cookie = ";".join(self.headers.getlist("Cookie")) + charset = self._charset if self._charset != "utf-8" else None + errors = self._encoding_errors if self._encoding_errors != "replace" else None return parse_cookie( # type: ignore wsgi_combined_cookie, - self.charset, - self.encoding_errors, + charset=charset, + errors=errors, cls=self.dict_storage_class, ) @@ -289,23 +403,16 @@ class Request: ) @cached_property - def content_length(self) -> t.Optional[int]: + def content_length(self) -> int | None: """The Content-Length entity-header field indicates the size of the entity-body in bytes or, in the case of the HEAD method, the size of the entity-body that would have been sent had the request been a GET. """ - if self.headers.get("Transfer-Encoding", "") == "chunked": - return None - - content_length = self.headers.get("Content-Length") - if content_length is not None: - try: - return max(0, int(content_length)) - except (ValueError, TypeError): - pass - - return None + return get_content_length( + http_content_length=self.headers.get("Content-Length"), + http_transfer_encoding=self.headers.get("Transfer-Encoding"), + ) content_encoding = header_property[str]( "Content-Encoding", @@ -380,7 +487,7 @@ class Request: return self._parsed_content_type[0].lower() @property - def mimetype_params(self) -> t.Dict[str, str]: + def mimetype_params(self) -> dict[str, str]: """The mimetype parameters as dict. For example if the content type is ``text/html; charset=utf-8`` the params would be ``{'charset': 'utf-8'}``. @@ -460,7 +567,7 @@ class Request: return parse_etags(self.headers.get("If-None-Match")) @cached_property - def if_modified_since(self) -> t.Optional[datetime]: + def if_modified_since(self) -> datetime | None: """The parsed `If-Modified-Since` header as a datetime object. .. versionchanged:: 2.0 @@ -469,7 +576,7 @@ class Request: return parse_date(self.headers.get("If-Modified-Since")) @cached_property - def if_unmodified_since(self) -> t.Optional[datetime]: + def if_unmodified_since(self) -> datetime | None: """The parsed `If-Unmodified-Since` header as a datetime object. .. versionchanged:: 2.0 @@ -489,7 +596,7 @@ class Request: return parse_if_range_header(self.headers.get("If-Range")) @cached_property - def range(self) -> t.Optional[Range]: + def range(self) -> Range | None: """The parsed `Range` header. .. versionadded:: 0.7 @@ -507,19 +614,24 @@ class Request: :class:`~werkzeug.user_agent.UserAgent` to provide parsing for the other properties or other extended data. - .. versionchanged:: 2.0 - The built in parser is deprecated and will be removed in - Werkzeug 2.1. A ``UserAgent`` subclass must be set to parse - data from the string. + .. versionchanged:: 2.1 + The built-in parser was removed. Set ``user_agent_class`` to a ``UserAgent`` + subclass to parse data from the string. """ return self.user_agent_class(self.headers.get("User-Agent", "")) # Authorization @cached_property - def authorization(self) -> t.Optional[Authorization]: - """The `Authorization` object in parsed form.""" - return parse_authorization_header(self.headers.get("Authorization")) + def authorization(self) -> Authorization | None: + """The ``Authorization`` header parsed into an :class:`.Authorization` object. + ``None`` if the header is not present. + + .. versionchanged:: 2.3 + :class:`Authorization` is no longer a ``dict``. The ``token`` attribute + was added for auth schemes that use a token instead of parameters. + """ + return Authorization.from_header(self.headers.get("Authorization")) # CORS diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/response.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/response.py index de0bec29677..e5c1df743de 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/response.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/response.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import typing as t +import warnings from datetime import datetime from datetime import timedelta from datetime import timezone from http import HTTPStatus -from .._internal import _to_str from ..datastructures import Headers from ..datastructures import HeaderSet from ..http import dump_cookie @@ -28,14 +30,13 @@ from werkzeug.http import parse_csp_header from werkzeug.http import parse_date from werkzeug.http import parse_options_header from werkzeug.http import parse_set_header -from werkzeug.http import parse_www_authenticate_header from werkzeug.http import quote_etag from werkzeug.http import unquote_etag from werkzeug.utils import header_property -def _set_property(name: str, doc: t.Optional[str] = None) -> property: - def fget(self: "Response") -> HeaderSet: +def _set_property(name: str, doc: str | None = None) -> property: + def fget(self: Response) -> HeaderSet: def on_update(header_set: HeaderSet) -> None: if not header_set and name in self.headers: del self.headers[name] @@ -45,10 +46,8 @@ def _set_property(name: str, doc: t.Optional[str] = None) -> property: return parse_set_header(self.headers.get(name), on_update) def fset( - self: "Response", - value: t.Optional[ - t.Union[str, t.Dict[str, t.Union[str, int]], t.Iterable[str]] - ], + self: Response, + value: None | (str | dict[str, str | int] | t.Iterable[str]), ) -> None: if not value: del self.headers[name] @@ -85,14 +84,38 @@ class Response: .. versionadded:: 2.0 """ - #: the charset of the response. - charset = "utf-8" + _charset: str + + @property + def charset(self) -> str: + """The charset used to encode body and cookie data. Defaults to UTF-8. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Response data must always be UTF-8. + """ + warnings.warn( + "The 'charset' attribute is deprecated and will not be used in Werkzeug" + " 2.4. Text in body and cookie data will always use UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + return self._charset + + @charset.setter + def charset(self, value: str) -> None: + warnings.warn( + "The 'charset' attribute is deprecated and will not be used in Werkzeug" + " 2.4. Text in body and cookie data will always use UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + self._charset = value #: the default status if none is provided. default_status = 200 #: the default mimetype if none is provided. - default_mimetype: t.Optional[str] = "text/plain" + default_mimetype: str | None = "text/plain" #: Warn if a cookie header exceeds this size. The default, 4093, should be #: safely `supported by most browsers <cookie_>`_. A cookie larger than @@ -109,16 +132,24 @@ class Response: def __init__( self, - status: t.Optional[t.Union[int, str, HTTPStatus]] = None, - headers: t.Optional[ - t.Union[ - t.Mapping[str, t.Union[str, int, t.Iterable[t.Union[str, int]]]], - t.Iterable[t.Tuple[str, t.Union[str, int]]], - ] - ] = None, - mimetype: t.Optional[str] = None, - content_type: t.Optional[str] = None, + status: int | str | HTTPStatus | None = None, + headers: t.Mapping[str, str | t.Iterable[str]] + | t.Iterable[tuple[str, str]] + | None = None, + mimetype: str | None = None, + content_type: str | None = None, ) -> None: + if not isinstance(type(self).charset, property): + warnings.warn( + "The 'charset' attribute is deprecated and will not be used in Werkzeug" + " 2.4. Text in body and cookie data will always use UTF-8.", + DeprecationWarning, + stacklevel=2, + ) + self._charset = self.charset + else: + self._charset = "utf-8" + if isinstance(headers, Headers): self.headers = headers elif not headers: @@ -130,7 +161,7 @@ class Response: if mimetype is None and "content-type" not in self.headers: mimetype = self.default_mimetype if mimetype is not None: - mimetype = get_content_type(mimetype, self.charset) + mimetype = get_content_type(mimetype, self._charset) content_type = mimetype if content_type is not None: self.headers["Content-Type"] = content_type @@ -156,30 +187,29 @@ class Response: return self._status @status.setter - def status(self, value: t.Union[str, int, HTTPStatus]) -> None: - if not isinstance(value, (str, bytes, int, HTTPStatus)): - raise TypeError("Invalid status argument") - + def status(self, value: str | int | HTTPStatus) -> None: self._status, self._status_code = self._clean_status(value) - def _clean_status(self, value: t.Union[str, int, HTTPStatus]) -> t.Tuple[str, int]: - if isinstance(value, HTTPStatus): - value = int(value) - status = _to_str(value, self.charset) - split_status = status.split(None, 1) + def _clean_status(self, value: str | int | HTTPStatus) -> tuple[str, int]: + if isinstance(value, (int, HTTPStatus)): + status_code = int(value) + else: + value = value.strip() + + if not value: + raise ValueError("Empty status argument") - if len(split_status) == 0: - raise ValueError("Empty status argument") + code_str, sep, _ = value.partition(" ") - try: - status_code = int(split_status[0]) - except ValueError: - # only message - return f"0 {status}", 0 + try: + status_code = int(code_str) + except ValueError: + # only message + return f"0 {value}", 0 - if len(split_status) > 1: - # code and message - return status, status_code + if sep: + # code and message + return value, status_code # only code, look up message try: @@ -193,13 +223,13 @@ class Response: self, key: str, value: str = "", - max_age: t.Optional[t.Union[timedelta, int]] = None, - expires: t.Optional[t.Union[str, datetime, int, float]] = None, - path: t.Optional[str] = "/", - domain: t.Optional[str] = None, + max_age: timedelta | int | None = None, + expires: str | datetime | int | float | None = None, + path: str | None = "/", + domain: str | None = None, secure: bool = False, httponly: bool = False, - samesite: t.Optional[str] = None, + samesite: str | None = None, ) -> None: """Sets a cookie. @@ -215,7 +245,7 @@ class Response: :param path: limits the cookie to a given path, per default it will span the whole domain. :param domain: if you want to set a cross-domain cookie. For example, - ``domain=".example.com"`` will set a cookie that is + ``domain="example.com"`` will set a cookie that is readable by the domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a cookie will only be readable by the domain that set it. @@ -225,6 +255,7 @@ class Response: :param samesite: Limit the scope of the cookie to only be attached to requests that are "same-site". """ + charset = self._charset if self._charset != "utf-8" else None self.headers.add( "Set-Cookie", dump_cookie( @@ -236,7 +267,7 @@ class Response: domain=domain, secure=secure, httponly=httponly, - charset=self.charset, + charset=charset, max_size=self.max_cookie_size, samesite=samesite, ), @@ -245,11 +276,11 @@ class Response: def delete_cookie( self, key: str, - path: str = "/", - domain: t.Optional[str] = None, + path: str | None = "/", + domain: str | None = None, secure: bool = False, httponly: bool = False, - samesite: t.Optional[str] = None, + samesite: str | None = None, ) -> None: """Delete a cookie. Fails silently if key doesn't exist. @@ -290,7 +321,7 @@ class Response: # Common Descriptors @property - def mimetype(self) -> t.Optional[str]: + def mimetype(self) -> str | None: """The mimetype (content type without charset etc.)""" ct = self.headers.get("content-type") @@ -301,10 +332,10 @@ class Response: @mimetype.setter def mimetype(self, value: str) -> None: - self.headers["Content-Type"] = get_content_type(value, self.charset) + self.headers["Content-Type"] = get_content_type(value, self._charset) @property - def mimetype_params(self) -> t.Dict[str, str]: + def mimetype_params(self) -> dict[str, str]: """The mimetype parameters as dict. For example if the content type is ``text/html; charset=utf-8`` the params would be ``{'charset': 'utf-8'}``. @@ -421,7 +452,7 @@ class Response: ) @property - def retry_after(self) -> t.Optional[datetime]: + def retry_after(self) -> datetime | None: """The Retry-After response-header field can be used with a 503 (Service Unavailable) response to indicate how long the service is expected to be unavailable to the requesting client. @@ -443,7 +474,7 @@ class Response: return datetime.now(timezone.utc) + timedelta(seconds=seconds) @retry_after.setter - def retry_after(self, value: t.Optional[t.Union[datetime, int, str]]) -> None: + def retry_after(self, value: datetime | int | str | None) -> None: if value is None: if "retry-after" in self.headers: del self.headers["retry-after"] @@ -501,7 +532,7 @@ class Response: """Set the etag, and override the old one if there was one.""" self.headers["ETag"] = quote_etag(etag, weak) - def get_etag(self) -> t.Union[t.Tuple[str, bool], t.Tuple[None, None]]: + def get_etag(self) -> tuple[str, bool] | tuple[None, None]: """Return a tuple in the form ``(etag, is_weak)``. If there is no ETag the return value is ``(None, None)``. """ @@ -542,7 +573,7 @@ class Response: return rv @content_range.setter - def content_range(self, value: t.Optional[t.Union[ContentRange, str]]) -> None: + def content_range(self, value: ContentRange | str | None) -> None: if not value: del self.headers["content-range"] elif isinstance(value, str): @@ -554,16 +585,70 @@ class Response: @property def www_authenticate(self) -> WWWAuthenticate: - """The ``WWW-Authenticate`` header in a parsed form.""" + """The ``WWW-Authenticate`` header parsed into a :class:`.WWWAuthenticate` + object. Modifying the object will modify the header value. + + This header is not set by default. To set this header, assign an instance of + :class:`.WWWAuthenticate` to this attribute. + + .. code-block:: python + + response.www_authenticate = WWWAuthenticate( + "basic", {"realm": "Authentication Required"} + ) + + Multiple values for this header can be sent to give the client multiple options. + Assign a list to set multiple headers. However, modifying the items in the list + will not automatically update the header values, and accessing this attribute + will only ever return the first value. + + To unset this header, assign ``None`` or use ``del``. + + .. versionchanged:: 2.3 + This attribute can be assigned to to set the header. A list can be assigned + to set multiple header values. Use ``del`` to unset the header. + + .. versionchanged:: 2.3 + :class:`WWWAuthenticate` is no longer a ``dict``. The ``token`` attribute + was added for auth challenges that use a token instead of parameters. + """ + value = WWWAuthenticate.from_header(self.headers.get("WWW-Authenticate")) + + if value is None: + value = WWWAuthenticate("basic") + + def on_update(value: WWWAuthenticate) -> None: + self.www_authenticate = value + + value._on_update = on_update + return value + + @www_authenticate.setter + def www_authenticate( + self, value: WWWAuthenticate | list[WWWAuthenticate] | None + ) -> None: + if not value: # None or empty list + del self.www_authenticate + elif isinstance(value, list): + # Clear any existing header by setting the first item. + self.headers.set("WWW-Authenticate", value[0].to_header()) + + for item in value[1:]: + # Add additional header lines for additional items. + self.headers.add("WWW-Authenticate", item.to_header()) + else: + self.headers.set("WWW-Authenticate", value.to_header()) + + def on_update(value: WWWAuthenticate) -> None: + self.www_authenticate = value - def on_update(www_auth: WWWAuthenticate) -> None: - if not www_auth and "www-authenticate" in self.headers: - del self.headers["www-authenticate"] - elif www_auth: - self.headers["WWW-Authenticate"] = www_auth.to_header() + # When setting a single value, allow updating it directly. + value._on_update = on_update - header = self.headers.get("www-authenticate") - return parse_www_authenticate_header(header, on_update) + @www_authenticate.deleter + def www_authenticate(self) -> None: + if "WWW-Authenticate" in self.headers: + del self.headers["WWW-Authenticate"] # CSP @@ -590,7 +675,7 @@ class Response: @content_security_policy.setter def content_security_policy( - self, value: t.Optional[t.Union[ContentSecurityPolicy, str]] + self, value: ContentSecurityPolicy | str | None ) -> None: if not value: del self.headers["content-security-policy"] @@ -625,7 +710,7 @@ class Response: @content_security_policy_report_only.setter def content_security_policy_report_only( - self, value: t.Optional[t.Union[ContentSecurityPolicy, str]] + self, value: ContentSecurityPolicy | str | None ) -> None: if not value: del self.headers["content-security-policy-report-only"] @@ -645,7 +730,7 @@ class Response: return "Access-Control-Allow-Credentials" in self.headers @access_control_allow_credentials.setter - def access_control_allow_credentials(self, value: t.Optional[bool]) -> None: + def access_control_allow_credentials(self, value: bool | None) -> None: if value is True: self.headers["Access-Control-Allow-Credentials"] = "true" else: diff --git a/contrib/python/Werkzeug/py3/werkzeug/sansio/utils.py b/contrib/python/Werkzeug/py3/werkzeug/sansio/utils.py index e639dcb40f7..48ec1bfa077 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/sansio/utils.py +++ b/contrib/python/Werkzeug/py3/werkzeug/sansio/utils.py @@ -1,9 +1,11 @@ +from __future__ import annotations + import typing as t +from urllib.parse import quote -from .._internal import _encode_idna +from .._internal import _plain_int from ..exceptions import SecurityError from ..urls import uri_to_iri -from ..urls import url_quote def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool: @@ -18,20 +20,14 @@ def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool: if not hostname: return False - if isinstance(trusted_list, str): - trusted_list = [trusted_list] - - def _normalize(hostname: str) -> bytes: - if ":" in hostname: - hostname = hostname.rsplit(":", 1)[0] - - return _encode_idna(hostname) - try: - hostname_bytes = _normalize(hostname) - except UnicodeError: + hostname = hostname.partition(":")[0].encode("idna").decode("ascii") + except UnicodeEncodeError: return False + if isinstance(trusted_list, str): + trusted_list = [trusted_list] + for ref in trusted_list: if ref.startswith("."): ref = ref[1:] @@ -40,14 +36,11 @@ def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool: suffix_match = False try: - ref_bytes = _normalize(ref) - except UnicodeError: + ref = ref.partition(":")[0].encode("idna").decode("ascii") + except UnicodeEncodeError: return False - if ref_bytes == hostname_bytes: - return True - - if suffix_match and hostname_bytes.endswith(b"." + ref_bytes): + if ref == hostname or (suffix_match and hostname.endswith(f".{ref}")): return True return False @@ -55,9 +48,9 @@ def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool: def get_host( scheme: str, - host_header: t.Optional[str], - server: t.Optional[t.Tuple[str, t.Optional[int]]] = None, - trusted_hosts: t.Optional[t.Iterable[str]] = None, + host_header: str | None, + server: tuple[str, int | None] | None = None, + trusted_hosts: t.Iterable[str] | None = None, ) -> str: """Return the host for the given parameters. @@ -104,9 +97,9 @@ def get_host( def get_current_url( scheme: str, host: str, - root_path: t.Optional[str] = None, - path: t.Optional[str] = None, - query_string: t.Optional[bytes] = None, + root_path: str | None = None, + path: str | None = None, + query_string: bytes | None = None, ) -> str: """Recreate the URL for a request. If an optional part isn't provided, it and subsequent parts are not included in the URL. @@ -127,39 +120,40 @@ def get_current_url( url.append("/") return uri_to_iri("".join(url)) - url.append(url_quote(root_path.rstrip("/"))) + # safe = https://url.spec.whatwg.org/#url-path-segment-string + # as well as percent for things that are already quoted + url.append(quote(root_path.rstrip("/"), safe="!$&'()*+,/:;=@%")) url.append("/") if path is None: return uri_to_iri("".join(url)) - url.append(url_quote(path.lstrip("/"))) + url.append(quote(path.lstrip("/"), safe="!$&'()*+,/:;=@%")) if query_string: url.append("?") - url.append(url_quote(query_string, safe=":&%=+$!*'(),")) + url.append(quote(query_string, safe="!$&'()*+,/:;=?@%")) return uri_to_iri("".join(url)) def get_content_length( - http_content_length: t.Union[str, None] = None, - http_transfer_encoding: t.Union[str, None] = "", -) -> t.Optional[int]: - """Returns the content length as an integer or ``None`` if - unavailable or chunked transfer encoding is used. + http_content_length: str | None = None, + http_transfer_encoding: str | None = None, +) -> int | None: + """Return the ``Content-Length`` header value as an int. If the header is not given + or the ``Transfer-Encoding`` header is ``chunked``, ``None`` is returned to indicate + a streaming request. If the value is not an integer, or negative, 0 is returned. :param http_content_length: The Content-Length HTTP header. :param http_transfer_encoding: The Transfer-Encoding HTTP header. .. versionadded:: 2.2 """ - if http_transfer_encoding == "chunked": + if http_transfer_encoding == "chunked" or http_content_length is None: return None - if http_content_length is not None: - try: - return max(0, int(http_content_length)) - except (ValueError, TypeError): - pass - return None + try: + return max(0, _plain_int(http_content_length)) + except ValueError: + return 0 diff --git a/contrib/python/Werkzeug/py3/werkzeug/security.py b/contrib/python/Werkzeug/py3/werkzeug/security.py index 4599fb3e1d7..282c4fd8c35 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/security.py +++ b/contrib/python/Werkzeug/py3/werkzeug/security.py @@ -1,17 +1,16 @@ +from __future__ import annotations + import hashlib import hmac import os import posixpath import secrets -import typing as t - -if t.TYPE_CHECKING: - pass +import warnings SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -DEFAULT_PBKDF2_ITERATIONS = 260000 +DEFAULT_PBKDF2_ITERATIONS = 600000 -_os_alt_seps: t.List[str] = list( +_os_alt_seps: list[str] = list( sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/" ) @@ -19,95 +18,128 @@ _os_alt_seps: t.List[str] = list( def gen_salt(length: int) -> str: """Generate a random string of SALT_CHARS with specified ``length``.""" if length <= 0: - raise ValueError("Salt length must be positive") + raise ValueError("Salt length must be at least 1.") return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) -def _hash_internal(method: str, salt: str, password: str) -> t.Tuple[str, str]: - """Internal password hash helper. Supports plaintext without salt, - unsalted and salted passwords. In case salted passwords are used - hmac is used. - """ +def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]: if method == "plain": + warnings.warn( + "The 'plain' password method is deprecated and will be removed in" + " Werkzeug 3.0. Migrate to the 'scrypt' method.", + stacklevel=3, + ) return password, method + method, *args = method.split(":") salt = salt.encode("utf-8") password = password.encode("utf-8") - if method.startswith("pbkdf2:"): - if not salt: - raise ValueError("Salt is required for PBKDF2") + if method == "scrypt": + if not args: + n = 2**15 + r = 8 + p = 1 + else: + try: + n, r, p = map(int, args) + except ValueError: + raise ValueError("'scrypt' takes 3 arguments.") from None - args = method[7:].split(":") + maxmem = 132 * n * r * p # ideally 128, but some extra seems needed + return ( + hashlib.scrypt(password, salt=salt, n=n, r=r, p=p, maxmem=maxmem).hex(), + f"scrypt:{n}:{r}:{p}", + ) + elif method == "pbkdf2": + len_args = len(args) - if len(args) not in (1, 2): - raise ValueError("Invalid number of arguments for PBKDF2") + if len_args == 0: + hash_name = "sha256" + iterations = DEFAULT_PBKDF2_ITERATIONS + elif len_args == 1: + hash_name = args[0] + iterations = DEFAULT_PBKDF2_ITERATIONS + elif len_args == 2: + hash_name = args[0] + iterations = int(args[1]) + else: + raise ValueError("'pbkdf2' takes 2 arguments.") - method = args.pop(0) - iterations = int(args[0] or 0) if args else DEFAULT_PBKDF2_ITERATIONS return ( - hashlib.pbkdf2_hmac(method, password, salt, iterations).hex(), - f"pbkdf2:{method}:{iterations}", + hashlib.pbkdf2_hmac(hash_name, password, salt, iterations).hex(), + f"pbkdf2:{hash_name}:{iterations}", + ) + else: + warnings.warn( + f"The '{method}' password method is deprecated and will be removed in" + " Werkzeug 3.0. Migrate to the 'scrypt' method.", + stacklevel=3, ) - - if salt: return hmac.new(salt, password, method).hexdigest(), method - return hashlib.new(method, password).hexdigest(), method - def generate_password_hash( - password: str, method: str = "pbkdf2:sha256", salt_length: int = 16 + password: str, method: str = "pbkdf2", salt_length: int = 16 ) -> str: - """Hash a password with the given method and salt with a string of - the given length. The format of the string returned includes the method - that was used so that :func:`check_password_hash` can check the hash. + """Securely hash a password for storage. A password can be compared to a stored hash + using :func:`check_password_hash`. - The format for the hashed string looks like this:: + The following methods are supported: - method$salt$hash + - ``scrypt``, more secure but not available on PyPy. The parameters are ``n``, + ``r``, and ``p``, the default is ``scrypt:32768:8:1``. See + :func:`hashlib.scrypt`. + - ``pbkdf2``, the default. The parameters are ``hash_method`` and ``iterations``, + the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`. - This method can **not** generate unsalted passwords but it is possible - to set param method='plain' in order to enforce plaintext passwords. - If a salt is used, hmac is used internally to salt the password. + Default parameters may be updated to reflect current guidelines, and methods may be + deprecated and removed if they are no longer considered secure. To migrate old + hashes, you may generate a new hash when checking an old hash, or you may contact + users with a link to reset their password. - If PBKDF2 is wanted it can be enabled by setting the method to - ``pbkdf2:method:iterations`` where iterations is optional:: + :param password: The plaintext password. + :param method: The key derivation function and parameters. + :param salt_length: The number of characters to generate for the salt. - pbkdf2:sha256:80000$salt$hash - pbkdf2:sha256$salt$hash + .. versionchanged:: 2.3 + Scrypt support was added. - :param password: the password to hash. - :param method: the hash method to use (one that hashlib supports). Can - optionally be in the format ``pbkdf2:method:iterations`` - to enable PBKDF2. - :param salt_length: the length of the salt in letters. + .. versionchanged:: 2.3 + The default iterations for pbkdf2 was increased to 600,000. + + .. versionchanged:: 2.3 + All plain hashes are deprecated and will not be supported in Werkzeug 3.0. """ - salt = gen_salt(salt_length) if method != "plain" else "" + salt = gen_salt(salt_length) h, actual_method = _hash_internal(method, salt, password) return f"{actual_method}${salt}${h}" def check_password_hash(pwhash: str, password: str) -> bool: - """Check a password against a given salted and hashed password value. - In order to support unsalted legacy passwords this method supports - plain text passwords, md5 and sha1 hashes (both salted and unsalted). + """Securely check that the given stored password hash, previously generated using + :func:`generate_password_hash`, matches the given password. + + Methods may be deprecated and removed if they are no longer considered secure. To + migrate old hashes, you may generate a new hash when checking an old hash, or you + may contact users with a link to reset their password. - Returns `True` if the password matched, `False` otherwise. + :param pwhash: The hashed password. + :param password: The plaintext password. - :param pwhash: a hashed string like returned by - :func:`generate_password_hash`. - :param password: the plaintext password to compare against the hash. + .. versionchanged:: 2.3 + All plain hashes are deprecated and will not be supported in Werkzeug 3.0. """ - if pwhash.count("$") < 2: + try: + method, salt, hashval = pwhash.split("$", 2) + except ValueError: return False - method, salt, hashval = pwhash.split("$", 2) return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval) -def safe_join(directory: str, *pathnames: str) -> t.Optional[str]: +def safe_join(directory: str, *pathnames: str) -> str | None: """Safely join zero or more untrusted path components to a base directory to avoid escaping the base directory. diff --git a/contrib/python/Werkzeug/py3/werkzeug/serving.py b/contrib/python/Werkzeug/py3/werkzeug/serving.py index 2a2e74de2cf..c031dc45edc 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/serving.py +++ b/contrib/python/Werkzeug/py3/werkzeug/serving.py @@ -11,9 +11,12 @@ It provides features like interactive debugging and code reloading. Use from myapp import create_app from werkzeug import run_simple """ +from __future__ import annotations + import errno import io import os +import selectors import socket import socketserver import sys @@ -23,13 +26,13 @@ from datetime import timedelta from datetime import timezone from http.server import BaseHTTPRequestHandler from http.server import HTTPServer +from urllib.parse import unquote +from urllib.parse import urlsplit from ._internal import _log from ._internal import _wsgi_encoding_dance from .exceptions import InternalServerError from .urls import uri_to_iri -from .urls import url_parse -from .urls import url_unquote try: import ssl @@ -70,11 +73,10 @@ except AttributeError: LISTEN_QUEUE = 128 _TSSLContextArg = t.Optional[ - t.Union["ssl.SSLContext", t.Tuple[str, t.Optional[str]], "te.Literal['adhoc']"] + t.Union["ssl.SSLContext", t.Tuple[str, t.Optional[str]], t.Literal["adhoc"]] ] if t.TYPE_CHECKING: - import typing_extensions as te # noqa: F401 from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment from cryptography.hazmat.primitives.asymmetric.rsa import ( @@ -148,7 +150,7 @@ class DechunkedInput(io.RawIOBase): class WSGIRequestHandler(BaseHTTPRequestHandler): """A request handler that implements WSGI dispatching.""" - server: "BaseWSGIServer" + server: BaseWSGIServer @property def server_version(self) -> str: # type: ignore @@ -156,8 +158,8 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): return f"Werkzeug/{__version__}" - def make_environ(self) -> "WSGIEnvironment": - request_url = url_parse(self.path) + def make_environ(self) -> WSGIEnvironment: + request_url = urlsplit(self.path) url_scheme = "http" if self.server.ssl_context is None else "https" if not self.client_address: @@ -173,9 +175,9 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): else: path_info = request_url.path - path_info = url_unquote(path_info) + path_info = unquote(path_info) - environ: "WSGIEnvironment" = { + environ: WSGIEnvironment = { "wsgi.version": (1, 0), "wsgi.url_scheme": url_scheme, "wsgi.input": self.rfile, @@ -201,6 +203,9 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): } for key, value in self.headers.items(): + if "_" in key: + continue + key = key.upper().replace("-", "_") value = value.replace("\r\n", "") if key not in ("CONTENT_TYPE", "CONTENT_LENGTH"): @@ -239,10 +244,10 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): self.wfile.write(b"HTTP/1.1 100 Continue\r\n\r\n") self.environ = environ = self.make_environ() - status_set: t.Optional[str] = None - headers_set: t.Optional[t.List[t.Tuple[str, str]]] = None - status_sent: t.Optional[str] = None - headers_sent: t.Optional[t.List[t.Tuple[str, str]]] = None + status_set: str | None = None + headers_set: list[tuple[str, str]] | None = None + status_sent: str | None = None + headers_sent: list[tuple[str, str]] | None = None chunk_response: bool = False def write(data: bytes) -> None: @@ -316,7 +321,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): headers_set = headers return write - def execute(app: "WSGIApplication") -> None: + def execute(app: WSGIApplication) -> None: application_iter = app(environ, start_response) try: for data in application_iter: @@ -326,6 +331,32 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): if chunk_response: self.wfile.write(b"0\r\n\r\n") finally: + # Check for any remaining data in the read socket, and discard it. This + # will read past request.max_content_length, but lets the client see a + # 413 response instead of a connection reset failure. If we supported + # keep-alive connections, this naive approach would break by reading the + # next request line. Since we know that write (above) closes every + # connection we can read everything. + selector = selectors.DefaultSelector() + selector.register(self.connection, selectors.EVENT_READ) + total_size = 0 + total_reads = 0 + + # A timeout of 0 tends to fail because a client needs a small amount of + # time to continue sending its data. + while selector.select(timeout=0.01): + # Only read 10MB into memory at a time. + data = self.rfile.read(10_000_000) + total_size += len(data) + total_reads += 1 + + # Stop reading on no data, >=10GB, or 1000 reads. If a client sends + # more than that, they'll get a connection reset failure. + if not data or total_size >= 10_000_000_000 or total_reads > 1000: + break + + selector.close() + if hasattr(application_iter, "close"): application_iter.close() @@ -368,7 +399,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): raise def connection_dropped( - self, error: BaseException, environ: t.Optional["WSGIEnvironment"] = None + self, error: BaseException, environ: WSGIEnvironment | None = None ) -> None: """Called if the connection was closed by the client. By default nothing happens. @@ -394,9 +425,13 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): def port_integer(self) -> int: return self.client_address[1] - def log_request( - self, code: t.Union[int, str] = "-", size: t.Union[int, str] = "-" - ) -> None: + # Escape control characters. This is defined (but private) in Python 3.12. + _control_char_table = str.maketrans( + {c: rf"\x{c:02x}" for c in [*range(0x20), *range(0x7F, 0xA0)]} + ) + _control_char_table[ord("\\")] = r"\\" + + def log_request(self, code: int | str = "-", size: int | str = "-") -> None: try: path = uri_to_iri(self.path) msg = f"{self.command} {path} {self.request_version}" @@ -404,6 +439,8 @@ class WSGIRequestHandler(BaseHTTPRequestHandler): # path isn't set if the requestline was bad msg = self.requestline + # Escape control characters that may be in the decoded path. + msg = msg.translate(self._control_char_table) code = str(code) if code[0] == "1": # 1xx - Informational @@ -457,8 +494,8 @@ def _ansi_style(value: str, *styles: str) -> str: def generate_adhoc_ssl_pair( - cn: t.Optional[str] = None, -) -> t.Tuple["Certificate", "RSAPrivateKeyWithSerialization"]: + cn: str | None = None, +) -> tuple[Certificate, RSAPrivateKeyWithSerialization]: try: from cryptography import x509 from cryptography.x509.oid import NameOID @@ -503,8 +540,8 @@ def generate_adhoc_ssl_pair( def make_ssl_devcert( - base_path: str, host: t.Optional[str] = None, cn: t.Optional[str] = None -) -> t.Tuple[str, str]: + base_path: str, host: str | None = None, cn: str | None = None +) -> tuple[str, str]: """Creates an SSL key for development. This should be used instead of the ``'adhoc'`` key which generates a new cert on each server start. It accepts a path for where it should store the key and cert and @@ -546,7 +583,7 @@ def make_ssl_devcert( return cert_file, pkey_file -def generate_adhoc_ssl_context() -> "ssl.SSLContext": +def generate_adhoc_ssl_context() -> ssl.SSLContext: """Generates an adhoc SSL context for the development server.""" import tempfile import atexit @@ -577,8 +614,8 @@ def generate_adhoc_ssl_context() -> "ssl.SSLContext": def load_ssl_context( - cert_file: str, pkey_file: t.Optional[str] = None, protocol: t.Optional[int] = None -) -> "ssl.SSLContext": + cert_file: str, pkey_file: str | None = None, protocol: int | None = None +) -> ssl.SSLContext: """Loads SSL context from cert/private key files and optional protocol. Many parameters are directly taken from the API of :py:class:`ssl.SSLContext`. @@ -597,7 +634,7 @@ def load_ssl_context( return ctx -def is_ssl_error(error: t.Optional[Exception] = None) -> bool: +def is_ssl_error(error: Exception | None = None) -> bool: """Checks if the given error (or the current one) is an SSL error.""" if error is None: error = t.cast(Exception, sys.exc_info()[1]) @@ -616,11 +653,12 @@ def select_address_family(host: str, port: int) -> socket.AddressFamily: def get_sockaddr( host: str, port: int, family: socket.AddressFamily -) -> t.Union[t.Tuple[str, int], str]: +) -> tuple[str, int] | str: """Return a fully qualified socket address that can be passed to :func:`socket.bind`.""" if family == af_unix: - return host.split("://", 1)[1] + # Absolute path avoids IDNA encoding error when path starts with dot. + return os.path.abspath(host.partition("://")[2]) try: res = socket.getaddrinfo( host, port, family, socket.SOCK_STREAM, socket.IPPROTO_TCP @@ -663,11 +701,11 @@ class BaseWSGIServer(HTTPServer): self, host: str, port: int, - app: "WSGIApplication", - handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + app: WSGIApplication, + handler: type[WSGIRequestHandler] | None = None, passthrough_errors: bool = False, - ssl_context: t.Optional[_TSSLContextArg] = None, - fd: t.Optional[int] = None, + ssl_context: _TSSLContextArg | None = None, + fd: int | None = None, ) -> None: if handler is None: handler = WSGIRequestHandler @@ -754,7 +792,7 @@ class BaseWSGIServer(HTTPServer): ssl_context = generate_adhoc_ssl_context() self.socket = ssl_context.wrap_socket(self.socket, server_side=True) - self.ssl_context: t.Optional["ssl.SSLContext"] = ssl_context + self.ssl_context: ssl.SSLContext | None = ssl_context else: self.ssl_context = None @@ -770,7 +808,7 @@ class BaseWSGIServer(HTTPServer): self.server_close() def handle_error( - self, request: t.Any, client_address: t.Union[t.Tuple[str, int], str] + self, request: t.Any, client_address: tuple[str, int] | str ) -> None: if self.passthrough_errors: raise @@ -836,12 +874,12 @@ class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer): self, host: str, port: int, - app: "WSGIApplication", + app: WSGIApplication, processes: int = 40, - handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + handler: type[WSGIRequestHandler] | None = None, passthrough_errors: bool = False, - ssl_context: t.Optional[_TSSLContextArg] = None, - fd: t.Optional[int] = None, + ssl_context: _TSSLContextArg | None = None, + fd: int | None = None, ) -> None: if not can_fork: raise ValueError("Your platform does not support forking.") @@ -853,13 +891,13 @@ class ForkingWSGIServer(ForkingMixIn, BaseWSGIServer): def make_server( host: str, port: int, - app: "WSGIApplication", + app: WSGIApplication, threaded: bool = False, processes: int = 1, - request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None, + request_handler: type[WSGIRequestHandler] | None = None, passthrough_errors: bool = False, - ssl_context: t.Optional[_TSSLContextArg] = None, - fd: t.Optional[int] = None, + ssl_context: _TSSLContextArg | None = None, + fd: int | None = None, ) -> BaseWSGIServer: """Create an appropriate WSGI server instance based on the value of ``threaded`` and ``processes``. @@ -907,20 +945,20 @@ def is_running_from_reloader() -> bool: def run_simple( hostname: str, port: int, - application: "WSGIApplication", + application: WSGIApplication, use_reloader: bool = False, use_debugger: bool = False, use_evalex: bool = True, - extra_files: t.Optional[t.Iterable[str]] = None, - exclude_patterns: t.Optional[t.Iterable[str]] = None, + extra_files: t.Iterable[str] | None = None, + exclude_patterns: t.Iterable[str] | None = None, reloader_interval: int = 1, reloader_type: str = "auto", threaded: bool = False, processes: int = 1, - request_handler: t.Optional[t.Type[WSGIRequestHandler]] = None, - static_files: t.Optional[t.Dict[str, t.Union[str, t.Tuple[str, str]]]] = None, + request_handler: type[WSGIRequestHandler] | None = None, + static_files: dict[str, str | tuple[str, str]] | None = None, passthrough_errors: bool = False, - ssl_context: t.Optional[_TSSLContextArg] = None, + ssl_context: _TSSLContextArg | None = None, ) -> None: """Start a development server for a WSGI application. Various optional features can be enabled. @@ -968,7 +1006,7 @@ def run_simple( serve static files from using :class:`~werkzeug.middleware.SharedDataMiddleware`. :param passthrough_errors: Don't catch unhandled exceptions at the - server level, let the serve crash instead. If ``use_debugger`` + server level, let the server crash instead. If ``use_debugger`` is enabled, the debugger will still catch such errors. :param ssl_context: Configure TLS to serve over HTTPS. Can be an :class:`ssl.SSLContext` object, a ``(cert_file, key_file)`` diff --git a/contrib/python/Werkzeug/py3/werkzeug/test.py b/contrib/python/Werkzeug/py3/werkzeug/test.py index be67979b58d..4490c5c70e3 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/test.py +++ b/contrib/python/Werkzeug/py3/werkzeug/test.py @@ -1,16 +1,20 @@ +from __future__ import annotations + +import dataclasses import mimetypes import sys import typing as t +import warnings from collections import defaultdict from datetime import datetime -from datetime import timedelta -from http.cookiejar import CookieJar from io import BytesIO from itertools import chain from random import random from tempfile import TemporaryFile from time import time -from urllib.request import Request as _UrllibRequest +from urllib.parse import unquote +from urllib.parse import urlsplit +from urllib.parse import urlunsplit from ._internal import _get_environ from ._internal import _make_encode_wrapper @@ -25,6 +29,8 @@ from .datastructures import Headers from .datastructures import MultiDict from .http import dump_cookie from .http import dump_options_header +from .http import parse_cookie +from .http import parse_date from .http import parse_options_header from .sansio.multipart import Data from .sansio.multipart import Epilogue @@ -32,12 +38,8 @@ from .sansio.multipart import Field from .sansio.multipart import File from .sansio.multipart import MultipartEncoder from .sansio.multipart import Preamble +from .urls import _urlencode from .urls import iri_to_uri -from .urls import url_encode -from .urls import url_fix -from .urls import url_parse -from .urls import url_unparse -from .urls import url_unquote from .utils import cached_property from .utils import get_content_type from .wrappers.request import Request @@ -48,19 +50,32 @@ from .wsgi import get_current_url if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment + import typing_extensions as te def stream_encode_multipart( data: t.Mapping[str, t.Any], use_tempfile: bool = True, threshold: int = 1024 * 500, - boundary: t.Optional[str] = None, - charset: str = "utf-8", -) -> t.Tuple[t.IO[bytes], int, str]: + boundary: str | None = None, + charset: str | None = None, +) -> tuple[t.IO[bytes], int, str]: """Encode a dict of values (either strings or file descriptors or :class:`FileStorage` objects.) into a multipart encoded string stored in a file descriptor. + + .. versionchanged:: 2.3 + The ``charset`` parameter is deprecated and will be removed in Werkzeug 3.0 """ + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be removed in Werkzeug 3.0", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + if boundary is None: boundary = f"---------------WerkzeugFormPart_{time()}{random()}" @@ -121,6 +136,7 @@ def stream_encode_multipart( chunk = reader(16384) if not chunk: + write_binary(encoder.send_event(Data(data=chunk, more_data=False))) break write_binary(encoder.send_event(Data(data=chunk, more_data=True))) @@ -141,11 +157,14 @@ def stream_encode_multipart( def encode_multipart( values: t.Mapping[str, t.Any], - boundary: t.Optional[str] = None, - charset: str = "utf-8", -) -> t.Tuple[str, bytes]: + boundary: str | None = None, + charset: str | None = None, +) -> tuple[str, bytes]: """Like `stream_encode_multipart` but returns a tuple in the form (``boundary``, ``data``) where data is bytes. + + .. versionchanged:: 2.3 + The ``charset`` parameter is deprecated and will be removed in Werkzeug 3.0 """ stream, length, boundary = stream_encode_multipart( values, use_tempfile=False, boundary=boundary, charset=charset @@ -153,74 +172,7 @@ def encode_multipart( return boundary, stream.read() -class _TestCookieHeaders: - """A headers adapter for cookielib""" - - def __init__(self, headers: t.Union[Headers, t.List[t.Tuple[str, str]]]) -> None: - self.headers = headers - - def getheaders(self, name: str) -> t.Iterable[str]: - headers = [] - name = name.lower() - for k, v in self.headers: - if k.lower() == name: - headers.append(v) - return headers - - def get_all( - self, name: str, default: t.Optional[t.Iterable[str]] = None - ) -> t.Iterable[str]: - headers = self.getheaders(name) - - if not headers: - return default # type: ignore - - return headers - - -class _TestCookieResponse: - """Something that looks like a httplib.HTTPResponse, but is actually just an - adapter for our test responses to make them available for cookielib. - """ - - def __init__(self, headers: t.Union[Headers, t.List[t.Tuple[str, str]]]) -> None: - self.headers = _TestCookieHeaders(headers) - - def info(self) -> _TestCookieHeaders: - return self.headers - - -class _TestCookieJar(CookieJar): - """A cookielib.CookieJar modified to inject and read cookie headers from - and to wsgi environments, and wsgi application responses. - """ - - def inject_wsgi(self, environ: "WSGIEnvironment") -> None: - """Inject the cookies as client headers into the server's wsgi - environment. - """ - cvals = [f"{c.name}={c.value}" for c in self] - - if cvals: - environ["HTTP_COOKIE"] = "; ".join(cvals) - #else: - # environ.pop("HTTP_COOKIE", None) - - def extract_wsgi( - self, - environ: "WSGIEnvironment", - headers: t.Union[Headers, t.List[t.Tuple[str, str]]], - ) -> None: - """Extract the server's set-cookie headers as cookies into the - cookie jar. - """ - self.extract_cookies( - _TestCookieResponse(headers), # type: ignore - _UrllibRequest(get_current_url(environ)), - ) - - -def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[t.Tuple[str, t.Any]]: +def _iter_data(data: t.Mapping[str, t.Any]) -> t.Iterator[tuple[str, t.Any]]: """Iterate over a mapping that might have a list of values, yielding all key, value pairs. Almost like iter_multi_items but only allows lists, not tuples, of values so tuples can be used for files. @@ -303,11 +255,13 @@ class EnvironBuilder: Serialized with the function assigned to :attr:`json_dumps`. :param environ_base: an optional dict of environment defaults. :param environ_overrides: an optional dict of environment overrides. - :param charset: the charset used to encode string data. :param auth: An authorization object to use for the ``Authorization`` header value. A ``(username, password)`` tuple is a shortcut for ``Basic`` authorization. + .. versionchanged:: 2.3 + The ``charset`` parameter is deprecated and will be removed in Werkzeug 3.0 + .. versionchanged:: 2.1 ``CONTENT_TYPE`` and ``CONTENT_LENGTH`` are not duplicated as header keys in the environ. @@ -351,49 +305,60 @@ class EnvironBuilder: json_dumps = staticmethod(json.dumps) del json - _args: t.Optional[MultiDict] - _query_string: t.Optional[str] - _input_stream: t.Optional[t.IO[bytes]] - _form: t.Optional[MultiDict] - _files: t.Optional[FileMultiDict] + _args: MultiDict | None + _query_string: str | None + _input_stream: t.IO[bytes] | None + _form: MultiDict | None + _files: FileMultiDict | None def __init__( self, path: str = "/", - base_url: t.Optional[str] = None, - query_string: t.Optional[t.Union[t.Mapping[str, str], str]] = None, + base_url: str | None = None, + query_string: t.Mapping[str, str] | str | None = None, method: str = "GET", - input_stream: t.Optional[t.IO[bytes]] = None, - content_type: t.Optional[str] = None, - content_length: t.Optional[int] = None, - errors_stream: t.Optional[t.IO[str]] = None, + input_stream: t.IO[bytes] | None = None, + content_type: str | None = None, + content_length: int | None = None, + errors_stream: t.IO[str] | None = None, multithread: bool = False, multiprocess: bool = False, run_once: bool = False, - headers: t.Optional[t.Union[Headers, t.Iterable[t.Tuple[str, str]]]] = None, - data: t.Optional[ - t.Union[t.IO[bytes], str, bytes, t.Mapping[str, t.Any]] - ] = None, - environ_base: t.Optional[t.Mapping[str, t.Any]] = None, - environ_overrides: t.Optional[t.Mapping[str, t.Any]] = None, - charset: str = "utf-8", - mimetype: t.Optional[str] = None, - json: t.Optional[t.Mapping[str, t.Any]] = None, - auth: t.Optional[t.Union[Authorization, t.Tuple[str, str]]] = None, + headers: Headers | t.Iterable[tuple[str, str]] | None = None, + data: None | (t.IO[bytes] | str | bytes | t.Mapping[str, t.Any]) = None, + environ_base: t.Mapping[str, t.Any] | None = None, + environ_overrides: t.Mapping[str, t.Any] | None = None, + charset: str | None = None, + mimetype: str | None = None, + json: t.Mapping[str, t.Any] | None = None, + auth: Authorization | tuple[str, str] | None = None, ) -> None: path_s = _make_encode_wrapper(path) if query_string is not None and path_s("?") in path: raise ValueError("Query string is defined in the path and as an argument") - request_uri = url_parse(path) + request_uri = urlsplit(path) if query_string is None and path_s("?") in path: query_string = request_uri.query + + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be" + " removed in Werkzeug 3.0", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + self.charset = charset self.path = iri_to_uri(request_uri.path) self.request_uri = path if base_url is not None: - base_url = url_fix(iri_to_uri(base_url, charset), charset) + base_url = iri_to_uri( + base_url, charset=charset if charset != "utf-8" else None + ) self.base_url = base_url # type: ignore - if isinstance(query_string, (bytes, str)): + if isinstance(query_string, str): self.query_string = query_string else: if query_string is None: @@ -460,9 +425,7 @@ class EnvironBuilder: self.mimetype = mimetype @classmethod - def from_environ( - cls, environ: "WSGIEnvironment", **kwargs: t.Any - ) -> "EnvironBuilder": + def from_environ(cls, environ: WSGIEnvironment, **kwargs: t.Any) -> EnvironBuilder: """Turn an environ dict back into a builder. Any extra kwargs override the args extracted from the environ. @@ -497,9 +460,7 @@ class EnvironBuilder: def _add_file_from_data( self, key: str, - value: t.Union[ - t.IO[bytes], t.Tuple[t.IO[bytes], str], t.Tuple[t.IO[bytes], str, str] - ], + value: (t.IO[bytes] | tuple[t.IO[bytes], str] | tuple[t.IO[bytes], str, str]), ) -> None: """Called in the EnvironBuilder to add files from the data dict.""" if isinstance(value, tuple): @@ -509,7 +470,7 @@ class EnvironBuilder: @staticmethod def _make_base_url(scheme: str, host: str, script_root: str) -> str: - return url_unparse((scheme, host, script_root, "", "")).rstrip("/") + "/" + return urlunsplit((scheme, host, script_root, "", "")).rstrip("/") + "/" @property def base_url(self) -> str: @@ -519,13 +480,13 @@ class EnvironBuilder: return self._make_base_url(self.url_scheme, self.host, self.script_root) @base_url.setter - def base_url(self, value: t.Optional[str]) -> None: + def base_url(self, value: str | None) -> None: if value is None: scheme = "http" netloc = "localhost" script_root = "" else: - scheme, netloc, script_root, qs, anchor = url_parse(value) + scheme, netloc, script_root, qs, anchor = urlsplit(value) if qs or anchor: raise ValueError("base url must not contain a query string or fragment") self.script_root = script_root.rstrip("/") @@ -533,7 +494,7 @@ class EnvironBuilder: self.url_scheme = scheme @property - def content_type(self) -> t.Optional[str]: + def content_type(self) -> str | None: """The content type for the request. Reflected from and to the :attr:`headers`. Do not set if you set :attr:`files` or :attr:`form` for auto detection. @@ -548,14 +509,14 @@ class EnvironBuilder: return ct @content_type.setter - def content_type(self, value: t.Optional[str]) -> None: + def content_type(self, value: str | None) -> None: if value is None: self.headers.pop("Content-Type", None) else: self.headers["Content-Type"] = value @property - def mimetype(self) -> t.Optional[str]: + def mimetype(self) -> str | None: """The mimetype (content type without charset etc.) .. versionadded:: 0.14 @@ -583,7 +544,7 @@ class EnvironBuilder: return CallbackDict(d, on_update) @property - def content_length(self) -> t.Optional[int]: + def content_length(self) -> int | None: """The content length as integer. Reflected from and to the :attr:`headers`. Do not set if you set :attr:`files` or :attr:`form` for auto detection. @@ -591,13 +552,13 @@ class EnvironBuilder: return self.headers.get("Content-Length", type=int) @content_length.setter - def content_length(self, value: t.Optional[int]) -> None: + def content_length(self, value: int | None) -> None: if value is None: self.headers.pop("Content-Length", None) else: self.headers["Content-Length"] = str(value) - def _get_form(self, name: str, storage: t.Type[_TAnyMultiDict]) -> _TAnyMultiDict: + def _get_form(self, name: str, storage: type[_TAnyMultiDict]) -> _TAnyMultiDict: """Common behavior for getting the :attr:`form` and :attr:`files` properties. @@ -646,7 +607,7 @@ class EnvironBuilder: self._set_form("_files", value) @property - def input_stream(self) -> t.Optional[t.IO[bytes]]: + def input_stream(self) -> t.IO[bytes] | None: """An optional input stream. This is mutually exclusive with setting :attr:`form` and :attr:`files`, setting it will clear those. Do not provide this if the method is not ``POST`` or @@ -655,7 +616,7 @@ class EnvironBuilder: return self._input_stream @input_stream.setter - def input_stream(self, value: t.Optional[t.IO[bytes]]) -> None: + def input_stream(self, value: t.IO[bytes] | None) -> None: self._input_stream = value self._form = None self._files = None @@ -667,12 +628,12 @@ class EnvironBuilder: """ if self._query_string is None: if self._args is not None: - return url_encode(self._args, charset=self.charset) + return _urlencode(self._args, encoding=self.charset) return "" return self._query_string @query_string.setter - def query_string(self, value: t.Optional[str]) -> None: + def query_string(self, value: str | None) -> None: self._query_string = value self._args = None @@ -686,7 +647,7 @@ class EnvironBuilder: return self._args @args.setter - def args(self, value: t.Optional[MultiDict]) -> None: + def args(self, value: MultiDict | None) -> None: self._query_string = None self._args = value @@ -734,7 +695,7 @@ class EnvironBuilder: pass self.closed = True - def get_environ(self) -> "WSGIEnvironment": + def get_environ(self) -> WSGIEnvironment: """Return the built environ. .. versionchanged:: 0.15 @@ -755,23 +716,24 @@ class EnvironBuilder: input_stream.seek(start_pos) content_length = end_pos - start_pos elif mimetype == "multipart/form-data": + charset = self.charset if self.charset != "utf-8" else None input_stream, content_length, boundary = stream_encode_multipart( - CombinedMultiDict([self.form, self.files]), charset=self.charset + CombinedMultiDict([self.form, self.files]), charset=charset ) content_type = f'{mimetype}; boundary="{boundary}"' elif mimetype == "application/x-www-form-urlencoded": - form_encoded = url_encode(self.form, charset=self.charset).encode("ascii") + form_encoded = _urlencode(self.form, encoding=self.charset).encode("ascii") content_length = len(form_encoded) input_stream = BytesIO(form_encoded) else: input_stream = BytesIO() - result: "WSGIEnvironment" = {} + result: WSGIEnvironment = {} if self.environ_base: result.update(self.environ_base) def _path_encode(x: str) -> str: - return _wsgi_encoding_dance(url_unquote(x, self.charset), self.charset) + return _wsgi_encoding_dance(unquote(x, encoding=self.charset), self.charset) raw_uri = _wsgi_encoding_dance(self.request_uri, self.charset) result.update( @@ -822,7 +784,7 @@ class EnvironBuilder: return result - def get_request(self, cls: t.Optional[t.Type[Request]] = None) -> Request: + def get_request(self, cls: type[Request] | None = None) -> Request: """Returns a request with the data. If the request class is not specified :attr:`request_class` is used. @@ -841,24 +803,28 @@ class ClientRedirectError(Exception): class Client: - """This class allows you to send requests to a wrapped application. + """Simulate sending requests to a WSGI application without running a WSGI or HTTP + server. - The use_cookies parameter indicates whether cookies should be stored and - sent for subsequent requests. This is True by default, but passing False - will disable this behaviour. + :param application: The WSGI application to make requests to. + :param response_wrapper: A :class:`.Response` class to wrap response data with. + Defaults to :class:`.TestResponse`. If it's not a subclass of ``TestResponse``, + one will be created. + :param use_cookies: Persist cookies from ``Set-Cookie`` response headers to the + ``Cookie`` header in subsequent requests. Domain and path matching is supported, + but other cookie parameters are ignored. + :param allow_subdomain_redirects: Allow requests to follow redirects to subdomains. + Enable this if the application handles subdomains and redirects between them. - If you want to request some subdomain of your application you may set - `allow_subdomain_redirects` to `True` as if not no external redirects - are allowed. + .. versionchanged:: 2.3 + Simplify cookie implementation, support domain and path matching. .. versionchanged:: 2.1 - Removed deprecated behavior of treating the response as a - tuple. All data is available as properties on the returned - response object. + All data is available as properties on the returned response object. The + response cannot be returned as a tuple. .. versionchanged:: 2.0 - ``response_wrapper`` is always a subclass of - :class:``TestResponse``. + ``response_wrapper`` is always a subclass of :class:``TestResponse``. .. versionchanged:: 0.5 Added the ``use_cookies`` parameter. @@ -866,8 +832,8 @@ class Client: def __init__( self, - application: "WSGIApplication", - response_wrapper: t.Optional[t.Type["Response"]] = None, + application: WSGIApplication, + response_wrapper: type[Response] | None = None, use_cookies: bool = True, allow_subdomain_redirects: bool = False, ) -> None: @@ -885,96 +851,237 @@ class Client: self.response_wrapper = t.cast(t.Type["TestResponse"], response_wrapper) if use_cookies: - self.cookie_jar: t.Optional[_TestCookieJar] = _TestCookieJar() + self._cookies: dict[tuple[str, str, str], Cookie] | None = {} else: - self.cookie_jar = None + self._cookies = None self.allow_subdomain_redirects = allow_subdomain_redirects + @property + def cookie_jar(self) -> t.Iterable[Cookie] | None: + warnings.warn( + "The 'cookie_jar' attribute is a private API and will be removed in" + " Werkzeug 3.0. Use the 'get_cookie' method instead.", + DeprecationWarning, + stacklevel=2, + ) + + if self._cookies is None: + return None + + return self._cookies.values() + + def get_cookie( + self, key: str, domain: str = "localhost", path: str = "/" + ) -> Cookie | None: + """Return a :class:`.Cookie` if it exists. Cookies are uniquely identified by + ``(domain, path, key)``. + + :param key: The decoded form of the key for the cookie. + :param domain: The domain the cookie was set for. + :param path: The path the cookie was set for. + + .. versionadded:: 2.3 + """ + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + return self._cookies.get((domain, path, key)) + def set_cookie( self, - server_name: str, key: str, value: str = "", - max_age: t.Optional[t.Union[timedelta, int]] = None, - expires: t.Optional[t.Union[str, datetime, int, float]] = None, + *args: t.Any, + domain: str = "localhost", + origin_only: bool = True, path: str = "/", - domain: t.Optional[str] = None, - secure: bool = False, - httponly: bool = False, - samesite: t.Optional[str] = None, - charset: str = "utf-8", + **kwargs: t.Any, ) -> None: - """Sets a cookie in the client's cookie jar. The server name - is required and has to match the one that is also passed to - the open call. + """Set a cookie to be sent in subsequent requests. + + This is a convenience to skip making a test request to a route that would set + the cookie. To test the cookie, make a test request to a route that uses the + cookie value. + + The client uses ``domain``, ``origin_only``, and ``path`` to determine which + cookies to send with a request. It does not use other cookie parameters that + browsers use, since they're not applicable in tests. + + :param key: The key part of the cookie. + :param value: The value part of the cookie. + :param domain: Send this cookie with requests that match this domain. If + ``origin_only`` is true, it must be an exact match, otherwise it may be a + suffix match. + :param origin_only: Whether the domain must be an exact match to the request. + :param path: Send this cookie with requests that match this path either exactly + or as a prefix. + :param kwargs: Passed to :func:`.dump_cookie`. + + .. versionchanged:: 2.3 + The ``origin_only`` parameter was added. + + .. versionchanged:: 2.3 + The ``domain`` parameter defaults to ``localhost``. + + .. versionchanged:: 2.3 + The first parameter ``server_name`` is deprecated and will be removed in + Werkzeug 3.0. The first parameter is ``key``. Use the ``domain`` and + ``origin_only`` parameters instead. """ - assert self.cookie_jar is not None, "cookies disabled" - header = dump_cookie( - key, - value, - max_age, - expires, - path, - domain, - secure, - httponly, - charset, - samesite=samesite, + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + if args: + warnings.warn( + "The first parameter 'server_name' is no longer used, and will be" + " removed in Werkzeug 3.0. The positional parameters are 'key' and" + " 'value'. Use the 'domain' and 'origin_only' parameters instead.", + DeprecationWarning, + stacklevel=2, + ) + domain = key + key = value + value = args[0] + + cookie = Cookie._from_response_header( + domain, "/", dump_cookie(key, value, domain=domain, path=path, **kwargs) ) - environ = create_environ(path, base_url=f"http://{server_name}") - headers = [("Set-Cookie", header)] - self.cookie_jar.extract_wsgi(environ, headers) + cookie.origin_only = origin_only + + if cookie._should_delete: + self._cookies.pop(cookie._storage_key, None) + else: + self._cookies[cookie._storage_key] = cookie def delete_cookie( self, - server_name: str, key: str, + *args: t.Any, + domain: str = "localhost", path: str = "/", - domain: t.Optional[str] = None, - secure: bool = False, - httponly: bool = False, - samesite: t.Optional[str] = None, + **kwargs: t.Any, ) -> None: - """Deletes a cookie in the test client.""" - self.set_cookie( - server_name, - key, - expires=0, - max_age=0, - path=path, - domain=domain, - secure=secure, - httponly=httponly, - samesite=samesite, + """Delete a cookie if it exists. Cookies are uniquely identified by + ``(domain, path, key)``. + + :param key: The decoded form of the key for the cookie. + :param domain: The domain the cookie was set for. + :param path: The path the cookie was set for. + + .. versionchanged:: 2.3 + The ``domain`` parameter defaults to ``localhost``. + + .. versionchanged:: 2.3 + The first parameter ``server_name`` is deprecated and will be removed in + Werkzeug 3.0. The first parameter is ``key``. Use the ``domain`` parameter + instead. + + .. versionchanged:: 2.3 + The ``secure``, ``httponly`` and ``samesite`` parameters are deprecated and + will be removed in Werkzeug 2.4. + """ + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + if args: + warnings.warn( + "The first parameter 'server_name' is no longer used, and will be" + " removed in Werkzeug 2.4. The first parameter is 'key'. Use the" + " 'domain' parameter instead.", + DeprecationWarning, + stacklevel=2, + ) + domain = key + key = args[0] + + if kwargs: + kwargs_keys = ", ".join(f"'{k}'" for k in kwargs) + plural = "parameters are" if len(kwargs) > 1 else "parameter is" + warnings.warn( + f"The {kwargs_keys} {plural} deprecated and will be" + f" removed in Werkzeug 2.4.", + DeprecationWarning, + stacklevel=2, + ) + + self._cookies.pop((domain, path, key), None) + + def _add_cookies_to_wsgi(self, environ: WSGIEnvironment) -> None: + """If cookies are enabled, set the ``Cookie`` header in the environ to the + cookies that are applicable to the request host and path. + + :meta private: + + .. versionadded:: 2.3 + """ + if self._cookies is None: + return + + url = urlsplit(get_current_url(environ)) + server_name = url.hostname or "localhost" + value = "; ".join( + c._to_request_header() + for c in self._cookies.values() + if c._matches_request(server_name, url.path) ) - def run_wsgi_app( - self, environ: "WSGIEnvironment", buffered: bool = False - ) -> t.Tuple[t.Iterable[bytes], str, Headers]: - """Runs the wrapped WSGI app with the given environment. + if value: + environ["HTTP_COOKIE"] = value + #else: + # environ.pop("HTTP_COOKIE", None) + + def _update_cookies_from_response( + self, server_name: str, path: str, headers: list[str] + ) -> None: + """If cookies are enabled, update the stored cookies from any ``Set-Cookie`` + headers in the response. :meta private: + + .. versionadded:: 2.3 """ - if self.cookie_jar is not None: - self.cookie_jar.inject_wsgi(environ) + if self._cookies is None: + return - rv = run_wsgi_app(self.application, environ, buffered=buffered) + for header in headers: + cookie = Cookie._from_response_header(server_name, path, header) - if self.cookie_jar is not None: - self.cookie_jar.extract_wsgi(environ, rv[2]) + if cookie._should_delete: + self._cookies.pop(cookie._storage_key, None) + else: + self._cookies[cookie._storage_key] = cookie + def run_wsgi_app( + self, environ: WSGIEnvironment, buffered: bool = False + ) -> tuple[t.Iterable[bytes], str, Headers]: + """Runs the wrapped WSGI app with the given environment. + + :meta private: + """ + self._add_cookies_to_wsgi(environ) + rv = run_wsgi_app(self.application, environ, buffered=buffered) + url = urlsplit(get_current_url(environ)) + self._update_cookies_from_response( + url.hostname or "localhost", url.path, rv[2].getlist("Set-Cookie") + ) return rv def resolve_redirect( - self, response: "TestResponse", buffered: bool = False - ) -> "TestResponse": + self, response: TestResponse, buffered: bool = False + ) -> TestResponse: """Perform a new request to the location given by the redirect response to the previous request. :meta private: """ - scheme, netloc, path, qs, anchor = url_parse(response.location) + scheme, netloc, path, qs, anchor = urlsplit(response.location) builder = EnvironBuilder.from_environ( response.request.environ, path=path, query_string=qs ) @@ -1035,7 +1142,7 @@ class Client: buffered: bool = False, follow_redirects: bool = False, **kwargs: t.Any, - ) -> "TestResponse": + ) -> TestResponse: """Generate an environ dict from the given arguments, make a request to the application using it, and return the response. @@ -1054,11 +1161,6 @@ class Client: Removed the ``as_tuple`` parameter. .. versionchanged:: 2.0 - ``as_tuple`` is deprecated and will be removed in Werkzeug - 2.1. Use :attr:`TestResponse.request` and - ``request.environ`` instead. - - .. versionchanged:: 2.0 The request input stream is closed when calling ``response.close()``. Input streams for redirects are automatically closed. @@ -1072,7 +1174,7 @@ class Client: .. versionchanged:: 0.5 Added the ``follow_redirects`` parameter. """ - request: t.Optional["Request"] = None + request: Request | None = None if not kwargs and len(args) == 1: arg = args[0] @@ -1096,7 +1198,7 @@ class Client: response = self.response_wrapper(*response, request=request) redirects = set() - history: t.List["TestResponse"] = [] + history: list[TestResponse] = [] if not follow_redirects: return response @@ -1135,42 +1237,42 @@ class Client: response.call_on_close(request.input_stream.close) return response - def get(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def get(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``GET``.""" kw["method"] = "GET" return self.open(*args, **kw) - def post(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def post(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``POST``.""" kw["method"] = "POST" return self.open(*args, **kw) - def put(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def put(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``PUT``.""" kw["method"] = "PUT" return self.open(*args, **kw) - def delete(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def delete(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``DELETE``.""" kw["method"] = "DELETE" return self.open(*args, **kw) - def patch(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def patch(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``PATCH``.""" kw["method"] = "PATCH" return self.open(*args, **kw) - def options(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def options(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``OPTIONS``.""" kw["method"] = "OPTIONS" return self.open(*args, **kw) - def head(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def head(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``HEAD``.""" kw["method"] = "HEAD" return self.open(*args, **kw) - def trace(self, *args: t.Any, **kw: t.Any) -> "TestResponse": + def trace(self, *args: t.Any, **kw: t.Any) -> TestResponse: """Call :meth:`open` with ``method`` set to ``TRACE``.""" kw["method"] = "TRACE" return self.open(*args, **kw) @@ -1179,7 +1281,7 @@ class Client: return f"<{type(self).__name__} {self.application!r}>" -def create_environ(*args: t.Any, **kwargs: t.Any) -> "WSGIEnvironment": +def create_environ(*args: t.Any, **kwargs: t.Any) -> WSGIEnvironment: """Create a new WSGI environ dict based on the values passed. The first parameter should be the path of the request which defaults to '/'. The second one can either be an absolute path (in that case the host is @@ -1203,8 +1305,8 @@ def create_environ(*args: t.Any, **kwargs: t.Any) -> "WSGIEnvironment": def run_wsgi_app( - app: "WSGIApplication", environ: "WSGIEnvironment", buffered: bool = False -) -> t.Tuple[t.Iterable[bytes], str, Headers]: + app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False +) -> tuple[t.Iterable[bytes], str, Headers]: """Return a tuple in the form (app_iter, status, headers) of the application output. This works best if you pass it an application that returns an iterator all the time. @@ -1225,8 +1327,8 @@ def run_wsgi_app( # example) don't affect subsequent requests (such as redirects). environ = _get_environ(environ).copy() status: str - response: t.Optional[t.Tuple[str, t.List[t.Tuple[str, str]]]] = None - buffer: t.List[bytes] = [] + response: tuple[str, list[tuple[str, str]]] | None = None + buffer: list[bytes] = [] def start_response(status, headers, exc_info=None): # type: ignore nonlocal response @@ -1291,8 +1393,7 @@ class TestResponse(Response): assumed if missing. .. versionchanged:: 2.1 - Removed deprecated behavior for treating the response instance - as a tuple. + Response instances cannot be treated as tuples. .. versionadded:: 2.0 Test client methods always return instances of this class. @@ -1306,7 +1407,7 @@ class TestResponse(Response): resulted in this response. """ - history: t.Tuple["TestResponse", ...] + history: tuple[TestResponse, ...] """A list of intermediate responses. Populated when the test request is made with ``follow_redirects`` enabled. """ @@ -1320,7 +1421,7 @@ class TestResponse(Response): status: str, headers: Headers, request: Request, - history: t.Tuple["TestResponse"] = (), # type: ignore + history: tuple[TestResponse] = (), # type: ignore **kwargs: t.Any, ) -> None: super().__init__(response, status, headers, **kwargs) @@ -1336,3 +1437,109 @@ class TestResponse(Response): .. versionadded:: 2.1 """ return self.get_data(as_text=True) + + +class Cookie: + """A cookie key, value, and parameters. + + The class itself is not a public API. Its attributes are documented for inspection + with :meth:`.Client.get_cookie` only. + + .. versionadded:: 2.3 + """ + + key: str + """The cookie key, encoded as a client would see it.""" + + value: str + """The cookie key, encoded as a client would see it.""" + + decoded_key: str + """The cookie key, decoded as the application would set and see it.""" + + decoded_value: str + """The cookie value, decoded as the application would set and see it.""" + + expires: datetime | None + """The time at which the cookie is no longer valid.""" + + max_age: int | None + """The number of seconds from when the cookie was set at which it is + no longer valid. + """ + + domain: str + """The domain that the cookie was set for, or the request domain if not set.""" + + origin_only: bool + """Whether the cookie will be sent for exact domain matches only. This is ``True`` + if the ``Domain`` parameter was not present. + """ + + path: str + """The path that the cookie was set for.""" + + secure: bool | None + """The ``Secure`` parameter.""" + + http_only: bool | None + """The ``HttpOnly`` parameter.""" + + same_site: str | None + """The ``SameSite`` parameter.""" + + def _matches_request(self, server_name: str, path: str) -> bool: + return ( + server_name == self.domain + or ( + not self.origin_only + and server_name.endswith(self.domain) + and server_name[: -len(self.domain)].endswith(".") + ) + ) and ( + path == self.path + or ( + path.startswith(self.path) + and path[len(self.path) - self.path.endswith("/") :].startswith("/") + ) + ) + + def _to_request_header(self) -> str: + return f"{self.key}={self.value}" + + @classmethod + def _from_response_header(cls, server_name: str, path: str, header: str) -> te.Self: + header, _, parameters_str = header.partition(";") + key, _, value = header.partition("=") + decoded_key, decoded_value = next(parse_cookie(header).items()) + params = {} + + for item in parameters_str.split(";"): + k, sep, v = item.partition("=") + params[k.strip().lower()] = v.strip() if sep else None + + return cls( + key=key.strip(), + value=value.strip(), + decoded_key=decoded_key, + decoded_value=decoded_value, + expires=parse_date(params.get("expires")), + max_age=int(params["max-age"] or 0) if "max-age" in params else None, + domain=params.get("domain") or server_name, + origin_only="domain" not in params, + path=params.get("path") or path.rpartition("/")[0] or "/", + secure="secure" in params, + http_only="httponly" in params, + same_site=params.get("samesite"), + ) + + @property + def _storage_key(self) -> tuple[str, str, str]: + return self.domain, self.path, self.decoded_key + + @property + def _should_delete(self) -> bool: + return self.max_age == 0 or ( + self.expires is not None and self.expires.timestamp() == 0 + ) diff --git a/contrib/python/Werkzeug/py3/werkzeug/testapp.py b/contrib/python/Werkzeug/py3/werkzeug/testapp.py index 0d7ffbb187a..57f1f6fdf5e 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/testapp.py +++ b/contrib/python/Werkzeug/py3/werkzeug/testapp.py @@ -1,7 +1,8 @@ """A small application that can be used to test a WSGI server and check it for WSGI compliance. """ -import base64 +from __future__ import annotations + import os import sys import typing as t @@ -13,53 +14,6 @@ from . import __version__ as _werkzeug_version from .wrappers.request import Request from .wrappers.response import Response -if t.TYPE_CHECKING: - from _typeshed.wsgi import StartResponse - from _typeshed.wsgi import WSGIEnvironment - - -logo = Response( - base64.b64decode( - """ -R0lGODlhoACgAOMIAAEDACwpAEpCAGdgAJaKAM28AOnVAP3rAP///////// -//////////////////////yH5BAEKAAgALAAAAACgAKAAAAT+EMlJq704680R+F0ojmRpnuj0rWnrv -nB8rbRs33gu0bzu/0AObxgsGn3D5HHJbCUFyqZ0ukkSDlAidctNFg7gbI9LZlrBaHGtzAae0eloe25 -7w9EDOX2fst/xenyCIn5/gFqDiVVDV4aGeYiKkhSFjnCQY5OTlZaXgZp8nJ2ekaB0SQOjqphrpnOiq -ncEn65UsLGytLVmQ6m4sQazpbtLqL/HwpnER8bHyLrLOc3Oz8PRONPU1crXN9na263dMt/g4SzjMeX -m5yDpLqgG7OzJ4u8lT/P69ej3JPn69kHzN2OIAHkB9RUYSFCFQYQJFTIkCDBiwoXWGnowaLEjRm7+G -p9A7Hhx4rUkAUaSLJlxHMqVMD/aSycSZkyTplCqtGnRAM5NQ1Ly5OmzZc6gO4d6DGAUKA+hSocWYAo -SlM6oUWX2O/o0KdaVU5vuSQLAa0ADwQgMEMB2AIECZhVSnTno6spgbtXmHcBUrQACcc2FrTrWS8wAf -78cMFBgwIBgbN+qvTt3ayikRBk7BoyGAGABAdYyfdzRQGV3l4coxrqQ84GpUBmrdR3xNIDUPAKDBSA -ADIGDhhqTZIWaDcrVX8EsbNzbkvCOxG8bN5w8ly9H8jyTJHC6DFndQydbguh2e/ctZJFXRxMAqqPVA -tQH5E64SPr1f0zz7sQYjAHg0In+JQ11+N2B0XXBeeYZgBZFx4tqBToiTCPv0YBgQv8JqA6BEf6RhXx -w1ENhRBnWV8ctEX4Ul2zc3aVGcQNC2KElyTDYyYUWvShdjDyMOGMuFjqnII45aogPhz/CodUHFwaDx -lTgsaOjNyhGWJQd+lFoAGk8ObghI0kawg+EV5blH3dr+digkYuAGSaQZFHFz2P/cTaLmhF52QeSb45 -Jwxd+uSVGHlqOZpOeJpCFZ5J+rkAkFjQ0N1tah7JJSZUFNsrkeJUJMIBi8jyaEKIhKPomnC91Uo+NB -yyaJ5umnnpInIFh4t6ZSpGaAVmizqjpByDegYl8tPE0phCYrhcMWSv+uAqHfgH88ak5UXZmlKLVJhd -dj78s1Fxnzo6yUCrV6rrDOkluG+QzCAUTbCwf9SrmMLzK6p+OPHx7DF+bsfMRq7Ec61Av9i6GLw23r -idnZ+/OO0a99pbIrJkproCQMA17OPG6suq3cca5ruDfXCCDoS7BEdvmJn5otdqscn+uogRHHXs8cbh -EIfYaDY1AkrC0cqwcZpnM6ludx72x0p7Fo/hZAcpJDjax0UdHavMKAbiKltMWCF3xxh9k25N/Viud8 -ba78iCvUkt+V6BpwMlErmcgc502x+u1nSxJSJP9Mi52awD1V4yB/QHONsnU3L+A/zR4VL/indx/y64 -gqcj+qgTeweM86f0Qy1QVbvmWH1D9h+alqg254QD8HJXHvjQaGOqEqC22M54PcftZVKVSQG9jhkv7C -JyTyDoAJfPdu8v7DRZAxsP/ky9MJ3OL36DJfCFPASC3/aXlfLOOON9vGZZHydGf8LnxYJuuVIbl83y -Az5n/RPz07E+9+zw2A2ahz4HxHo9Kt79HTMx1Q7ma7zAzHgHqYH0SoZWyTuOLMiHwSfZDAQTn0ajk9 -YQqodnUYjByQZhZak9Wu4gYQsMyEpIOAOQKze8CmEF45KuAHTvIDOfHJNipwoHMuGHBnJElUoDmAyX -c2Qm/R8Ah/iILCCJOEokGowdhDYc/yoL+vpRGwyVSCWFYZNljkhEirGXsalWcAgOdeAdoXcktF2udb -qbUhjWyMQxYO01o6KYKOr6iK3fE4MaS+DsvBsGOBaMb0Y6IxADaJhFICaOLmiWTlDAnY1KzDG4ambL -cWBA8mUzjJsN2KjSaSXGqMCVXYpYkj33mcIApyhQf6YqgeNAmNvuC0t4CsDbSshZJkCS1eNisKqlyG -cF8G2JeiDX6tO6Mv0SmjCa3MFb0bJaGPMU0X7c8XcpvMaOQmCajwSeY9G0WqbBmKv34DsMIEztU6Y2 -KiDlFdt6jnCSqx7Dmt6XnqSKaFFHNO5+FmODxMCWBEaco77lNDGXBM0ECYB/+s7nKFdwSF5hgXumQe -EZ7amRg39RHy3zIjyRCykQh8Zo2iviRKyTDn/zx6EefptJj2Cw+Ep2FSc01U5ry4KLPYsTyWnVGnvb -UpyGlhjBUljyjHhWpf8OFaXwhp9O4T1gU9UeyPPa8A2l0p1kNqPXEVRm1AOs1oAGZU596t6SOR2mcB -Oco1srWtkaVrMUzIErrKri85keKqRQYX9VX0/eAUK1hrSu6HMEX3Qh2sCh0q0D2CtnUqS4hj62sE/z -aDs2Sg7MBS6xnQeooc2R2tC9YrKpEi9pLXfYXp20tDCpSP8rKlrD4axprb9u1Df5hSbz9QU0cRpfgn -kiIzwKucd0wsEHlLpe5yHXuc6FrNelOl7pY2+11kTWx7VpRu97dXA3DO1vbkhcb4zyvERYajQgAADs -=""" - ), - mimetype="image/png", -) - - TEMPLATE = """\ <!doctype html> <html lang=en> @@ -70,7 +24,6 @@ TEMPLATE = """\ body { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; background-color: white; color: #000; font-size: 15px; text-align: center; } - #logo { float: right; padding: 0 0 10px 10px; } div.box { text-align: left; width: 45em; margin: auto; padding: 50px 0; background-color: white; } h1, h2 { font-family: 'Ubuntu', 'Lucida Grande', 'Lucida Sans Unicode', @@ -92,7 +45,6 @@ TEMPLATE = """\ li.exp { background: white; } </style> <div class="box"> - <img src="?resource=logo" id="logo" alt="[The Werkzeug Logo]" /> <h1>WSGI Information</h1> <p> This page displays all available information about the WSGI server and @@ -139,7 +91,7 @@ TEMPLATE = """\ """ -def iter_sys_path() -> t.Iterator[t.Tuple[str, bool, bool]]: +def iter_sys_path() -> t.Iterator[tuple[str, bool, bool]]: if os.name == "posix": def strip(x: str) -> str: @@ -159,7 +111,21 @@ def iter_sys_path() -> t.Iterator[t.Tuple[str, bool, bool]]: yield strip(os.path.normpath(path)), not os.path.isdir(path), path != item -def render_testapp(req: Request) -> bytes: +def test_app(req: Request) -> Response: + """Simple test application that dumps the environment. You can use + it to check if Werkzeug is working properly: + + .. sourcecode:: pycon + + >>> from werkzeug.serving import run_simple + >>> from werkzeug.testapp import test_app + >>> run_simple('localhost', 3000, test_app) + * Running on http://localhost:3000/ + + The application displays important information from the WSGI environment, + the Python interpreter and the installed libraries. + """ try: import pkg_resources except ImportError: @@ -167,7 +133,7 @@ def render_testapp(req: Request) -> bytes: else: eggs = sorted( pkg_resources.working_set, - key=lambda x: x.project_name.lower(), # type: ignore + key=lambda x: x.project_name.lower(), ) python_eggs = [] for egg in eggs: @@ -195,44 +161,18 @@ def render_testapp(req: Request) -> bytes: class_ = f' class="{" ".join(class_)}"' if class_ else "" sys_path.append(f"<li{class_}>{escape(item)}") - return ( - TEMPLATE - % { - "python_version": "<br>".join(escape(sys.version).splitlines()), - "platform": escape(sys.platform), - "os": escape(os.name), - "api_version": sys.api_version, - "byteorder": sys.byteorder, - "werkzeug_version": _werkzeug_version, - "python_eggs": "\n".join(python_eggs), - "wsgi_env": "\n".join(wsgi_env), - "sys_path": "\n".join(sys_path), - } - ).encode("utf-8") - - -def test_app( - environ: "WSGIEnvironment", start_response: "StartResponse" -) -> t.Iterable[bytes]: - """Simple test application that dumps the environment. You can use - it to check if Werkzeug is working properly: - - .. sourcecode:: pycon - - >>> from werkzeug.serving import run_simple - >>> from werkzeug.testapp import test_app - >>> run_simple('localhost', 3000, test_app) - * Running on http://localhost:3000/ - - The application displays important information from the WSGI environment, - the Python interpreter and the installed libraries. - """ - req = Request(environ, populate_request=False) - if req.args.get("resource") == "logo": - response = logo - else: - response = Response(render_testapp(req), mimetype="text/html") - return response(environ, start_response) + context = { + "python_version": "<br>".join(escape(sys.version).splitlines()), + "platform": escape(sys.platform), + "os": escape(os.name), + "api_version": sys.api_version, + "byteorder": sys.byteorder, + "werkzeug_version": _werkzeug_version, + "python_eggs": "\n".join(python_eggs), + "wsgi_env": "\n".join(wsgi_env), + "sys_path": "\n".join(sys_path), + } + return Response(TEMPLATE % context, mimetype="text/html") if __name__ == "__main__": diff --git a/contrib/python/Werkzeug/py3/werkzeug/urls.py b/contrib/python/Werkzeug/py3/werkzeug/urls.py index 1cb9418d2fc..a65cd7ea6fa 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/urls.py +++ b/contrib/python/Werkzeug/py3/werkzeug/urls.py @@ -3,16 +3,24 @@ Contains implementations of functions from :mod:`urllib.parse` that handle bytes and strings. """ +from __future__ import annotations + import codecs import os import re import typing as t +import warnings +from urllib.parse import quote +from urllib.parse import unquote +from urllib.parse import urlencode +from urllib.parse import urlsplit +from urllib.parse import urlunsplit from ._internal import _check_str_tuple from ._internal import _decode_idna -from ._internal import _encode_idna from ._internal import _make_encode_wrapper from ._internal import _to_str +from .datastructures import iter_multi_items if t.TYPE_CHECKING: from . import datastructures as ds @@ -21,14 +29,13 @@ if t.TYPE_CHECKING: _scheme_re = re.compile(r"^[a-zA-Z0-9+-.]+$") # Characters that are safe in any part of an URL. -_always_safe = frozenset( - bytearray( - b"abcdefghijklmnopqrstuvwxyz" - b"ABCDEFGHIJKLMNOPQRSTUVWXYZ" - b"0123456789" - b"-._~" - ) +_always_safe_chars = ( + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "-._~" ) +_always_safe = frozenset(_always_safe_chars.encode("ascii")) _hexdigits = "0123456789ABCDEFabcdef" _hextobyte = { @@ -48,7 +55,11 @@ class _URLTuple(t.NamedTuple): class BaseURL(_URLTuple): - """Superclass of :py:class:`URL` and :py:class:`BytesURL`.""" + """Superclass of :py:class:`URL` and :py:class:`BytesURL`. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use the ``urllib.parse`` library instead. + """ __slots__ = () _at: str @@ -56,16 +67,25 @@ class BaseURL(_URLTuple): _lbracket: str _rbracket: str + def __new__(cls, *args: t.Any, **kwargs: t.Any) -> BaseURL: + warnings.warn( + f"'werkzeug.urls.{cls.__name__}' is deprecated and will be removed in" + " Werkzeug 3.0. Use the 'urllib.parse' library instead.", + DeprecationWarning, + stacklevel=2, + ) + return super().__new__(cls, *args, **kwargs) + def __str__(self) -> str: return self.to_url() - def replace(self, **kwargs: t.Any) -> "BaseURL": + def replace(self, **kwargs: t.Any) -> BaseURL: """Return an URL with the same values, except for those parameters given new values by whichever keyword arguments are specified.""" return self._replace(**kwargs) @property - def host(self) -> t.Optional[str]: + def host(self) -> str | None: """The host part of the URL if available, otherwise `None`. The host is either the hostname or the IP address mentioned in the URL. It will not contain the port. @@ -73,7 +93,7 @@ class BaseURL(_URLTuple): return self._split_host()[0] @property - def ascii_host(self) -> t.Optional[str]: + def ascii_host(self) -> str | None: """Works exactly like :attr:`host` but will return a result that is restricted to ASCII. If it finds a netloc that is not ASCII it will attempt to idna decode it. This is useful for socket @@ -82,13 +102,13 @@ class BaseURL(_URLTuple): rv = self.host if rv is not None and isinstance(rv, str): try: - rv = _encode_idna(rv) # type: ignore + rv = rv.encode("idna").decode("ascii") except UnicodeError: - rv = rv.encode("ascii", "ignore") # type: ignore - return _to_str(rv, "ascii", "ignore") + pass + return rv @property - def port(self) -> t.Optional[int]: + def port(self) -> int | None: """The port in the URL as an integer if it was present, `None` otherwise. This does not fill in default ports. """ @@ -101,14 +121,14 @@ class BaseURL(_URLTuple): return None @property - def auth(self) -> t.Optional[str]: + def auth(self) -> str | None: """The authentication part in the URL if available, `None` otherwise. """ return self._split_netloc()[0] @property - def username(self) -> t.Optional[str]: + def username(self) -> str | None: """The username if it was part of the URL, `None` otherwise. This undergoes URL decoding and will always be a string. """ @@ -118,14 +138,14 @@ class BaseURL(_URLTuple): return None @property - def raw_username(self) -> t.Optional[str]: + def raw_username(self) -> str | None: """The username if it was part of the URL, `None` otherwise. Unlike :attr:`username` this one is not being decoded. """ return self._split_auth()[0] @property - def password(self) -> t.Optional[str]: + def password(self) -> str | None: """The password if it was part of the URL, `None` otherwise. This undergoes URL decoding and will always be a string. """ @@ -135,20 +155,20 @@ class BaseURL(_URLTuple): return None @property - def raw_password(self) -> t.Optional[str]: + def raw_password(self) -> str | None: """The password if it was part of the URL, `None` otherwise. Unlike :attr:`password` this one is not being decoded. """ return self._split_auth()[1] - def decode_query(self, *args: t.Any, **kwargs: t.Any) -> "ds.MultiDict[str, str]": + def decode_query(self, *args: t.Any, **kwargs: t.Any) -> ds.MultiDict[str, str]: """Decodes the query part of the URL. Ths is a shortcut for calling :func:`url_decode` on the query argument. The arguments and keyword arguments are forwarded to :func:`url_decode` unchanged. """ return url_decode(self.query, *args, **kwargs) - def join(self, *args: t.Any, **kwargs: t.Any) -> "BaseURL": + def join(self, *args: t.Any, **kwargs: t.Any) -> BaseURL: """Joins this URL with another one. This is just a convenience function for calling into :meth:`url_join` and then parsing the return value again. @@ -185,7 +205,12 @@ class BaseURL(_URLTuple): def decode_netloc(self) -> str: """Decodes the netloc part into a string.""" - rv = _decode_idna(self.host or "") + host = self.host or "" + + if isinstance(host, bytes): + host = host.decode() + + rv = _decode_idna(host) if ":" in rv: rv = f"[{rv}]" @@ -205,7 +230,7 @@ class BaseURL(_URLTuple): rv = f"{auth}@{rv}" return rv - def to_uri_tuple(self) -> "BaseURL": + def to_uri_tuple(self) -> BaseURL: """Returns a :class:`BytesURL` tuple that holds a URI. This will encode all the information in the URL properly to ASCII using the rules a web browser would follow. @@ -215,7 +240,7 @@ class BaseURL(_URLTuple): """ return url_parse(iri_to_uri(self)) - def to_iri_tuple(self) -> "BaseURL": + def to_iri_tuple(self) -> BaseURL: """Returns a :class:`URL` tuple that holds a IRI. This will try to decode as much information as possible in the URL without losing information similar to how a web browser does it for the @@ -227,8 +252,8 @@ class BaseURL(_URLTuple): return url_parse(uri_to_iri(self)) def get_file_location( - self, pathformat: t.Optional[str] = None - ) -> t.Tuple[t.Optional[str], t.Optional[str]]: + self, pathformat: str | None = None + ) -> tuple[str | None, str | None]: """Returns a tuple with the location of the file in the form ``(server, location)``. If the netloc is empty in the URL or points to localhost, it's represented as ``None``. @@ -288,13 +313,13 @@ class BaseURL(_URLTuple): return host, path - def _split_netloc(self) -> t.Tuple[t.Optional[str], str]: + def _split_netloc(self) -> tuple[str | None, str]: if self._at in self.netloc: auth, _, netloc = self.netloc.partition(self._at) return auth, netloc return None, self.netloc - def _split_auth(self) -> t.Tuple[t.Optional[str], t.Optional[str]]: + def _split_auth(self) -> tuple[str | None, str | None]: auth = self._split_netloc()[0] if not auth: return None, None @@ -304,7 +329,7 @@ class BaseURL(_URLTuple): username, _, password = auth.partition(self._colon) return username, password - def _split_host(self) -> t.Tuple[t.Optional[str], t.Optional[str]]: + def _split_host(self) -> tuple[str | None, str | None]: rv = self._split_netloc()[1] if not rv: return None, None @@ -330,6 +355,9 @@ class URL(BaseURL): """Represents a parsed URL. This behaves like a regular tuple but also has some extra attributes that give further insight into the URL. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use the ``urllib.parse`` library instead. """ __slots__ = () @@ -338,21 +366,25 @@ class URL(BaseURL): _lbracket = "[" _rbracket = "]" - def encode(self, charset: str = "utf-8", errors: str = "replace") -> "BytesURL": + def encode(self, charset: str = "utf-8", errors: str = "replace") -> BytesURL: """Encodes the URL to a tuple made out of bytes. The charset is only being used for the path, query and fragment. """ return BytesURL( - self.scheme.encode("ascii"), # type: ignore + self.scheme.encode("ascii"), self.encode_netloc(), - self.path.encode(charset, errors), # type: ignore - self.query.encode(charset, errors), # type: ignore - self.fragment.encode(charset, errors), # type: ignore + self.path.encode(charset, errors), + self.query.encode(charset, errors), + self.fragment.encode(charset, errors), ) class BytesURL(BaseURL): - """Represents a parsed URL in bytes.""" + """Represents a parsed URL in bytes. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use the ``urllib.parse`` library instead. + """ __slots__ = () _at = b"@" # type: ignore @@ -367,7 +399,7 @@ class BytesURL(BaseURL): """Returns the netloc unchanged as bytes.""" return self.netloc # type: ignore - def decode(self, charset: str = "utf-8", errors: str = "replace") -> "URL": + def decode(self, charset: str = "utf-8", errors: str = "replace") -> URL: """Decodes the URL to a tuple made out of strings. The charset is only being used for the path, query and fragment. """ @@ -380,12 +412,10 @@ class BytesURL(BaseURL): ) -_unquote_maps: t.Dict[t.FrozenSet[int], t.Dict[bytes, int]] = {frozenset(): _hextobyte} +_unquote_maps: dict[frozenset[int], dict[bytes, int]] = {frozenset(): _hextobyte} -def _unquote_to_bytes( - string: t.Union[str, bytes], unsafe: t.Union[str, bytes] = "" -) -> bytes: +def _unquote_to_bytes(string: str | bytes, unsafe: str | bytes = "") -> bytes: if isinstance(string, str): string = string.encode("utf-8") @@ -417,14 +447,14 @@ def _unquote_to_bytes( def _url_encode_impl( - obj: t.Union[t.Mapping[str, str], t.Iterable[t.Tuple[str, str]]], + obj: t.Mapping[str, str] | t.Iterable[tuple[str, str]], charset: str, sort: bool, - key: t.Optional[t.Callable[[t.Tuple[str, str]], t.Any]], + key: t.Callable[[tuple[str, str]], t.Any] | None, ) -> t.Iterator[str]: from .datastructures import iter_multi_items - iterable: t.Iterable[t.Tuple[str, str]] = iter_multi_items(obj) + iterable: t.Iterable[tuple[str, str]] = iter_multi_items(obj) if sort: iterable = sorted(iterable, key=key) @@ -454,7 +484,7 @@ def _url_unquote_legacy(value: str, unsafe: str = "") -> str: def url_parse( - url: str, scheme: t.Optional[str] = None, allow_fragments: bool = True + url: str, scheme: str | None = None, allow_fragments: bool = True ) -> BaseURL: """Parses a URL from a string into a :class:`URL` tuple. If the URL is lacking a scheme it can be provided as second argument. Otherwise, @@ -467,7 +497,16 @@ def url_parse( :param scheme: the default schema to use if the URL is schemaless. :param allow_fragments: if set to `False` a fragment will be removed from the URL. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.urlsplit`` instead. """ + warnings.warn( + "'werkzeug.urls.url_parse' is deprecated and will be removed in Werkzeug 3.0." + " Use 'urllib.parse.urlsplit' instead.", + DeprecationWarning, + stacklevel=2, + ) s = _make_encode_wrapper(url) is_text_based = isinstance(url, str) @@ -501,14 +540,15 @@ def url_parse( url, query = url.split(s("?"), 1) result_type = URL if is_text_based else BytesURL + return result_type(scheme, netloc, url, query, fragment) def _make_fast_url_quote( charset: str = "utf-8", errors: str = "strict", - safe: t.Union[str, bytes] = "/:", - unsafe: t.Union[str, bytes] = "", + safe: str | bytes = "/:", + unsafe: str | bytes = "", ) -> t.Callable[[bytes], str]: """Precompile the translation table for a URL encoding function. @@ -544,11 +584,11 @@ def _fast_url_quote_plus(string: bytes) -> str: def url_quote( - string: t.Union[str, bytes], + string: str | bytes, charset: str = "utf-8", errors: str = "strict", - safe: t.Union[str, bytes] = "/:", - unsafe: t.Union[str, bytes] = "", + safe: str | bytes = "/:", + unsafe: str | bytes = "", ) -> str: """URL encode a single string with a given encoding. @@ -557,9 +597,19 @@ def url_quote( :param safe: an optional sequence of safe characters. :param unsafe: an optional sequence of unsafe characters. + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.quote`` instead. + .. versionadded:: 0.9.2 The `unsafe` parameter was added. """ + warnings.warn( + "'werkzeug.urls.url_quote' is deprecated and will be removed in Werkzeug 3.0." + " Use 'urllib.parse.quote' instead.", + DeprecationWarning, + stacklevel=2, + ) + if not isinstance(string, (str, bytes, bytearray)): string = str(string) if isinstance(string, str): @@ -587,17 +637,36 @@ def url_quote_plus( :param s: The string to quote. :param charset: The charset to be used. :param safe: An optional sequence of safe characters. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.quote_plus`` instead. """ + warnings.warn( + "'werkzeug.urls.url_quote_plus' is deprecated and will be removed in Werkzeug" + " 2.4. Use 'urllib.parse.quote_plus' instead.", + DeprecationWarning, + stacklevel=2, + ) + return url_quote(string, charset, errors, safe + " ", "+").replace(" ", "+") -def url_unparse(components: t.Tuple[str, str, str, str, str]) -> str: +def url_unparse(components: tuple[str, str, str, str, str]) -> str: """The reverse operation to :meth:`url_parse`. This accepts arbitrary as well as :class:`URL` tuples and returns a URL as a string. :param components: the parsed URL as tuple which should be converted into a URL string. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.urlunsplit`` instead. """ + warnings.warn( + "'werkzeug.urls.url_unparse' is deprecated and will be removed in Werkzeug 3.0." + " Use 'urllib.parse.urlunsplit' instead.", + DeprecationWarning, + stacklevel=2, + ) _check_str_tuple(components) scheme, netloc, path, query, fragment = components s = _make_encode_wrapper(scheme) @@ -623,7 +692,7 @@ def url_unparse(components: t.Tuple[str, str, str, str, str]) -> str: def url_unquote( - s: t.Union[str, bytes], + s: str | bytes, charset: str = "utf-8", errors: str = "replace", unsafe: str = "", @@ -636,7 +705,16 @@ def url_unquote( :param charset: the charset of the query string. If set to `None` no decoding will take place. :param errors: the error handling for the charset decoding. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.unquote`` instead. """ + warnings.warn( + "'werkzeug.urls.url_unquote' is deprecated and will be removed in Werkzeug 3.0." + " Use 'urllib.parse.unquote' instead.", + DeprecationWarning, + stacklevel=2, + ) rv = _unquote_to_bytes(s, unsafe) if charset is None: return rv @@ -644,7 +722,7 @@ def url_unquote( def url_unquote_plus( - s: t.Union[str, bytes], charset: str = "utf-8", errors: str = "replace" + s: str | bytes, charset: str = "utf-8", errors: str = "replace" ) -> str: """URL decode a single string with the given `charset` and decode "+" to whitespace. @@ -656,11 +734,22 @@ def url_unquote_plus( :param charset: the charset of the query string. If set to `None` no decoding will take place. :param errors: The error handling for the `charset` decoding. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.unquote_plus`` instead. """ + warnings.warn( + "'werkzeug.urls.url_unquote_plus' is deprecated and will be removed in Werkzeug" + " 2.4. Use 'urllib.parse.unquote_plus' instead.", + DeprecationWarning, + stacklevel=2, + ) + if isinstance(s, str): s = s.replace("+", " ") else: s = s.replace(b"+", b" ") + return url_unquote(s, charset, errors) @@ -676,7 +765,15 @@ def url_fix(s: str, charset: str = "utf-8") -> str: :param s: the string with the URL to fix. :param charset: The target charset for the URL if the url was given as a string. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. """ + warnings.warn( + "'werkzeug.urls.url_fix' is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) # First step is to switch to text processing and to convert # backslashes (which are invalid in URLs anyways) to slashes. This is # consistent with what Chrome does. @@ -694,27 +791,55 @@ def url_fix(s: str, charset: str = "utf-8") -> str: return url_unparse((url.scheme, url.encode_netloc(), path, qs, anchor)) -# not-unreserved characters remain quoted when unquoting to IRI -_to_iri_unsafe = "".join([chr(c) for c in range(128) if c not in _always_safe]) - - -def _codec_error_url_quote(e: UnicodeError) -> t.Tuple[str, int]: +def _codec_error_url_quote(e: UnicodeError) -> tuple[str, int]: """Used in :func:`uri_to_iri` after unquoting to re-quote any invalid bytes. """ # the docs state that UnicodeError does have these attributes, # but mypy isn't picking them up - out = _fast_url_quote(e.object[e.start : e.end]) # type: ignore + out = quote(e.object[e.start : e.end], safe="") # type: ignore return out, e.end # type: ignore codecs.register_error("werkzeug.url_quote", _codec_error_url_quote) +def _make_unquote_part(name: str, chars: str) -> t.Callable[[str, str, str], str]: + """Create a function that unquotes all percent encoded characters except those + given. This allows working with unquoted characters if possible while not changing + the meaning of a given part of a URL. + """ + choices = "|".join(f"{ord(c):02X}" for c in sorted(chars)) + pattern = re.compile(f"((?:%(?:{choices}))+)", re.I) + + def _unquote_partial(value: str, encoding: str, errors: str) -> str: + parts = iter(pattern.split(value)) + out = [] + + for part in parts: + out.append(unquote(part, encoding, errors)) + out.append(next(parts, "")) + + return "".join(out) + + _unquote_partial.__name__ = f"_unquote_{name}" + return _unquote_partial + + +# characters that should remain quoted in URL parts +# based on https://url.spec.whatwg.org/#percent-encoded-bytes +# always keep all controls, space, and % quoted +_always_unsafe = bytes((*range(0x21), 0x25, 0x7F)).decode() +_unquote_fragment = _make_unquote_part("fragment", _always_unsafe) +_unquote_query = _make_unquote_part("query", _always_unsafe + "&=+#") +_unquote_path = _make_unquote_part("path", _always_unsafe + "/?#") +_unquote_user = _make_unquote_part("user", _always_unsafe + ":@/?#") + + def uri_to_iri( - uri: t.Union[str, t.Tuple[str, str, str, str, str]], - charset: str = "utf-8", - errors: str = "werkzeug.url_quote", + uri: str | tuple[str, str, str, str, str], + charset: str | None = None, + errors: str | None = None, ) -> str: """Convert a URI to an IRI. All valid UTF-8 characters are unquoted, leaving all reserved and invalid characters quoted. If the URL has @@ -728,6 +853,13 @@ def uri_to_iri( :param errors: Error handler to use during ``bytes.encode``. By default, invalid bytes are left quoted. + .. versionchanged:: 2.3 + Passing a tuple or bytes, and the ``charset`` and ``errors`` parameters, are + deprecated and will be removed in Werkzeug 3.0. + + .. versionchanged:: 2.3 + Which characters remain quoted is specific to each part of the URL. + .. versionchanged:: 0.15 All reserved and invalid characters remain quoted. Previously, only some reserved characters were preserved, and invalid bytes @@ -736,24 +868,72 @@ def uri_to_iri( .. versionadded:: 0.6 """ if isinstance(uri, tuple): - uri = url_unparse(uri) + warnings.warn( + "Passing a tuple is deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + uri = urlunsplit(uri) + + if isinstance(uri, bytes): + warnings.warn( + "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + uri = uri.decode() + + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be removed" + " in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + + if errors is not None: + warnings.warn( + "The 'errors' parameter is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + errors = "werkzeug.url_quote" + + parts = urlsplit(uri) + path = _unquote_path(parts.path, charset, errors) + query = _unquote_query(parts.query, charset, errors) + fragment = _unquote_fragment(parts.fragment, charset, errors) + + if parts.hostname: + netloc = _decode_idna(parts.hostname) + else: + netloc = "" + + if ":" in netloc: + netloc = f"[{netloc}]" + + if parts.port: + netloc = f"{netloc}:{parts.port}" + + if parts.username: + auth = _unquote_user(parts.username, charset, errors) - uri = url_parse(_to_str(uri, charset)) - path = url_unquote(uri.path, charset, errors, _to_iri_unsafe) - query = url_unquote(uri.query, charset, errors, _to_iri_unsafe) - fragment = url_unquote(uri.fragment, charset, errors, _to_iri_unsafe) - return url_unparse((uri.scheme, uri.decode_netloc(), path, query, fragment)) + if parts.password: + auth = f"{auth}:{_unquote_user(parts.password, charset, errors)}" + netloc = f"{auth}@{netloc}" -# reserved characters remain unquoted when quoting to URI -_to_uri_safe = ":/?#[]@!$&'()*+,;=%" + return urlunsplit((parts.scheme, netloc, path, query, fragment)) def iri_to_uri( - iri: t.Union[str, t.Tuple[str, str, str, str, str]], - charset: str = "utf-8", - errors: str = "strict", - safe_conversion: bool = False, + iri: str | tuple[str, str, str, str, str], + charset: str | None = None, + errors: str | None = None, + safe_conversion: bool | None = None, ) -> str: """Convert an IRI to a URI. All non-ASCII and unsafe characters are quoted. If the URL has a domain, it is encoded to Punycode. @@ -764,55 +944,133 @@ def iri_to_uri( :param iri: The IRI to convert. :param charset: The encoding of the IRI. :param errors: Error handler to use during ``bytes.encode``. - :param safe_conversion: Return the URL unchanged if it only contains - ASCII characters and no whitespace. See the explanation below. - There is a general problem with IRI conversion with some protocols - that are in violation of the URI specification. Consider the - following two IRIs:: + .. versionchanged:: 2.3 + Passing a tuple or bytes, and the ``charset`` and ``errors`` parameters, are + deprecated and will be removed in Werkzeug 3.0. - magnet:?xt=uri:whatever - itms-services://?action=download-manifest + .. versionchanged:: 2.3 + Which characters remain unquoted is specific to each part of the URL. - After parsing, we don't know if the scheme requires the ``//``, - which is dropped if empty, but conveys different meanings in the - final URL if it's present or not. In this case, you can use - ``safe_conversion``, which will return the URL unchanged if it only - contains ASCII characters and no whitespace. This can result in a - URI with unquoted characters if it was not already quoted correctly, - but preserves the URL's semantics. Werkzeug uses this for the - ``Location`` header for redirects. + .. versionchanged:: 2.3 + The ``safe_conversion`` parameter is deprecated and will be removed in Werkzeug + 2.4. .. versionchanged:: 0.15 - All reserved characters remain unquoted. Previously, only some - reserved characters were left unquoted. + All reserved characters remain unquoted. Previously, only some reserved + characters were left unquoted. .. versionchanged:: 0.9.6 The ``safe_conversion`` parameter was added. .. versionadded:: 0.6 """ + if charset is not None: + warnings.warn( + "The 'charset' parameter is deprecated and will be removed" + " in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + charset = "utf-8" + if isinstance(iri, tuple): - iri = url_unparse(iri) + warnings.warn( + "Passing a tuple is deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + iri = urlunsplit(iri) + + if isinstance(iri, bytes): + warnings.warn( + "Passing bytes is deprecated and will not be supported in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + iri = iri.decode(charset) + + if errors is not None: + warnings.warn( + "The 'errors' parameter is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + else: + errors = "strict" + + if safe_conversion is not None: + warnings.warn( + "The 'safe_conversion' parameter is deprecated and will be removed in" + " Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) if safe_conversion: - # If we're not sure if it's safe to convert the URL, and it only - # contains ASCII characters, return it unconverted. + # If we're not sure if it's safe to normalize the URL, and it only contains + # ASCII characters, return it as-is. try: - native_iri = _to_str(iri) - ascii_iri = native_iri.encode("ascii") + ascii_iri = iri.encode("ascii") # Only return if it doesn't have whitespace. (Why?) if len(ascii_iri.split()) == 1: - return native_iri + return iri except UnicodeError: pass - iri = url_parse(_to_str(iri, charset, errors)) - path = url_quote(iri.path, charset, errors, _to_uri_safe) - query = url_quote(iri.query, charset, errors, _to_uri_safe) - fragment = url_quote(iri.fragment, charset, errors, _to_uri_safe) - return url_unparse((iri.scheme, iri.encode_netloc(), path, query, fragment)) + parts = urlsplit(iri) + # safe = https://url.spec.whatwg.org/#url-path-segment-string + # as well as percent for things that are already quoted + path = quote(parts.path, safe="%!$&'()*+,/:;=@", encoding=charset, errors=errors) + query = quote(parts.query, safe="%!$&'()*+,/:;=?@", encoding=charset, errors=errors) + fragment = quote( + parts.fragment, safe="%!#$&'()*+,/:;=?@", encoding=charset, errors=errors + ) + + if parts.hostname: + netloc = parts.hostname.encode("idna").decode("ascii") + else: + netloc = "" + + if ":" in netloc: + netloc = f"[{netloc}]" + + if parts.port: + netloc = f"{netloc}:{parts.port}" + + if parts.username: + auth = quote(parts.username, safe="%!$&'()*+,;=") + + if parts.password: + pass_quoted = quote(parts.password, safe="%!$&'()*+,;=") + auth = f"{auth}:{pass_quoted}" + + netloc = f"{auth}@{netloc}" + + return urlunsplit((parts.scheme, netloc, path, query, fragment)) + + +def _invalid_iri_to_uri(iri: str) -> str: + """The URL scheme ``itms-services://`` must contain the ``//`` even though it does + not have a host component. There may be other invalid schemes as well. Currently, + responses will always call ``iri_to_uri`` on the redirect ``Location`` header, which + removes the ``//``. For now, if the IRI only contains ASCII and does not contain + spaces, pass it on as-is. In Werkzeug 3.0, this should become a + ``response.process_location`` flag. + + :meta private: + """ + try: + iri.encode("ascii") + except UnicodeError: + pass + else: + if len(iri.split(None, 1)) == 1: + return iri + + return iri_to_uri(iri) def url_decode( @@ -821,8 +1079,8 @@ def url_decode( include_empty: bool = True, errors: str = "replace", separator: str = "&", - cls: t.Optional[t.Type["ds.MultiDict"]] = None, -) -> "ds.MultiDict[str, str]": + cls: type[ds.MultiDict] | None = None, +) -> ds.MultiDict[str, str]: """Parse a query string and return it as a :class:`MultiDict`. :param s: The query string to parse. @@ -833,9 +1091,11 @@ def url_decode( :param separator: Separator character between pairs. :param cls: Container to hold result instead of :class:`MultiDict`. - .. versionchanged:: 2.0 - The ``decode_keys`` parameter is deprecated and will be removed - in Werkzeug 2.1. + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. Use ``urllib.parse.parse_qs`` instead. + + .. versionchanged:: 2.1 + The ``decode_keys`` parameter was removed. .. versionchanged:: 0.5 In previous versions ";" and "&" could be used for url decoding. @@ -845,6 +1105,13 @@ def url_decode( .. versionchanged:: 0.5 The ``cls`` parameter was added. """ + warnings.warn( + "'werkzeug.urls.url_decode' is deprecated and will be removed in Werkzeug 2.4." + " Use 'urllib.parse.parse_qs' instead.", + DeprecationWarning, + stacklevel=2, + ) + if cls is None: from .datastructures import MultiDict # noqa: F811 @@ -866,9 +1133,9 @@ def url_decode_stream( include_empty: bool = True, errors: str = "replace", separator: bytes = b"&", - cls: t.Optional[t.Type["ds.MultiDict"]] = None, - limit: t.Optional[int] = None, -) -> "ds.MultiDict[str, str]": + cls: type[ds.MultiDict] | None = None, + limit: int | None = None, +) -> ds.MultiDict[str, str]: """Works like :func:`url_decode` but decodes a stream. The behavior of stream and limit follows functions like :func:`~werkzeug.wsgi.make_line_iter`. The generator of pairs is @@ -887,12 +1154,21 @@ def url_decode_stream( :param limit: the content length of the URL data. Not necessary if a limited stream is provided. - .. versionchanged:: 2.0 - The ``decode_keys`` and ``return_iterator`` parameters are - deprecated and will be removed in Werkzeug 2.1. + .. deprecated:: 2.3 + Will be removed in Werkzeug 2.4. Use ``urllib.parse.parse_qs`` instead. + + .. versionchanged:: 2.1 + The ``decode_keys`` and ``return_iterator`` parameters were removed. .. versionadded:: 0.8 """ + warnings.warn( + "'werkzeug.urls.url_decode_stream' is deprecated and will be removed in" + " Werkzeug 2.4. Use 'urllib.parse.parse_qs' instead.", + DeprecationWarning, + stacklevel=2, + ) + from .wsgi import make_chunk_iter pair_iter = make_chunk_iter(stream, separator, limit) @@ -908,7 +1184,7 @@ def url_decode_stream( def _url_decode_impl( pair_iter: t.Iterable[t.AnyStr], charset: str, include_empty: bool, errors: str -) -> t.Iterator[t.Tuple[str, str]]: +) -> t.Iterator[tuple[str, str]]: for pair in pair_iter: if not pair: continue @@ -928,10 +1204,10 @@ def _url_decode_impl( def url_encode( - obj: t.Union[t.Mapping[str, str], t.Iterable[t.Tuple[str, str]]], + obj: t.Mapping[str, str] | t.Iterable[tuple[str, str]], charset: str = "utf-8", sort: bool = False, - key: t.Optional[t.Callable[[t.Tuple[str, str]], t.Any]] = None, + key: t.Callable[[tuple[str, str]], t.Any] | None = None, separator: str = "&", ) -> str: """URL encode a dict/`MultiDict`. If a value is `None` it will not appear @@ -945,23 +1221,31 @@ def url_encode( :param key: an optional function to be used for sorting. For more details check out the :func:`sorted` documentation. - .. versionchanged:: 2.0 - The ``encode_keys`` parameter is deprecated and will be removed - in Werkzeug 2.1. + .. deprecated:: 2.3 + Will be removed in Werkzeug 2.4. Use ``urllib.parse.urlencode`` instead. + + .. versionchanged:: 2.1 + The ``encode_keys`` parameter was removed. .. versionchanged:: 0.5 Added the ``sort``, ``key``, and ``separator`` parameters. """ + warnings.warn( + "'werkzeug.urls.url_encode' is deprecated and will be removed in Werkzeug 2.4." + " Use 'urllib.parse.urlencode' instead.", + DeprecationWarning, + stacklevel=2, + ) separator = _to_str(separator, "ascii") return separator.join(_url_encode_impl(obj, charset, sort, key)) def url_encode_stream( - obj: t.Union[t.Mapping[str, str], t.Iterable[t.Tuple[str, str]]], - stream: t.Optional[t.IO[str]] = None, + obj: t.Mapping[str, str] | t.Iterable[tuple[str, str]], + stream: t.IO[str] | None = None, charset: str = "utf-8", sort: bool = False, - key: t.Optional[t.Callable[[t.Tuple[str, str]], t.Any]] = None, + key: t.Callable[[tuple[str, str]], t.Any] | None = None, separator: str = "&", ) -> None: """Like :meth:`url_encode` but writes the results to a stream @@ -978,12 +1262,20 @@ def url_encode_stream( :param key: an optional function to be used for sorting. For more details check out the :func:`sorted` documentation. - .. versionchanged:: 2.0 - The ``encode_keys`` parameter is deprecated and will be removed - in Werkzeug 2.1. + .. deprecated:: 2.3 + Will be removed in Werkzeug 2.4. Use ``urllib.parse.urlencode`` instead. + + .. versionchanged:: 2.1 + The ``encode_keys`` parameter was removed. .. versionadded:: 0.8 """ + warnings.warn( + "'werkzeug.urls.url_encode_stream' is deprecated and will be removed in" + " Werkzeug 2.4. Use 'urllib.parse.urlencode' instead.", + DeprecationWarning, + stacklevel=2, + ) separator = _to_str(separator, "ascii") gen = _url_encode_impl(obj, charset, sort, key) if stream is None: @@ -996,8 +1288,8 @@ def url_encode_stream( def url_join( - base: t.Union[str, t.Tuple[str, str, str, str, str]], - url: t.Union[str, t.Tuple[str, str, str, str, str]], + base: str | tuple[str, str, str, str, str], + url: str | tuple[str, str, str, str, str], allow_fragments: bool = True, ) -> str: """Join a base URL and a possibly relative URL to form an absolute @@ -1006,7 +1298,17 @@ def url_join( :param base: the base URL for the join operation. :param url: the URL to join. :param allow_fragments: indicates whether fragments should be allowed. + + .. deprecated:: 2.3 + Will be removed in Werkzeug 2.4. Use ``urllib.parse.urljoin`` instead. """ + warnings.warn( + "'werkzeug.urls.url_join' is deprecated and will be removed in Werkzeug 2.4." + " Use 'urllib.parse.urljoin' instead.", + DeprecationWarning, + stacklevel=2, + ) + if isinstance(base, tuple): base = url_unparse(base) if isinstance(url, tuple): @@ -1064,3 +1366,11 @@ def url_join( path = s("/").join(segments) return url_unparse((scheme, netloc, path, query, fragment)) + + +def _urlencode( + query: t.Mapping[str, str] | t.Iterable[tuple[str, str]], encoding: str = "utf-8" +) -> str: + items = [x for x in iter_multi_items(query) if x[1] is not None] + # safe = https://url.spec.whatwg.org/#percent-encoded-bytes + return urlencode(items, safe="!$'()*,/:;?@", encoding=encoding) diff --git a/contrib/python/Werkzeug/py3/werkzeug/user_agent.py b/contrib/python/Werkzeug/py3/werkzeug/user_agent.py index 66ffcbe07db..17e5d3fdbf0 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/user_agent.py +++ b/contrib/python/Werkzeug/py3/werkzeug/user_agent.py @@ -1,4 +1,4 @@ -import typing as t +from __future__ import annotations class UserAgent: @@ -17,16 +17,16 @@ class UserAgent: provide a built-in parser. """ - platform: t.Optional[str] = None + platform: str | None = None """The OS name, if it could be parsed from the string.""" - browser: t.Optional[str] = None + browser: str | None = None """The browser name, if it could be parsed from the string.""" - version: t.Optional[str] = None + version: str | None = None """The browser version, if it could be parsed from the string.""" - language: t.Optional[str] = None + language: str | None = None """The browser language, if it could be parsed from the string.""" def __init__(self, string: str) -> None: diff --git a/contrib/python/Werkzeug/py3/werkzeug/utils.py b/contrib/python/Werkzeug/py3/werkzeug/utils.py index 4ef5837137b..785ac28b980 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/utils.py +++ b/contrib/python/Werkzeug/py3/werkzeug/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import mimetypes import os @@ -8,6 +10,7 @@ import typing as t import unicodedata from datetime import datetime from time import time +from urllib.parse import quote from zlib import adler32 from markupsafe import escape @@ -19,7 +22,6 @@ from .datastructures import Headers from .exceptions import NotFound from .exceptions import RequestedRangeNotSatisfiable from .security import safe_join -from .urls import url_quote from .wsgi import wrap_file if t.TYPE_CHECKING: @@ -31,19 +33,14 @@ _T = t.TypeVar("_T") _entity_re = re.compile(r"&([^;]+);") _filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") -_windows_device_files = ( +_windows_device_files = { "CON", - "AUX", - "COM1", - "COM2", - "COM3", - "COM4", - "LPT1", - "LPT2", - "LPT3", "PRN", + "AUX", "NUL", -) + *(f"COM{i}" for i in range(10)), + *(f"LPT{i}" for i in range(10)), +} class cached_property(property, t.Generic[_T]): @@ -80,8 +77,8 @@ class cached_property(property, t.Generic[_T]): def __init__( self, fget: t.Callable[[t.Any], _T], - name: t.Optional[str] = None, - doc: t.Optional[str] = None, + name: str | None = None, + doc: str | None = None, ) -> None: super().__init__(fget, doc=doc) self.__name__ = name or fget.__name__ @@ -145,14 +142,14 @@ class environ_property(_DictAccessorProperty[_TAccessorValue]): read_only = True - def lookup(self, obj: "Request") -> "WSGIEnvironment": + def lookup(self, obj: Request) -> WSGIEnvironment: return obj.environ class header_property(_DictAccessorProperty[_TAccessorValue]): """Like `environ_property` but for headers.""" - def lookup(self, obj: t.Union["Request", "Response"]) -> Headers: + def lookup(self, obj: Request | Response) -> Headers: return obj.headers @@ -242,8 +239,8 @@ def secure_filename(filename: str) -> str: def redirect( - location: str, code: int = 302, Response: t.Optional[t.Type["Response"]] = None -) -> "Response": + location: str, code: int = 302, Response: type[Response] | None = None +) -> Response: """Returns a response object (a WSGI application) that, if called, redirects the client to the target location. Supported codes are 301, 302, 303, 305, 307, and 308. 300 is not supported because @@ -264,24 +261,16 @@ def redirect( unspecified. """ if Response is None: - from .wrappers import Response # type: ignore - - display_location = escape(location) - if isinstance(location, str): - # Safe conversion is necessary here as we might redirect - # to a broken URI scheme (for instance itms-services). - from .urls import iri_to_uri - - location = iri_to_uri(location, safe_conversion=True) + from .wrappers import Response - response = Response( # type: ignore + html_location = escape(location) + response = Response( # type: ignore[misc] "<!doctype html>\n" "<html lang=en>\n" "<title>Redirecting...</title>\n" "<h1>Redirecting...</h1>\n" "<p>You should be redirected automatically to the target URL: " - f'<a href="{escape(location)}">{display_location}</a>. If' - " not, click the link.\n", + f'<a href="{html_location}">{html_location}</a>. If not, click the link.\n', code, mimetype="text/html", ) @@ -289,7 +278,7 @@ def redirect( return response -def append_slash_redirect(environ: "WSGIEnvironment", code: int = 308) -> "Response": +def append_slash_redirect(environ: WSGIEnvironment, code: int = 308) -> Response: """Redirect to the current URL with a slash appended. If the current URL is ``/user/42``, the redirect URL will be @@ -327,21 +316,19 @@ def append_slash_redirect(environ: "WSGIEnvironment", code: int = 308) -> "Respo def send_file( - path_or_file: t.Union[os.PathLike, str, t.IO[bytes]], - environ: "WSGIEnvironment", - mimetype: t.Optional[str] = None, + path_or_file: os.PathLike | str | t.IO[bytes], + environ: WSGIEnvironment, + mimetype: str | None = None, as_attachment: bool = False, - download_name: t.Optional[str] = None, + download_name: str | None = None, conditional: bool = True, - etag: t.Union[bool, str] = True, - last_modified: t.Optional[t.Union[datetime, int, float]] = None, - max_age: t.Optional[ - t.Union[int, t.Callable[[t.Optional[str]], t.Optional[int]]] - ] = None, + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, use_x_sendfile: bool = False, - response_class: t.Optional[t.Type["Response"]] = None, - _root_path: t.Optional[t.Union[os.PathLike, str]] = None, -) -> "Response": + response_class: type[Response] | None = None, + _root_path: os.PathLike | str | None = None, +) -> Response: """Send the contents of a file to the client. The first argument can be a file path or a file-like object. Paths @@ -419,10 +406,10 @@ def send_file( response_class = Response - path: t.Optional[str] = None - file: t.Optional[t.IO[bytes]] = None - size: t.Optional[int] = None - mtime: t.Optional[float] = None + path: str | None = None + file: t.IO[bytes] | None = None + size: int | None = None + mtime: float | None = None headers = Headers() if isinstance(path_or_file, (os.PathLike, str)) or hasattr( @@ -470,7 +457,8 @@ def send_file( except UnicodeEncodeError: simple = unicodedata.normalize("NFKD", download_name) simple = simple.encode("ascii", "ignore").decode("ascii") - quoted = url_quote(download_name, safe="") + # safe = RFC 5987 attr-char + quoted = quote(download_name, safe="!#$&+-.^_`|~") names = {"filename": simple, "filename*": f"UTF-8''{quoted}"} else: names = {"filename": download_name} @@ -547,11 +535,11 @@ def send_file( def send_from_directory( - directory: t.Union[os.PathLike, str], - path: t.Union[os.PathLike, str], - environ: "WSGIEnvironment", + directory: os.PathLike | str, + path: os.PathLike | str, + environ: WSGIEnvironment, **kwargs: t.Any, -) -> "Response": +) -> Response: """Send a file from within a directory using :func:`send_file`. This is a secure way to serve files from a folder, such as static @@ -582,12 +570,8 @@ def send_from_directory( if "_root_path" in kwargs: path = os.path.join(kwargs["_root_path"], path) - try: - if not os.path.isfile(path): - raise NotFound() - except ValueError: - # path contains null byte on Python < 3.8 - raise NotFound() from None + if not os.path.isfile(path): + raise NotFound() return send_file(path, environ, **kwargs) diff --git a/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py b/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py index 2de77df42a3..f4f51b1dc60 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py +++ b/contrib/python/Werkzeug/py3/werkzeug/wrappers/request.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import functools import json -import typing import typing as t from io import BytesIO @@ -11,6 +12,8 @@ from ..datastructures import FileStorage from ..datastructures import ImmutableMultiDict from ..datastructures import iter_multi_items from ..datastructures import MultiDict +from ..exceptions import BadRequest +from ..exceptions import UnsupportedMediaType from ..formparser import default_stream_factory from ..formparser import FormDataParser from ..sansio.request import Request as _SansIORequest @@ -18,10 +21,8 @@ from ..utils import cached_property from ..utils import environ_property from ..wsgi import _get_server from ..wsgi import get_input_stream -from werkzeug.exceptions import BadRequest if t.TYPE_CHECKING: - import typing_extensions as te from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment @@ -50,12 +51,14 @@ class Request(_SansIORequest): it unavailable to the final application. .. versionchanged:: 2.1 + Old ``BaseRequest`` and mixin classes were removed. + + .. versionchanged:: 2.1 Remove the ``disable_data_descriptor`` attribute. .. versionchanged:: 2.0 Combine ``BaseRequest`` and mixins into a single ``Request`` - class. Using the old classes is deprecated and will be removed - in Werkzeug 2.1. + class. .. versionchanged:: 0.5 Read-only mode is enforced with immutable classes for all data. @@ -67,10 +70,8 @@ class Request(_SansIORequest): #: parsing fails because more than the specified value is transmitted #: a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised. #: - #: Have a look at :doc:`/request_data` for more details. - #: #: .. versionadded:: 0.5 - max_content_length: t.Optional[int] = None + max_content_length: int | None = None #: the maximum form field size. This is forwarded to the form data #: parsing function (:func:`parse_form_data`). When set and the @@ -78,10 +79,8 @@ class Request(_SansIORequest): #: data in memory for post data is longer than the specified value a #: :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised. #: - #: Have a look at :doc:`/request_data` for more details. - #: #: .. versionadded:: 0.5 - max_form_memory_size: t.Optional[int] = None + max_form_memory_size: int | None = None #: The maximum number of multipart parts to parse, passed to #: :attr:`form_data_parser_class`. Parsing form data with more than this @@ -92,11 +91,11 @@ class Request(_SansIORequest): #: The form data parser that should be used. Can be replaced to customize #: the form date parsing. - form_data_parser_class: t.Type[FormDataParser] = FormDataParser + form_data_parser_class: type[FormDataParser] = FormDataParser #: The WSGI environment containing HTTP headers and information from #: the WSGI server. - environ: "WSGIEnvironment" + environ: WSGIEnvironment #: Set when creating the request object. If ``True``, reading from #: the request body will cause a ``RuntimeException``. Useful to @@ -105,7 +104,7 @@ class Request(_SansIORequest): def __init__( self, - environ: "WSGIEnvironment", + environ: WSGIEnvironment, populate_request: bool = True, shallow: bool = False, ) -> None: @@ -113,12 +112,8 @@ class Request(_SansIORequest): method=environ.get("REQUEST_METHOD", "GET"), scheme=environ.get("wsgi.url_scheme", "http"), server=_get_server(environ), - root_path=_wsgi_decoding_dance( - environ.get("SCRIPT_NAME") or "", self.charset, self.encoding_errors - ), - path=_wsgi_decoding_dance( - environ.get("PATH_INFO") or "", self.charset, self.encoding_errors - ), + root_path=_wsgi_decoding_dance(environ.get("SCRIPT_NAME") or ""), + path=_wsgi_decoding_dance(environ.get("PATH_INFO") or ""), query_string=environ.get("QUERY_STRING", "").encode("latin1"), headers=EnvironHeaders(environ), remote_addr=environ.get("REMOTE_ADDR"), @@ -130,7 +125,7 @@ class Request(_SansIORequest): self.environ["werkzeug.request"] = self @classmethod - def from_values(cls, *args: t.Any, **kwargs: t.Any) -> "Request": + def from_values(cls, *args: t.Any, **kwargs: t.Any) -> Request: """Create a new request object based on the values provided. If environ is given missing values are filled from there. This method is useful for small scripts when you need to simulate a request from an URL. @@ -150,8 +145,9 @@ class Request(_SansIORequest): """ from ..test import EnvironBuilder - charset = kwargs.pop("charset", cls.charset) - kwargs["charset"] = charset + kwargs.setdefault( + "charset", cls.charset if not isinstance(cls.charset, property) else None + ) builder = EnvironBuilder(*args, **kwargs) try: return builder.get_request(cls) @@ -159,9 +155,7 @@ class Request(_SansIORequest): builder.close() @classmethod - def application( - cls, f: t.Callable[["Request"], "WSGIApplication"] - ) -> "WSGIApplication": + def application(cls, f: t.Callable[[Request], WSGIApplication]) -> WSGIApplication: """Decorate a function as responder that accepts the request as the last argument. This works like the :func:`responder` decorator but the function is passed the request object as the @@ -200,10 +194,10 @@ class Request(_SansIORequest): def _get_file_stream( self, - total_content_length: t.Optional[int], - content_type: t.Optional[str], - filename: t.Optional[str] = None, - content_length: t.Optional[int] = None, + total_content_length: int | None, + content_type: str | None, + filename: str | None = None, + content_length: int | None = None, ) -> t.IO[bytes]: """Called to get a stream for the file upload. @@ -246,14 +240,16 @@ class Request(_SansIORequest): .. versionadded:: 0.8 """ + charset = self._charset if self._charset != "utf-8" else None + errors = self._encoding_errors if self._encoding_errors != "replace" else None return self.form_data_parser_class( - self._get_file_stream, - self.charset, - self.encoding_errors, - self.max_form_memory_size, - self.max_content_length, - self.parameter_storage_class, + stream_factory=self._get_file_stream, + charset=charset, + errors=errors, + max_form_memory_size=self.max_form_memory_size, + max_content_length=self.max_content_length, max_form_parts=self.max_form_parts, + cls=self.parameter_storage_class, ) def _load_form_data(self) -> None: @@ -312,7 +308,7 @@ class Request(_SansIORequest): for _key, value in iter_multi_items(files or ()): value.close() - def __enter__(self) -> "Request": + def __enter__(self) -> Request: return self def __exit__(self, exc_type, exc_value, tb) -> None: # type: ignore @@ -320,21 +316,30 @@ class Request(_SansIORequest): @cached_property def stream(self) -> t.IO[bytes]: - """ - If the incoming form data was not encoded with a known mimetype - the data is stored unmodified in this stream for consumption. Most - of the time it is a better idea to use :attr:`data` which will give - you that data as a string. The stream only returns the data once. + """The WSGI input stream, with safety checks. This stream can only be consumed + once. + + Use :meth:`get_data` to get the full data as bytes or text. The :attr:`data` + attribute will contain the full bytes only if they do not represent form data. + The :attr:`form` attribute will contain the parsed form data in that case. + + Unlike :attr:`input_stream`, this stream guards against infinite streams or + reading past :attr:`content_length` or :attr:`max_content_length`. + + If ``max_content_length`` is set, it can be enforced on streams if + ``wsgi.input_terminated`` is set. Otherwise, an empty stream is returned. - Unlike :attr:`input_stream` this stream is properly guarded that you - can't accidentally read past the length of the input. Werkzeug will - internally always refer to this stream to read data which makes it - possible to wrap this object with a stream that does filtering. + If the limit is reached before the underlying stream is exhausted (such as a + file that is too large, or an infinite stream), the remaining contents of the + stream cannot be read safely. Depending on how the server handles this, clients + may show a "connection reset" failure instead of seeing the 413 response. + + .. versionchanged:: 2.3 + Check ``max_content_length`` preemptively and while reading. .. versionchanged:: 0.9 - This stream is now always available but might be consumed by the - form parser later on. Previously the stream was only set if no - parsing happened. + The stream is always set (but may be consumed) even if form parsing was + accessed first. """ if self.shallow: raise RuntimeError( @@ -342,46 +347,51 @@ class Request(_SansIORequest): " from the input stream is disabled." ) - return get_input_stream(self.environ) + return get_input_stream( + self.environ, max_content_length=self.max_content_length + ) input_stream = environ_property[t.IO[bytes]]( "wsgi.input", - doc="""The WSGI input stream. + doc="""The raw WSGI input stream, without any safety checks. + + This is dangerous to use. It does not guard against infinite streams or reading + past :attr:`content_length` or :attr:`max_content_length`. - In general it's a bad idea to use this one because you can - easily read past the boundary. Use the :attr:`stream` - instead.""", + Use :attr:`stream` instead. + """, ) @cached_property def data(self) -> bytes: - """ - Contains the incoming request data as string in case it came with - a mimetype Werkzeug does not handle. + """The raw data read from :attr:`stream`. Will be empty if the request + represents form data. + + To get the raw data even if it represents form data, use :meth:`get_data`. """ return self.get_data(parse_form_data=True) - @typing.overload + @t.overload def get_data( # type: ignore self, cache: bool = True, - as_text: "te.Literal[False]" = False, + as_text: t.Literal[False] = False, parse_form_data: bool = False, ) -> bytes: ... - @typing.overload + @t.overload def get_data( self, cache: bool = True, - as_text: "te.Literal[True]" = ..., + as_text: t.Literal[True] = ..., parse_form_data: bool = False, ) -> str: ... def get_data( self, cache: bool = True, as_text: bool = False, parse_form_data: bool = False - ) -> t.Union[bytes, str]: + ) -> bytes | str: """This reads the buffered incoming data from the client into one bytes object. By default this is cached but that behavior can be changed by setting `cache` to `False`. @@ -414,11 +424,11 @@ class Request(_SansIORequest): if cache: self._cached_data = rv if as_text: - rv = rv.decode(self.charset, self.encoding_errors) + rv = rv.decode(self._charset, self._encoding_errors) return rv @cached_property - def form(self) -> "ImmutableMultiDict[str, str]": + def form(self) -> ImmutableMultiDict[str, str]: """The form parameters. By default an :class:`~werkzeug.datastructures.ImmutableMultiDict` is returned from this function. This can be changed by setting @@ -437,7 +447,7 @@ class Request(_SansIORequest): return self.form @cached_property - def values(self) -> "CombinedMultiDict[str, str]": + def values(self) -> CombinedMultiDict[str, str]: """A :class:`werkzeug.datastructures.CombinedMultiDict` that combines :attr:`args` and :attr:`form`. @@ -466,7 +476,7 @@ class Request(_SansIORequest): return CombinedMultiDict(args) @cached_property - def files(self) -> "ImmutableMultiDict[str, FileStorage]": + def files(self) -> ImmutableMultiDict[str, FileStorage]: """:class:`~werkzeug.datastructures.MultiDict` object containing all uploaded files. Each key in :attr:`files` is the name from the ``<input type="file" name="">``. Each value in :attr:`files` is a @@ -533,14 +543,17 @@ class Request(_SansIORequest): json_module = json @property - def json(self) -> t.Optional[t.Any]: + def json(self) -> t.Any | None: """The parsed JSON data if :attr:`mimetype` indicates JSON (:mimetype:`application/json`, see :attr:`is_json`). Calls :meth:`get_json` with default arguments. If the request content type is not ``application/json``, this - will raise a 400 Bad Request error. + will raise a 415 Unsupported Media Type error. + + .. versionchanged:: 2.3 + Raise a 415 error instead of 400. .. versionchanged:: 2.1 Raise a 400 error if the content type is incorrect. @@ -549,30 +562,30 @@ class Request(_SansIORequest): # Cached values for ``(silent=False, silent=True)``. Initialized # with sentinel values. - _cached_json: t.Tuple[t.Any, t.Any] = (Ellipsis, Ellipsis) + _cached_json: tuple[t.Any, t.Any] = (Ellipsis, Ellipsis) @t.overload def get_json( - self, force: bool = ..., silent: "te.Literal[False]" = ..., cache: bool = ... + self, force: bool = ..., silent: t.Literal[False] = ..., cache: bool = ... ) -> t.Any: ... @t.overload def get_json( self, force: bool = ..., silent: bool = ..., cache: bool = ... - ) -> t.Optional[t.Any]: + ) -> t.Any | None: ... def get_json( self, force: bool = False, silent: bool = False, cache: bool = True - ) -> t.Optional[t.Any]: + ) -> t.Any | None: """Parse :attr:`data` as JSON. If the mimetype does not indicate JSON (:mimetype:`application/json`, see :attr:`is_json`), or parsing fails, :meth:`on_json_loading_failed` is called and its return value is used as the return value. By default this - raises a 400 Bad Request error. + raises a 415 Unsupported Media Type resp. :param force: Ignore the mimetype and always try to parse JSON. :param silent: Silence mimetype and parsing errors, and @@ -580,6 +593,9 @@ class Request(_SansIORequest): :param cache: Store the parsed JSON to return for subsequent calls. + .. versionchanged:: 2.3 + Raise a 415 error instead of 400. + .. versionchanged:: 2.1 Raise a 400 error if the content type is incorrect. """ @@ -615,7 +631,7 @@ class Request(_SansIORequest): return rv - def on_json_loading_failed(self, e: t.Optional[ValueError]) -> t.Any: + def on_json_loading_failed(self, e: ValueError | None) -> t.Any: """Called if :meth:`get_json` fails and isn't silenced. If this method returns a value, it is used as the return value @@ -624,11 +640,14 @@ class Request(_SansIORequest): :param e: If parsing failed, this is the exception. It will be ``None`` if the content type wasn't ``application/json``. + + .. versionchanged:: 2.3 + Raise a 415 error instead of 400. """ if e is not None: raise BadRequest(f"Failed to decode JSON object: {e}") - raise BadRequest( + raise UnsupportedMediaType( "Did not attempt to load JSON data because the request" " Content-Type was not 'application/json'." ) diff --git a/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py b/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py index 454208c773f..c8488094e11 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py +++ b/contrib/python/Werkzeug/py3/werkzeug/wrappers/response.py @@ -1,15 +1,15 @@ +from __future__ import annotations + import json -import typing import typing as t -import warnings from http import HTTPStatus +from urllib.parse import urljoin -from .._internal import _to_bytes from ..datastructures import Headers from ..http import remove_entity_headers from ..sansio.response import Response as _SansIOResponse +from ..urls import _invalid_iri_to_uri from ..urls import iri_to_uri -from ..urls import url_join from ..utils import cached_property from ..wsgi import ClosingIterator from ..wsgi import get_current_url @@ -22,31 +22,13 @@ from werkzeug.http import parse_range_header from werkzeug.wsgi import _RangeWrapper if t.TYPE_CHECKING: - import typing_extensions as te from _typeshed.wsgi import StartResponse from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment from .request import Request -def _warn_if_string(iterable: t.Iterable) -> None: - """Helper for the response objects to check if the iterable returned - to the WSGI server is not a string. - """ - if isinstance(iterable, str): - warnings.warn( - "Response iterable was set to a string. This will appear to" - " work but means that the server will send the data to the" - " client one character at a time. This is almost never" - " intended behavior, use 'response.data' to assign strings" - " to the response object.", - stacklevel=2, - ) - - -def _iter_encoded( - iterable: t.Iterable[t.Union[str, bytes]], charset: str -) -> t.Iterator[bytes]: +def _iter_encoded(iterable: t.Iterable[str | bytes], charset: str) -> t.Iterator[bytes]: for item in iterable: if isinstance(item, str): yield item.encode(charset) @@ -54,16 +36,6 @@ def _iter_encoded( yield item -def _clean_accept_ranges(accept_ranges: t.Union[bool, str]) -> str: - if accept_ranges is True: - return "bytes" - elif accept_ranges is False: - return "none" - elif isinstance(accept_ranges, str): - return accept_ranges - raise ValueError("Invalid accept_ranges value") - - class Response(_SansIOResponse): """Represents an outgoing WSGI HTTP response with body, status, and headers. Has properties and methods for using the functionality @@ -123,10 +95,12 @@ class Response(_SansIOResponse): checks. Use :func:`~werkzeug.utils.send_file` instead of setting this manually. + .. versionchanged:: 2.1 + Old ``BaseResponse`` and mixin classes were removed. + .. versionchanged:: 2.0 Combine ``BaseResponse`` and mixins into a single ``Response`` - class. Using the old classes is deprecated and will be removed - in Werkzeug 2.1. + class. .. versionchanged:: 0.5 The ``direct_passthrough`` parameter was added. @@ -165,22 +139,17 @@ class Response(_SansIOResponse): #: Do not set to a plain string or bytes, that will cause sending #: the response to be very inefficient as it will iterate one byte #: at a time. - response: t.Union[t.Iterable[str], t.Iterable[bytes]] + response: t.Iterable[str] | t.Iterable[bytes] def __init__( self, - response: t.Optional[ - t.Union[t.Iterable[bytes], bytes, t.Iterable[str], str] - ] = None, - status: t.Optional[t.Union[int, str, HTTPStatus]] = None, - headers: t.Optional[ - t.Union[ - t.Mapping[str, t.Union[str, int, t.Iterable[t.Union[str, int]]]], - t.Iterable[t.Tuple[str, t.Union[str, int]]], - ] - ] = None, - mimetype: t.Optional[str] = None, - content_type: t.Optional[str] = None, + response: t.Iterable[bytes] | bytes | t.Iterable[str] | str | None = None, + status: int | str | HTTPStatus | None = None, + headers: t.Mapping[str, str | t.Iterable[str]] + | t.Iterable[tuple[str, str]] + | None = None, + mimetype: str | None = None, + content_type: str | None = None, direct_passthrough: bool = False, ) -> None: super().__init__( @@ -196,7 +165,7 @@ class Response(_SansIOResponse): #: :func:`~werkzeug.utils.send_file` instead of setting this #: manually. self.direct_passthrough = direct_passthrough - self._on_close: t.List[t.Callable[[], t.Any]] = [] + self._on_close: list[t.Callable[[], t.Any]] = [] # we set the response after the headers so that if a class changes # the charset attribute, the data is set in the correct charset. @@ -227,8 +196,8 @@ class Response(_SansIOResponse): @classmethod def force_type( - cls, response: "Response", environ: t.Optional["WSGIEnvironment"] = None - ) -> "Response": + cls, response: Response, environ: WSGIEnvironment | None = None + ) -> Response: """Enforce that the WSGI response is a response object of the current type. Werkzeug will use the :class:`Response` internally in many situations like the exceptions. If you call :meth:`get_response` on an @@ -272,8 +241,8 @@ class Response(_SansIOResponse): @classmethod def from_app( - cls, app: "WSGIApplication", environ: "WSGIEnvironment", buffered: bool = False - ) -> "Response": + cls, app: WSGIApplication, environ: WSGIEnvironment, buffered: bool = False + ) -> Response: """Create a new response object from an application output. This works best if you pass it an application that returns a generator all the time. Sometimes applications may use the `write()` callable @@ -290,15 +259,15 @@ class Response(_SansIOResponse): return cls(*run_wsgi_app(app, environ, buffered)) - @typing.overload - def get_data(self, as_text: "te.Literal[False]" = False) -> bytes: + @t.overload + def get_data(self, as_text: t.Literal[False] = False) -> bytes: ... - @typing.overload - def get_data(self, as_text: "te.Literal[True]") -> str: + @t.overload + def get_data(self, as_text: t.Literal[True]) -> str: ... - def get_data(self, as_text: bool = False) -> t.Union[bytes, str]: + def get_data(self, as_text: bool = False) -> bytes | str: """The string representation of the response body. Whenever you call this property the response iterable is encoded and flattened. This can lead to unwanted behavior if you stream big data. @@ -315,23 +284,19 @@ class Response(_SansIOResponse): rv = b"".join(self.iter_encoded()) if as_text: - return rv.decode(self.charset) + return rv.decode(self._charset) return rv - def set_data(self, value: t.Union[bytes, str]) -> None: + def set_data(self, value: bytes | str) -> None: """Sets a new string as response. The value must be a string or bytes. If a string is set it's encoded to the charset of the response (utf-8 by default). .. versionadded:: 0.9 """ - # if a string is set, it's encoded directly so that we - # can set the content length if isinstance(value, str): - value = value.encode(self.charset) - else: - value = bytes(value) + value = value.encode(self._charset) self.response = [value] if self.automatically_set_content_length: self.headers["Content-Length"] = str(len(value)) @@ -342,7 +307,7 @@ class Response(_SansIOResponse): doc="A descriptor that calls :meth:`get_data` and :meth:`set_data`.", ) - def calculate_content_length(self) -> t.Optional[int]: + def calculate_content_length(self) -> int | None: """Returns the content length if available or `None` otherwise.""" try: self._ensure_sequence() @@ -398,12 +363,10 @@ class Response(_SansIOResponse): value of this method is used as application iterator unless :attr:`direct_passthrough` was activated. """ - if __debug__: - _warn_if_string(self.response) # Encode in a separate function so that self.response is fetched # early. This allows us to wrap the response with the return # value from get_app_iter or iter_encoded. - return _iter_encoded(self.response, self.charset) + return _iter_encoded(self.response, self._charset) @property def is_streamed(self) -> bool: @@ -443,7 +406,7 @@ class Response(_SansIOResponse): for func in self._on_close: func() - def __enter__(self) -> "Response": + def __enter__(self) -> Response: return self def __exit__(self, exc_type, exc_value, tb): # type: ignore @@ -463,8 +426,7 @@ class Response(_SansIOResponse): Removed the ``no_etag`` parameter. .. versionchanged:: 2.0 - An ``ETag`` header is added, the ``no_etag`` parameter is - deprecated and will be removed in Werkzeug 2.1. + An ``ETag`` header is always added. .. versionchanged:: 0.6 The ``Content-Length`` header is set. @@ -475,7 +437,7 @@ class Response(_SansIOResponse): self.headers["Content-Length"] = str(sum(map(len, self.response))) self.add_etag() - def get_wsgi_headers(self, environ: "WSGIEnvironment") -> Headers: + def get_wsgi_headers(self, environ: WSGIEnvironment) -> Headers: """This is automatically called right before the response is started and returns headers modified for the given environment. It returns a copy of the headers from the response with some modifications applied @@ -500,9 +462,9 @@ class Response(_SansIOResponse): object. """ headers = Headers(self.headers) - location: t.Optional[str] = None - content_location: t.Optional[str] = None - content_length: t.Optional[t.Union[str, int]] = None + location: str | None = None + content_location: str | None = None + content_length: str | int | None = None status = self.status_code # iterate over the headers to find all values in one go. Because @@ -517,24 +479,19 @@ class Response(_SansIOResponse): elif ikey == "content-length": content_length = value - # make sure the location header is an absolute URL if location is not None: - old_location = location - if isinstance(location, str): - # Safe conversion is necessary here as we might redirect - # to a broken URI scheme (for instance itms-services). - location = iri_to_uri(location, safe_conversion=True) + location = _invalid_iri_to_uri(location) if self.autocorrect_location_header: + # Make the location header an absolute URL. current_url = get_current_url(environ, strip_querystring=True) - if isinstance(current_url, str): - current_url = iri_to_uri(current_url) - location = url_join(current_url, location) - if location != old_location: - headers["Location"] = location + current_url = iri_to_uri(current_url) + location = urljoin(current_url, location) + + headers["Location"] = location # make sure the content location is a URL - if content_location is not None and isinstance(content_location, str): + if content_location is not None: headers["Content-Location"] = iri_to_uri(content_location) if 100 <= status < 200 or status == 204: @@ -557,18 +514,12 @@ class Response(_SansIOResponse): and status not in (204, 304) and not (100 <= status < 200) ): - try: - content_length = sum(len(_to_bytes(x, "ascii")) for x in self.response) - except UnicodeError: - # Something other than bytes, can't safely figure out - # the length of the response. - pass - else: - headers["Content-Length"] = str(content_length) + content_length = sum(len(x) for x in self.iter_encoded()) + headers["Content-Length"] = str(content_length) return headers - def get_app_iter(self, environ: "WSGIEnvironment") -> t.Iterable[bytes]: + def get_app_iter(self, environ: WSGIEnvironment) -> t.Iterable[bytes]: """Returns the application iterator for the given environ. Depending on the request method and the current status code the return value might be an empty response rather than the one from the response. @@ -590,16 +541,14 @@ class Response(_SansIOResponse): ): iterable: t.Iterable[bytes] = () elif self.direct_passthrough: - if __debug__: - _warn_if_string(self.response) return self.response # type: ignore else: iterable = self.iter_encoded() return ClosingIterator(iterable, self.close) def get_wsgi_response( - self, environ: "WSGIEnvironment" - ) -> t.Tuple[t.Iterable[bytes], str, t.List[t.Tuple[str, str]]]: + self, environ: WSGIEnvironment + ) -> tuple[t.Iterable[bytes], str, list[tuple[str, str]]]: """Returns the final WSGI response as tuple. The first item in the tuple is the application iterator, the second the status and the third the list of headers. The response returned is created @@ -617,7 +566,7 @@ class Response(_SansIOResponse): return app_iter, self.status, headers.to_wsgi_list() def __call__( - self, environ: "WSGIEnvironment", start_response: "StartResponse" + self, environ: WSGIEnvironment, start_response: StartResponse ) -> t.Iterable[bytes]: """Process this response as WSGI application. @@ -637,7 +586,7 @@ class Response(_SansIOResponse): json_module = json @property - def json(self) -> t.Optional[t.Any]: + def json(self) -> t.Any | None: """The parsed JSON data if :attr:`mimetype` indicates JSON (:mimetype:`application/json`, see :attr:`is_json`). @@ -646,14 +595,14 @@ class Response(_SansIOResponse): return self.get_json() @t.overload - def get_json(self, force: bool = ..., silent: "te.Literal[False]" = ...) -> t.Any: + def get_json(self, force: bool = ..., silent: t.Literal[False] = ...) -> t.Any: ... @t.overload - def get_json(self, force: bool = ..., silent: bool = ...) -> t.Optional[t.Any]: + def get_json(self, force: bool = ..., silent: bool = ...) -> t.Any | None: ... - def get_json(self, force: bool = False, silent: bool = False) -> t.Optional[t.Any]: + def get_json(self, force: bool = False, silent: bool = False) -> t.Any | None: """Parse :attr:`data` as JSON. Useful during testing. If the mimetype does not indicate JSON @@ -682,7 +631,7 @@ class Response(_SansIOResponse): # Stream @cached_property - def stream(self) -> "ResponseStream": + def stream(self) -> ResponseStream: """The response iterable as write-only stream.""" return ResponseStream(self) @@ -691,7 +640,7 @@ class Response(_SansIOResponse): if self.status_code == 206: self.response = _RangeWrapper(self.response, start, length) # type: ignore - def _is_range_request_processable(self, environ: "WSGIEnvironment") -> bool: + def _is_range_request_processable(self, environ: WSGIEnvironment) -> bool: """Return ``True`` if `Range` header is present and if underlying resource is considered unchanged when compared with `If-Range` header. """ @@ -708,9 +657,9 @@ class Response(_SansIOResponse): def _process_range_request( self, - environ: "WSGIEnvironment", - complete_length: t.Optional[int] = None, - accept_ranges: t.Optional[t.Union[bool, str]] = None, + environ: WSGIEnvironment, + complete_length: int | None, + accept_ranges: bool | str, ) -> bool: """Handle Range Request related headers (RFC7233). If `Accept-Ranges` header is valid, and Range Request is processable, we set the headers @@ -728,13 +677,16 @@ class Response(_SansIOResponse): from ..exceptions import RequestedRangeNotSatisfiable if ( - accept_ranges is None + not accept_ranges or complete_length is None or complete_length == 0 or not self._is_range_request_processable(environ) ): return False + if accept_ranges is True: + accept_ranges = "bytes" + parsed_range = parse_range_header(environ.get("HTTP_RANGE")) if parsed_range is None: @@ -747,7 +699,7 @@ class Response(_SansIOResponse): raise RequestedRangeNotSatisfiable(complete_length) content_length = range_tuple[1] - range_tuple[0] - self.headers["Content-Length"] = content_length + self.headers["Content-Length"] = str(content_length) self.headers["Accept-Ranges"] = accept_ranges self.content_range = content_range_header # type: ignore self.status_code = 206 @@ -756,10 +708,10 @@ class Response(_SansIOResponse): def make_conditional( self, - request_or_environ: t.Union["WSGIEnvironment", "Request"], - accept_ranges: t.Union[bool, str] = False, - complete_length: t.Optional[int] = None, - ) -> "Response": + request_or_environ: WSGIEnvironment | Request, + accept_ranges: bool | str = False, + complete_length: int | None = None, + ) -> Response: """Make the response conditional to the request. This method works best if an etag was defined for the response already. The `add_etag` method can be used to do that. If called without etag just the date @@ -785,8 +737,7 @@ class Response(_SansIOResponse): :param accept_ranges: This parameter dictates the value of `Accept-Ranges` header. If ``False`` (default), the header is not set. If ``True``, it will be set - to ``"bytes"``. If ``None``, it will be set to - ``"none"``. If it's a string, it will use this + to ``"bytes"``. If it's a string, it will use this value. :param complete_length: Will be used only in valid Range Requests. It will set `Content-Range` complete length @@ -808,7 +759,6 @@ class Response(_SansIOResponse): # wsgiref. if "date" not in self.headers: self.headers["Date"] = http_date() - accept_ranges = _clean_accept_ranges(accept_ranges) is206 = self._process_range_request(environ, complete_length, accept_ranges) if not is206 and not is_resource_modified( environ, @@ -826,7 +776,7 @@ class Response(_SansIOResponse): ): length = self.calculate_content_length() if length is not None: - self.headers["Content-Length"] = length + self.headers["Content-Length"] = str(length) return self def add_etag(self, overwrite: bool = False, weak: bool = False) -> None: @@ -882,4 +832,4 @@ class ResponseStream: @property def encoding(self) -> str: - return self.response.charset + return self.response._charset diff --git a/contrib/python/Werkzeug/py3/werkzeug/wsgi.py b/contrib/python/Werkzeug/py3/werkzeug/wsgi.py index d74430d8bbb..6061e114114 100644 --- a/contrib/python/Werkzeug/py3/werkzeug/wsgi.py +++ b/contrib/python/Werkzeug/py3/werkzeug/wsgi.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import io import re import typing as t @@ -9,20 +11,17 @@ from itertools import chain from ._internal import _make_encode_wrapper from ._internal import _to_bytes from ._internal import _to_str +from .exceptions import ClientDisconnected +from .exceptions import RequestEntityTooLarge from .sansio import utils as _sansio_utils from .sansio.utils import host_is_trusted # noqa: F401 # Imported as part of API -from .urls import _URLTuple -from .urls import uri_to_iri -from .urls import url_join -from .urls import url_parse -from .urls import url_quote if t.TYPE_CHECKING: from _typeshed.wsgi import WSGIApplication from _typeshed.wsgi import WSGIEnvironment -def responder(f: t.Callable[..., "WSGIApplication"]) -> "WSGIApplication": +def responder(f: t.Callable[..., WSGIApplication]) -> WSGIApplication: """Marks a function as responder. Decorate a function with it and it will automatically call the return value as WSGI application. @@ -36,11 +35,11 @@ def responder(f: t.Callable[..., "WSGIApplication"]) -> "WSGIApplication": def get_current_url( - environ: "WSGIEnvironment", + environ: WSGIEnvironment, root_only: bool = False, strip_querystring: bool = False, host_only: bool = False, - trusted_hosts: t.Optional[t.Iterable[str]] = None, + trusted_hosts: t.Iterable[str] | None = None, ) -> str: """Recreate the URL for a request from the parts in a WSGI environment. @@ -74,15 +73,15 @@ def get_current_url( def _get_server( - environ: "WSGIEnvironment", -) -> t.Optional[t.Tuple[str, t.Optional[int]]]: + environ: WSGIEnvironment, +) -> tuple[str, int | None] | None: name = environ.get("SERVER_NAME") if name is None: return None try: - port: t.Optional[int] = int(environ.get("SERVER_PORT", None)) + port: int | None = int(environ.get("SERVER_PORT", None)) except (TypeError, ValueError): # unix socket port = None @@ -91,7 +90,7 @@ def _get_server( def get_host( - environ: "WSGIEnvironment", trusted_hosts: t.Optional[t.Iterable[str]] = None + environ: WSGIEnvironment, trusted_hosts: t.Iterable[str] | None = None ) -> str: """Return the host for the given WSGI environment. @@ -118,337 +117,128 @@ def get_host( ) -def get_content_length(environ: "WSGIEnvironment") -> t.Optional[int]: - """Returns the content length from the WSGI environment as - integer. If it's not available or chunked transfer encoding is used, - ``None`` is returned. +def get_content_length(environ: WSGIEnvironment) -> int | None: + """Return the ``Content-Length`` header value as an int. If the header is not given + or the ``Transfer-Encoding`` header is ``chunked``, ``None`` is returned to indicate + a streaming request. If the value is not an integer, or negative, 0 is returned. - .. versionadded:: 0.9 + :param environ: The WSGI environ to get the content length from. - :param environ: the WSGI environ to fetch the content length from. + .. versionadded:: 0.9 """ return _sansio_utils.get_content_length( http_content_length=environ.get("CONTENT_LENGTH"), - http_transfer_encoding=environ.get("HTTP_TRANSFER_ENCODING", ""), + http_transfer_encoding=environ.get("HTTP_TRANSFER_ENCODING"), ) def get_input_stream( - environ: "WSGIEnvironment", safe_fallback: bool = True + environ: WSGIEnvironment, + safe_fallback: bool = True, + max_content_length: int | None = None, ) -> t.IO[bytes]: - """Returns the input stream from the WSGI environment and wraps it - in the most sensible way possible. The stream returned is not the - raw WSGI stream in most cases but one that is safe to read from - without taking into account the content length. - - If content length is not set, the stream will be empty for safety reasons. - If the WSGI server supports chunked or infinite streams, it should set - the ``wsgi.input_terminated`` value in the WSGI environ to indicate that. - - .. versionadded:: 0.9 - - :param environ: the WSGI environ to fetch the stream from. - :param safe_fallback: use an empty stream as a safe fallback when the - content length is not set. Disabling this allows infinite streams, - which can be a denial-of-service risk. - """ - stream = t.cast(t.IO[bytes], environ["wsgi.input"]) - content_length = get_content_length(environ) + """Return the WSGI input stream, wrapped so that it may be read safely without going + past the ``Content-Length`` header value or ``max_content_length``. - # A wsgi extension that tells us if the input is terminated. In - # that case we return the stream unchanged as we know we can safely - # read it until the end. - if environ.get("wsgi.input_terminated"): - return stream + If ``Content-Length`` exceeds ``max_content_length``, a + :exc:`RequestEntityTooLarge`` ``413 Content Too Large`` error is raised. - # If the request doesn't specify a content length, returning the stream is - # potentially dangerous because it could be infinite, malicious or not. If - # safe_fallback is true, return an empty stream instead for safety. - if content_length is None: - return io.BytesIO() if safe_fallback else stream + If the WSGI server sets ``environ["wsgi.input_terminated"]``, it indicates that the + server handles terminating the stream, so it is safe to read directly. For example, + a server that knows how to handle chunked requests safely would set this. - # Otherwise limit the stream to the content length - return t.cast(t.IO[bytes], LimitedStream(stream, content_length)) + If ``max_content_length`` is set, it can be enforced on streams if + ``wsgi.input_terminated`` is set. Otherwise, an empty stream is returned unless the + user explicitly disables this safe fallback. + If the limit is reached before the underlying stream is exhausted (such as a file + that is too large, or an infinite stream), the remaining contents of the stream + cannot be read safely. Depending on how the server handles this, clients may show a + "connection reset" failure instead of seeing the 413 response. -def get_query_string(environ: "WSGIEnvironment") -> str: - """Returns the ``QUERY_STRING`` from the WSGI environment. This also - takes care of the WSGI decoding dance. The string returned will be - restricted to ASCII characters. + :param environ: The WSGI environ containing the stream. + :param safe_fallback: Return an empty stream when ``Content-Length`` is not set. + Disabling this allows infinite streams, which can be a denial-of-service risk. + :param max_content_length: The maximum length that content-length or streaming + requests may not exceed. - :param environ: WSGI environment to get the query string from. + .. versionchanged:: 2.3.2 + ``max_content_length`` is only applied to streaming requests if the server sets + ``wsgi.input_terminated``. - .. deprecated:: 2.2 - Will be removed in Werkzeug 2.3. + .. versionchanged:: 2.3 + Check ``max_content_length`` and raise an error if it is exceeded. .. versionadded:: 0.9 """ - warnings.warn( - "'get_query_string' is deprecated and will be removed in Werkzeug 2.3.", - DeprecationWarning, - stacklevel=2, - ) - qs = environ.get("QUERY_STRING", "").encode("latin1") - # QUERY_STRING really should be ascii safe but some browsers - # will send us some unicode stuff (I am looking at you IE). - # In that case we want to urllib quote it badly. - return url_quote(qs, safe=":&%=+$!*'(),") + stream = t.cast(t.IO[bytes], environ["wsgi.input"]) + content_length = get_content_length(environ) + if content_length is not None and max_content_length is not None: + if content_length > max_content_length: + raise RequestEntityTooLarge() -def get_path_info( - environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" -) -> str: - """Return the ``PATH_INFO`` from the WSGI environment and decode it - unless ``charset`` is ``None``. + # A WSGI server can set this to indicate that it terminates the input stream. In + # that case the stream is safe without wrapping, or can enforce a max length. + if "wsgi.input_terminated" in environ: + if max_content_length is not None: + # If this is moved above, it can cause the stream to hang if a read attempt + # is made when the client sends no data. For example, the development server + # does not handle buffering except for chunked encoding. + return t.cast( + t.IO[bytes], LimitedStream(stream, max_content_length, is_max=True) + ) - :param environ: WSGI environment to get the path from. - :param charset: The charset for the path info, or ``None`` if no - decoding should be performed. - :param errors: The decoding error handling. + return stream - .. versionadded:: 0.9 - """ - path = environ.get("PATH_INFO", "").encode("latin1") - return _to_str(path, charset, errors, allow_none_charset=True) # type: ignore + # No limit given, return an empty stream unless the user explicitly allows the + # potentially infinite stream. An infinite stream is dangerous if it's not expected, + # as it can tie up a worker indefinitely. + if content_length is None: + return io.BytesIO() if safe_fallback else stream + return t.cast(t.IO[bytes], LimitedStream(stream, content_length)) -def get_script_name( - environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" + +def get_path_info( + environ: WSGIEnvironment, + charset: t.Any = ..., + errors: str | None = None, ) -> str: - """Return the ``SCRIPT_NAME`` from the WSGI environment and decode - it unless `charset` is set to ``None``. + """Return ``PATH_INFO`` from the WSGI environment. :param environ: WSGI environment to get the path from. - :param charset: The charset for the path, or ``None`` if no decoding - should be performed. - :param errors: The decoding error handling. - .. deprecated:: 2.2 - Will be removed in Werkzeug 2.3. + .. versionchanged:: 2.3 + The ``charset`` and ``errors`` parameters are deprecated and will be removed in + Werkzeug 3.0. .. versionadded:: 0.9 """ - warnings.warn( - "'get_script_name' is deprecated and will be removed in Werkzeug 2.3.", - DeprecationWarning, - stacklevel=2, - ) - path = environ.get("SCRIPT_NAME", "").encode("latin1") - return _to_str(path, charset, errors, allow_none_charset=True) # type: ignore - - -def pop_path_info( - environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" -) -> t.Optional[str]: - """Removes and returns the next segment of `PATH_INFO`, pushing it onto - `SCRIPT_NAME`. Returns `None` if there is nothing left on `PATH_INFO`. - - If the `charset` is set to `None` bytes are returned. - - If there are empty segments (``'/foo//bar``) these are ignored but - properly pushed to the `SCRIPT_NAME`: - - >>> env = {'SCRIPT_NAME': '/foo', 'PATH_INFO': '/a/b'} - >>> pop_path_info(env) - 'a' - >>> env['SCRIPT_NAME'] - '/foo/a' - >>> pop_path_info(env) - 'b' - >>> env['SCRIPT_NAME'] - '/foo/a/b' - - .. deprecated:: 2.2 - Will be removed in Werkzeug 2.3. - - .. versionadded:: 0.5 - - .. versionchanged:: 0.9 - The path is now decoded and a charset and encoding - parameter can be provided. - - :param environ: the WSGI environment that is modified. - :param charset: The ``encoding`` parameter passed to - :func:`bytes.decode`. - :param errors: The ``errors`` paramater passed to - :func:`bytes.decode`. - """ - warnings.warn( - "'pop_path_info' is deprecated and will be removed in Werkzeug 2.3.", - DeprecationWarning, - stacklevel=2, - ) - - path = environ.get("PATH_INFO") - if not path: - return None - - script_name = environ.get("SCRIPT_NAME", "") - - # shift multiple leading slashes over - old_path = path - path = path.lstrip("/") - if path != old_path: - script_name += "/" * (len(old_path) - len(path)) + if charset is not ...: + warnings.warn( + "The 'charset' parameter is deprecated and will be removed" + " in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) - if "/" not in path: - environ["PATH_INFO"] = "" - environ["SCRIPT_NAME"] = script_name + path - rv = path.encode("latin1") + if charset is None: + charset = "utf-8" else: - segment, path = path.split("/", 1) - environ["PATH_INFO"] = f"/{path}" - environ["SCRIPT_NAME"] = script_name + segment - rv = segment.encode("latin1") - - return _to_str(rv, charset, errors, allow_none_charset=True) # type: ignore - + charset = "utf-8" -def peek_path_info( - environ: "WSGIEnvironment", charset: str = "utf-8", errors: str = "replace" -) -> t.Optional[str]: - """Returns the next segment on the `PATH_INFO` or `None` if there - is none. Works like :func:`pop_path_info` without modifying the - environment: - - >>> env = {'SCRIPT_NAME': '/foo', 'PATH_INFO': '/a/b'} - >>> peek_path_info(env) - 'a' - >>> peek_path_info(env) - 'a' - - If the `charset` is set to `None` bytes are returned. - - .. deprecated:: 2.2 - Will be removed in Werkzeug 2.3. - - .. versionadded:: 0.5 - - .. versionchanged:: 0.9 - The path is now decoded and a charset and encoding - parameter can be provided. - - :param environ: the WSGI environment that is checked. - """ - warnings.warn( - "'peek_path_info' is deprecated and will be removed in Werkzeug 2.3.", - DeprecationWarning, - stacklevel=2, - ) - - segments = environ.get("PATH_INFO", "").lstrip("/").split("/", 1) - if segments: - return _to_str( # type: ignore - segments[0].encode("latin1"), charset, errors, allow_none_charset=True + if errors is not None: + warnings.warn( + "The 'errors' parameter is deprecated and will be removed in Werkzeug 3.0", + DeprecationWarning, + stacklevel=2, ) - return None - - -def extract_path_info( - environ_or_baseurl: t.Union[str, "WSGIEnvironment"], - path_or_url: t.Union[str, _URLTuple], - charset: str = "utf-8", - errors: str = "werkzeug.url_quote", - collapse_http_schemes: bool = True, -) -> t.Optional[str]: - """Extracts the path info from the given URL (or WSGI environment) and - path. The path info returned is a string. The URLs might also be IRIs. - - If the path info could not be determined, `None` is returned. - - Some examples: - - >>> extract_path_info('http://example.com/app', '/app/hello') - '/hello' - >>> extract_path_info('http://example.com/app', - ... 'https://example.com/app/hello') - '/hello' - >>> extract_path_info('http://example.com/app', - ... 'https://example.com/app/hello', - ... collapse_http_schemes=False) is None - True - - Instead of providing a base URL you can also pass a WSGI environment. - - :param environ_or_baseurl: a WSGI environment dict, a base URL or - base IRI. This is the root of the - application. - :param path_or_url: an absolute path from the server root, a - relative path (in which case it's the path info) - or a full URL. - :param charset: the charset for byte data in URLs - :param errors: the error handling on decode - :param collapse_http_schemes: if set to `False` the algorithm does - not assume that http and https on the - same server point to the same - resource. - - .. deprecated:: 2.2 - Will be removed in Werkzeug 2.3. - - .. versionchanged:: 0.15 - The ``errors`` parameter defaults to leaving invalid bytes - quoted instead of replacing them. - - .. versionadded:: 0.6 - - """ - warnings.warn( - "'extract_path_info' is deprecated and will be removed in Werkzeug 2.3.", - DeprecationWarning, - stacklevel=2, - ) - - def _normalize_netloc(scheme: str, netloc: str) -> str: - parts = netloc.split("@", 1)[-1].split(":", 1) - port: t.Optional[str] - - if len(parts) == 2: - netloc, port = parts - if (scheme == "http" and port == "80") or ( - scheme == "https" and port == "443" - ): - port = None - else: - netloc = parts[0] - port = None - - if port is not None: - netloc += f":{port}" - - return netloc - - # make sure whatever we are working on is a IRI and parse it - path = uri_to_iri(path_or_url, charset, errors) - if isinstance(environ_or_baseurl, dict): - environ_or_baseurl = get_current_url(environ_or_baseurl, root_only=True) - base_iri = uri_to_iri(environ_or_baseurl, charset, errors) - base_scheme, base_netloc, base_path = url_parse(base_iri)[:3] - cur_scheme, cur_netloc, cur_path = url_parse(url_join(base_iri, path))[:3] - - # normalize the network location - base_netloc = _normalize_netloc(base_scheme, base_netloc) - cur_netloc = _normalize_netloc(cur_scheme, cur_netloc) - - # is that IRI even on a known HTTP scheme? - if collapse_http_schemes: - for scheme in base_scheme, cur_scheme: - if scheme not in ("http", "https"): - return None else: - if not (base_scheme in ("http", "https") and base_scheme == cur_scheme): - return None + errors = "replace" - # are the netlocs compatible? - if base_netloc != cur_netloc: - return None - - # are we below the application path? - base_path = base_path.rstrip("/") - if not cur_path.startswith(base_path): - return None - - return f"/{cur_path[len(base_path) :].lstrip('/')}" + path = environ.get("PATH_INFO", "").encode("latin1") + return path.decode(charset, errors) # type: ignore[no-any-return] class ClosingIterator: @@ -476,9 +266,8 @@ class ClosingIterator: def __init__( self, iterable: t.Iterable[bytes], - callbacks: t.Optional[ - t.Union[t.Callable[[], None], t.Iterable[t.Callable[[], None]]] - ] = None, + callbacks: None + | (t.Callable[[], None] | t.Iterable[t.Callable[[], None]]) = None, ) -> None: iterator = iter(iterable) self._next = t.cast(t.Callable[[], bytes], partial(next, iterator)) @@ -493,7 +282,7 @@ class ClosingIterator: callbacks.insert(0, iterable_close) self._callbacks = callbacks - def __iter__(self) -> "ClosingIterator": + def __iter__(self) -> ClosingIterator: return self def __next__(self) -> bytes: @@ -505,7 +294,7 @@ class ClosingIterator: def wrap_file( - environ: "WSGIEnvironment", file: t.IO[bytes], buffer_size: int = 8192 + environ: WSGIEnvironment, file: t.IO[bytes], buffer_size: int = 8192 ) -> t.Iterable[bytes]: """Wraps a file. This uses the WSGI server's file wrapper if available or otherwise the generic :class:`FileWrapper`. @@ -564,12 +353,12 @@ class FileWrapper: if hasattr(self.file, "seek"): self.file.seek(*args) - def tell(self) -> t.Optional[int]: + def tell(self) -> int | None: if hasattr(self.file, "tell"): return self.file.tell() return None - def __iter__(self) -> "FileWrapper": + def __iter__(self) -> FileWrapper: return self def __next__(self) -> bytes: @@ -598,9 +387,9 @@ class _RangeWrapper: def __init__( self, - iterable: t.Union[t.Iterable[bytes], t.IO[bytes]], + iterable: t.Iterable[bytes] | t.IO[bytes], start_byte: int = 0, - byte_range: t.Optional[int] = None, + byte_range: int | None = None, ): self.iterable = iter(iterable) self.byte_range = byte_range @@ -614,7 +403,7 @@ class _RangeWrapper: self.seekable = hasattr(iterable, "seekable") and iterable.seekable() self.end_reached = False - def __iter__(self) -> "_RangeWrapper": + def __iter__(self) -> _RangeWrapper: return self def _next_chunk(self) -> bytes: @@ -626,7 +415,7 @@ class _RangeWrapper: self.end_reached = True raise - def _first_iteration(self) -> t.Tuple[t.Optional[bytes], int]: + def _first_iteration(self) -> tuple[bytes | None, int]: chunk = None if self.seekable: self.iterable.seek(self.start_byte) # type: ignore @@ -667,11 +456,17 @@ class _RangeWrapper: def _make_chunk_iter( - stream: t.Union[t.Iterable[bytes], t.IO[bytes]], - limit: t.Optional[int], + stream: t.Iterable[bytes] | t.IO[bytes], + limit: int | None, buffer_size: int, ) -> t.Iterator[bytes]: """Helper for the line and chunk iter functions.""" + warnings.warn( + "'_make_chunk_iter' is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) + if isinstance(stream, (bytes, bytearray, str)): raise TypeError( "Passed a string or byte object instead of true iterator or stream." @@ -693,8 +488,8 @@ def _make_chunk_iter( def make_line_iter( - stream: t.Union[t.Iterable[bytes], t.IO[bytes]], - limit: t.Optional[int] = None, + stream: t.Iterable[bytes] | t.IO[bytes], + limit: int | None = None, buffer_size: int = 10 * 1024, cap_at_buffer: bool = False, ) -> t.Iterator[bytes]: @@ -710,14 +505,17 @@ def make_line_iter( If you need line-by-line processing it's strongly recommended to iterate over the input stream using this helper function. - .. versionchanged:: 0.8 - This function now ensures that the limit was reached. + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. + + .. versionadded:: 0.11 + added support for the `cap_at_buffer` parameter. .. versionadded:: 0.9 added support for iterators as input stream. - .. versionadded:: 0.11.10 - added support for the `cap_at_buffer` parameter. + .. versionchanged:: 0.8 + This function now ensures that the limit was reached. :param stream: the stream or iterate to iterate over. :param limit: the limit in bytes for the stream. (Usually @@ -729,9 +527,15 @@ def make_line_iter( that the buffer size might be exhausted by a factor of two however. """ + warnings.warn( + "'make_line_iter' is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) _iter = _make_chunk_iter(stream, limit, buffer_size) first_item = next(_iter, "") + if not first_item: return @@ -745,12 +549,12 @@ def make_line_iter( def _iter_basic_lines() -> t.Iterator[bytes]: _join = empty.join - buffer: t.List[bytes] = [] + buffer: list[bytes] = [] while True: new_data = next(_iter, "") if not new_data: break - new_buf: t.List[bytes] = [] + new_buf: list[bytes] = [] buf_size = 0 for item in t.cast( t.Iterator[bytes], chain(buffer, new_data.splitlines(True)) @@ -785,9 +589,9 @@ def make_line_iter( def make_chunk_iter( - stream: t.Union[t.Iterable[bytes], t.IO[bytes]], + stream: t.Iterable[bytes] | t.IO[bytes], separator: bytes, - limit: t.Optional[int] = None, + limit: int | None = None, buffer_size: int = 10 * 1024, cap_at_buffer: bool = False, ) -> t.Iterator[bytes]: @@ -796,13 +600,16 @@ def make_chunk_iter( you should use :func:`make_line_iter` instead as it supports arbitrary newline markers. - .. versionadded:: 0.8 + .. deprecated:: 2.3 + Will be removed in Werkzeug 3.0. - .. versionadded:: 0.9 + .. versionchanged:: 0.11 + added support for the `cap_at_buffer` parameter. + + .. versionchanged:: 0.9 added support for iterators as input stream. - .. versionadded:: 0.11.10 - added support for the `cap_at_buffer` parameter. + .. versionadded:: 0.8 :param stream: the stream or iterate to iterate over. :param separator: the separator that divides chunks. @@ -815,9 +622,15 @@ def make_chunk_iter( that the buffer size might be exhausted by a factor of two however. """ + warnings.warn( + "'make_chunk_iter' is deprecated and will be removed in Werkzeug 3.0.", + DeprecationWarning, + stacklevel=2, + ) _iter = _make_chunk_iter(stream, limit, buffer_size) first_item = next(_iter, b"") + if not first_item: return @@ -831,13 +644,13 @@ def make_chunk_iter( _split = re.compile(b"(" + re.escape(separator) + b")").split _join = b"".join - buffer: t.List[bytes] = [] + buffer: list[bytes] = [] while True: new_data = next(_iter, b"") if not new_data: break chunks = _split(new_data) - new_buf: t.List[bytes] = [] + new_buf: list[bytes] = [] buf_size = 0 for item in chain(buffer, chunks): if item == separator: @@ -861,198 +674,174 @@ def make_chunk_iter( yield _join(buffer) -class LimitedStream(io.IOBase): - """Wraps a stream so that it doesn't read more than n bytes. If the - stream is exhausted and the caller tries to get more bytes from it - :func:`on_exhausted` is called which by default returns an empty - string. The return value of that function is forwarded - to the reader function. So if it returns an empty string - :meth:`read` will return an empty string as well. +class LimitedStream(io.RawIOBase): + """Wrap a stream so that it doesn't read more than a given limit. This is used to + limit ``wsgi.input`` to the ``Content-Length`` header value or + :attr:`.Request.max_content_length`. - The limit however must never be higher than what the stream can - output. Otherwise :meth:`readlines` will try to read past the - limit. + When attempting to read after the limit has been reached, :meth:`on_exhausted` is + called. When the limit is a maximum, this raises :exc:`.RequestEntityTooLarge`. - .. admonition:: Note on WSGI compliance + If reading from the stream returns zero bytes or raises an error, + :meth:`on_disconnect` is called, which raises :exc:`.ClientDisconnected`. When the + limit is a maximum and zero bytes were read, no error is raised, since it may be the + end of the stream. - calls to :meth:`readline` and :meth:`readlines` are not - WSGI compliant because it passes a size argument to the - readline methods. Unfortunately the WSGI PEP is not safely - implementable without a size argument to :meth:`readline` - because there is no EOF marker in the stream. As a result - of that the use of :meth:`readline` is discouraged. + If the limit is reached before the underlying stream is exhausted (such as a file + that is too large, or an infinite stream), the remaining contents of the stream + cannot be read safely. Depending on how the server handles this, clients may show a + "connection reset" failure instead of seeing the 413 response. - For the same reason iterating over the :class:`LimitedStream` - is not portable. It internally calls :meth:`readline`. + :param stream: The stream to read from. Must be a readable binary IO object. + :param limit: The limit in bytes to not read past. Should be either the + ``Content-Length`` header value or ``request.max_content_length``. + :param is_max: Whether the given ``limit`` is ``request.max_content_length`` instead + of the ``Content-Length`` header value. This changes how exhausted and + disconnect events are handled. - We strongly suggest using :meth:`read` only or using the - :func:`make_line_iter` which safely iterates line-based - over a WSGI input stream. + .. versionchanged:: 2.3 + Handle ``max_content_length`` differently than ``Content-Length``. - :param stream: the stream to wrap. - :param limit: the limit for the stream, must not be longer than - what the string can provide if the stream does not - end with `EOF` (like `wsgi.input`) + .. versionchanged:: 2.3 + Implements ``io.RawIOBase`` rather than ``io.IOBase``. """ - def __init__(self, stream: t.IO[bytes], limit: int) -> None: - self._read = stream.read - self._readline = stream.readline + def __init__(self, stream: t.IO[bytes], limit: int, is_max: bool = False) -> None: + self._stream = stream self._pos = 0 self.limit = limit - - def __iter__(self) -> "LimitedStream": - return self + self._limit_is_max = is_max @property def is_exhausted(self) -> bool: - """If the stream is exhausted this attribute is `True`.""" + """Whether the current stream position has reached the limit.""" return self._pos >= self.limit - def on_exhausted(self) -> bytes: - """This is called when the stream tries to read past the limit. - The return value of this function is returned from the reading - function. - """ - # Read null bytes from the stream so that we get the - # correct end of stream marker. - return self._read(0) + def on_exhausted(self) -> None: + """Called when attempting to read after the limit has been reached. - def on_disconnect(self) -> bytes: - """What should happen if a disconnect is detected? The return - value of this function is returned from read functions in case - the client went away. By default a - :exc:`~werkzeug.exceptions.ClientDisconnected` exception is raised. - """ - from .exceptions import ClientDisconnected + The default behavior is to do nothing, unless the limit is a maximum, in which + case it raises :exc:`.RequestEntityTooLarge`. - raise ClientDisconnected() + .. versionchanged:: 2.3 + Raises ``RequestEntityTooLarge`` if the limit is a maximum. - def _exhaust_chunks(self, chunk_size: int = 1024 * 64) -> t.Iterator[bytes]: - """Exhaust the stream by reading until the limit is reached or the client - disconnects, yielding each chunk. + .. versionchanged:: 2.3 + Any return value is ignored. + """ + if self._limit_is_max: + raise RequestEntityTooLarge() + + def on_disconnect(self, error: Exception | None = None) -> None: + """Called when an attempted read receives zero bytes before the limit was + reached. This indicates that the client disconnected before sending the full + request body. - :param chunk_size: How many bytes to read at a time. + The default behavior is to raise :exc:`.ClientDisconnected`, unless the limit is + a maximum and no error was raised. - :meta private: + .. versionchanged:: 2.3 + Added the ``error`` parameter. Do nothing if the limit is a maximum and no + error was raised. - .. versionadded:: 2.2.3 + .. versionchanged:: 2.3 + Any return value is ignored. """ - to_read = self.limit - self._pos + if not self._limit_is_max or error is not None: + raise ClientDisconnected() - while to_read > 0: - chunk = self.read(min(to_read, chunk_size)) - yield chunk - to_read -= len(chunk) + # If the limit is a maximum, then we may have read zero bytes because the + # streaming body is complete. There's no way to distinguish that from the + # client disconnecting early. - def exhaust(self, chunk_size: int = 1024 * 64) -> None: + def exhaust(self) -> bytes: """Exhaust the stream by reading until the limit is reached or the client - disconnects, discarding the data. + disconnects, returning the remaining data. - :param chunk_size: How many bytes to read at a time. + .. versionchanged:: 2.3 + Return the remaining data. .. versionchanged:: 2.2.3 Handle case where wrapped stream returns fewer bytes than requested. """ - for _ in self._exhaust_chunks(chunk_size): - pass + if not self.is_exhausted: + return self.readall() - def read(self, size: t.Optional[int] = None) -> bytes: - """Read up to ``size`` bytes from the underlying stream. If size is not - provided, read until the limit. + return b"" - If the limit is reached, :meth:`on_exhausted` is called, which returns empty - bytes. + def readinto(self, b: bytearray) -> int | None: # type: ignore[override] + size = len(b) + remaining = self.limit - self._pos - If no bytes are read and the limit is not reached, or if an error occurs during - the read, :meth:`on_disconnect` is called, which raises - :exc:`.ClientDisconnected`. + if remaining <= 0: + self.on_exhausted() + return 0 - :param size: The number of bytes to read. ``None``, default, reads until the - limit is reached. + if hasattr(self._stream, "readinto"): + # Use stream.readinto if it's available. + if size <= remaining: + # The size fits in the remaining limit, use the buffer directly. + try: + out_size: int | None = self._stream.readinto(b) + except (OSError, ValueError) as e: + self.on_disconnect(error=e) + return 0 + else: + # Use a temp buffer with the remaining limit as the size. + temp_b = bytearray(remaining) - .. versionchanged:: 2.2.3 - Handle case where wrapped stream returns fewer bytes than requested. - """ - if self._pos >= self.limit: - return self.on_exhausted() + try: + out_size = self._stream.readinto(temp_b) + except (OSError, ValueError) as e: + self.on_disconnect(error=e) + return 0 - if size is None or size == -1: # -1 is for consistency with file - # Keep reading from the wrapped stream until the limit is reached. Can't - # rely on stream.read(size) because it's not guaranteed to return size. - buf = bytearray() + if out_size: + b[:out_size] = temp_b + else: + # WSGI requires that stream.read is available. + try: + data = self._stream.read(min(size, remaining)) + except (OSError, ValueError) as e: + self.on_disconnect(error=e) + return 0 - for chunk in self._exhaust_chunks(): - buf.extend(chunk) + out_size = len(data) + b[:out_size] = data - return bytes(buf) + if not out_size: + # Read zero bytes from the stream. + self.on_disconnect() + return 0 - to_read = min(self.limit - self._pos, size) + self._pos += out_size + return out_size - try: - read = self._read(to_read) - except (OSError, ValueError): - return self.on_disconnect() + def readall(self) -> bytes: + if self.is_exhausted: + self.on_exhausted() + return b"" - if to_read and not len(read): - # If no data was read, treat it as a disconnect. As long as some data was - # read, a subsequent call can still return more before reaching the limit. - return self.on_disconnect() + out = bytearray() - self._pos += len(read) - return read + # The parent implementation uses "while True", which results in an extra read. + while not self.is_exhausted: + data = self.read(1024 * 64) - def readline(self, size: t.Optional[int] = None) -> bytes: - """Reads one line from the stream.""" - if self._pos >= self.limit: - return self.on_exhausted() - if size is None: - size = self.limit - self._pos - else: - size = min(size, self.limit - self._pos) - try: - line = self._readline(size) - except (ValueError, OSError): - return self.on_disconnect() - if size and not line: - return self.on_disconnect() - self._pos += len(line) - return line - - def readlines(self, size: t.Optional[int] = None) -> t.List[bytes]: - """Reads a file into a list of strings. It calls :meth:`readline` - until the file is read to the end. It does support the optional - `size` argument if the underlying stream supports it for - `readline`. - """ - last_pos = self._pos - result = [] - if size is not None: - end = min(self.limit, last_pos + size) - else: - end = self.limit - while True: - if size is not None: - size -= last_pos - self._pos - if self._pos >= end: + # Stream may return empty before a max limit is reached. + if not data: break - result.append(self.readline(size)) - if size is not None: - last_pos = self._pos - return result + + out.extend(data) + + return bytes(out) def tell(self) -> int: - """Returns the position of the stream. + """Return the current stream position. .. versionadded:: 0.9 """ return self._pos - def __next__(self) -> bytes: - line = self.readline() - if not line: - raise StopIteration() - return line - def readable(self) -> bool: return True diff --git a/contrib/python/Werkzeug/py3/ya.make b/contrib/python/Werkzeug/py3/ya.make index 0d9e451aa94..dbf2562c4fb 100644 --- a/contrib/python/Werkzeug/py3/ya.make +++ b/contrib/python/Werkzeug/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(2.2.3) +VERSION(2.3.8) LICENSE(BSD-3-Clause) @@ -21,8 +21,26 @@ PY_SRCS( werkzeug/__init__.py werkzeug/_internal.py werkzeug/_reloader.py - werkzeug/datastructures.py - werkzeug/datastructures.pyi + werkzeug/datastructures/__init__.py + werkzeug/datastructures/accept.py + werkzeug/datastructures/accept.pyi + werkzeug/datastructures/auth.py + werkzeug/datastructures/cache_control.py + werkzeug/datastructures/cache_control.pyi + werkzeug/datastructures/csp.py + werkzeug/datastructures/csp.pyi + werkzeug/datastructures/etag.py + werkzeug/datastructures/etag.pyi + werkzeug/datastructures/file_storage.py + werkzeug/datastructures/file_storage.pyi + werkzeug/datastructures/headers.py + werkzeug/datastructures/headers.pyi + werkzeug/datastructures/mixins.py + werkzeug/datastructures/mixins.pyi + werkzeug/datastructures/range.py + werkzeug/datastructures/range.pyi + werkzeug/datastructures/structures.py + werkzeug/datastructures/structures.pyi werkzeug/debug/__init__.py werkzeug/debug/console.py werkzeug/debug/repr.py |
