This commit is contained in:
2025-05-22 21:22:15 +02:00
parent 3d57f842f9
commit 97cb9c8703
156 changed files with 1205 additions and 6603 deletions

View File

@@ -1,6 +1,25 @@
from __future__ import annotations
import typing as t
from .serving import run_simple as run_simple
from .test import Client as Client
from .wrappers import Request as Request
from .wrappers import Response as Response
__version__ = "2.3.7"
def __getattr__(name: str) -> t.Any:
if name == "__version__":
import importlib.metadata
import warnings
warnings.warn(
"The '__version__' attribute is deprecated and will be removed in"
" Werkzeug 3.1. Use feature detection or"
" 'importlib.metadata.version(\"werkzeug\")' instead.",
DeprecationWarning,
stacklevel=2,
)
return importlib.metadata.version("werkzeug")
raise AttributeError(name)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import logging
import operator
import re
import sys
import typing as t
@@ -26,102 +25,12 @@ class _Missing:
_missing = _Missing()
@t.overload
def _make_encode_wrapper(reference: str) -> t.Callable[[str], str]:
...
def _wsgi_decoding_dance(s: str) -> str:
return s.encode("latin1").decode(errors="replace")
@t.overload
def _make_encode_wrapper(reference: bytes) -> t.Callable[[str], bytes]:
...
def _make_encode_wrapper(reference: t.AnyStr) -> t.Callable[[str], t.AnyStr]:
"""Create a function that will be called with a string argument. If
the reference is bytes, values will be encoded to bytes.
"""
if isinstance(reference, str):
return lambda x: x
return operator.methodcaller("encode", "latin1")
def _check_str_tuple(value: tuple[t.AnyStr, ...]) -> None:
"""Ensure tuple items are all strings or all bytes."""
if not value:
return
item_type = str if isinstance(value[0], str) else bytes
if any(not isinstance(item, item_type) for item in value):
raise TypeError(f"Cannot mix str and bytes arguments (got {value!r})")
_default_encoding = sys.getdefaultencoding()
def _to_bytes(
x: str | bytes, charset: str = _default_encoding, errors: str = "strict"
) -> bytes:
if x is None or isinstance(x, bytes):
return x
if isinstance(x, (bytearray, memoryview)):
return bytes(x)
if isinstance(x, str):
return x.encode(charset, errors)
raise TypeError("Expected bytes")
@t.overload
def _to_str( # type: ignore
x: None,
charset: str | None = ...,
errors: str = ...,
allow_none_charset: bool = ...,
) -> None:
...
@t.overload
def _to_str(
x: t.Any,
charset: str | None = ...,
errors: str = ...,
allow_none_charset: bool = ...,
) -> str:
...
def _to_str(
x: t.Any | None,
charset: str | None = _default_encoding,
errors: str = "strict",
allow_none_charset: bool = False,
) -> str | bytes | None:
if x is None or isinstance(x, str):
return x
if not isinstance(x, (bytes, bytearray)):
return str(x)
if charset is None:
if allow_none_charset:
return x
return x.decode(charset, errors) # type: ignore
def _wsgi_decoding_dance(
s: str, charset: str = "utf-8", errors: str = "replace"
) -> str:
return s.encode("latin1").decode(charset, errors)
def _wsgi_encoding_dance(s: str, charset: str = "utf-8", errors: str = "strict") -> str:
return s.encode(charset).decode("latin1", errors)
def _wsgi_encoding_dance(s: str) -> str:
return s.encode().decode("latin1")
def _get_environ(obj: WSGIEnvironment | Request) -> WSGIEnvironment:
@@ -287,31 +196,6 @@ class _DictAccessorProperty(t.Generic[_TAccessorValue]):
return f"<{type(self).__name__} {self.name}>"
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
try:
# Try decoding in one shot.
return data.decode("idna")
except UnicodeDecodeError:
pass
# Decode each part separately, leaving invalid parts as punycode.
parts = []
for part in data.split(b"."):
try:
parts.append(part.decode("idna"))
except UnicodeDecodeError:
parts.append(part.decode("ascii"))
return ".".join(parts)
_plain_int_re = re.compile(r"-?\d+", re.ASCII)

View File

@@ -3,15 +3,11 @@ 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
@@ -46,7 +42,7 @@ class Authorization:
def __init__(
self,
auth_type: str,
data: dict[str, str] | None = None,
data: dict[str, str | None] | None = None,
token: str | None = None,
) -> None:
self.type = auth_type
@@ -143,31 +139,6 @@ class Authorization:
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.
@@ -196,21 +167,12 @@ class WWWAuthenticate:
def __init__(
self,
auth_type: str | None = None,
values: dict[str, str] | None = None,
auth_type: str,
values: dict[str, str | None] | 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]
self._parameters: dict[str, str | None] = CallbackDict( # type: ignore[misc]
values, lambda _: self._trigger_on_update()
)
self._token = token
@@ -231,7 +193,7 @@ class WWWAuthenticate:
self._trigger_on_update()
@property
def parameters(self) -> dict[str, str]:
def parameters(self) -> dict[str, str | None]:
"""A dict of parameters for the header. Only one of this or :attr:`token` should
have a value for a given scheme.
"""
@@ -261,62 +223,6 @@ class WWWAuthenticate:
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)
@@ -410,101 +316,3 @@ class WWWAuthenticate:
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)

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import re
import typing as t
import warnings
from .._internal import _missing
from ..exceptions import BadRequestKeyError
@@ -82,7 +81,7 @@ class Headers:
__hash__ = None
def get(self, key, default=None, type=None, as_bytes=None):
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
@@ -101,27 +100,16 @@ class Headers:
: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:: 3.0
The ``as_bytes`` parameter was removed.
.. 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:
@@ -129,7 +117,7 @@ class Headers:
except ValueError:
return default
def getlist(self, key, type=None, as_bytes=None):
def getlist(self, key, type=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
@@ -141,27 +129,16 @@ class Headers:
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:: 3.0
The ``as_bytes`` parameter was removed.
.. 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)
@@ -293,7 +270,6 @@ class Headers:
"""
if kw:
_value = _options_header_vkw(_value, kw)
_key = _str_header_key(_key)
_value = _str_header_value(_value)
self._list.append((_key, _value))
@@ -326,7 +302,6 @@ class Headers:
"""
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))
@@ -399,7 +374,7 @@ class Headers:
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]
value = [(k, _str_header_value(v)) for (k, v) in value]
if isinstance(key, int):
self._list[key] = value[0]
else:
@@ -476,36 +451,10 @@ def _options_header_vkw(value: str, kw: dict[str, t.Any]):
)
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)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import typing as t
import warnings
from io import BytesIO
from urllib.parse import parse_qsl
@@ -68,8 +67,6 @@ def default_stream_factory(
def parse_form_data(
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,
@@ -108,12 +105,11 @@ def parse_form_data(
is exceeded, a :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
:return: A tuple in the form ``(stream, form, files)``.
.. versionchanged:: 2.3
Added the ``max_form_parts`` parameter.
.. versionchanged:: 3.0
The ``charset`` and ``errors`` parameters were removed.
.. versionchanged:: 2.3
The ``charset`` and ``errors`` parameters are deprecated and will be removed in
Werkzeug 3.0.
Added the ``max_form_parts`` parameter.
.. versionadded:: 0.5.1
Added the ``silent`` parameter.
@@ -124,8 +120,6 @@ def parse_form_data(
"""
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,
@@ -159,13 +153,11 @@ class FormDataParser:
: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:: 3.0
The ``charset`` and ``errors`` parameters were removed.
.. versionchanged:: 2.3
The ``parse_functions`` attribute and ``get_parse_func`` methods are deprecated
and will be removed in Werkzeug 3.0.
.. versionchanged:: 3.0
The ``parse_functions`` attribute and ``get_parse_func`` methods were removed.
.. versionchanged:: 2.2.3
Added the ``max_form_parts`` parameter.
@@ -176,8 +168,6 @@ class FormDataParser:
def __init__(
self,
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,
@@ -189,30 +179,6 @@ class FormDataParser:
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
self.max_form_parts = max_form_parts
@@ -223,44 +189,6 @@ class FormDataParser:
self.cls = cls
self.silent = silent
def get_parse_func(
self, mimetype: str, options: dict[str, str]
) -> None | (
t.Callable[
[FormDataParser, t.IO[bytes], str, int | None, dict[str, str]],
t_parse_result,
]
):
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]
return None
def parse_from_environ(self, environ: WSGIEnvironment) -> t_parse_result:
"""Parses the information from the environment as form data.
@@ -294,30 +222,14 @@ class FormDataParser:
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.
.. versionchanged:: 3.0
The invalid ``application/x-url-encoded`` content type is not
treated as ``application/x-www-form-urlencoded``.
"""
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()
@@ -339,12 +251,8 @@ class FormDataParser:
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(
stream_factory=self.stream_factory,
charset=charset,
errors=errors,
max_form_memory_size=self.max_form_memory_size,
max_form_parts=self.max_form_parts,
cls=self.cls,
@@ -375,7 +283,6 @@ class FormDataParser:
items = parse_qsl(
stream.read().decode(),
keep_blank_values=True,
encoding=self.charset,
errors="werkzeug.url_quote",
)
except ValueError as e:
@@ -383,49 +290,16 @@ class FormDataParser:
return stream, self.cls(items), self.cls()
parse_functions: dict[
str,
t.Callable[
[FormDataParser, t.IO[bytes], str, int | None, dict[str, str]],
t_parse_result,
],
] = {}
class MultiPartParser:
def __init__(
self,
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: 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
@@ -456,7 +330,7 @@ class MultiPartParser:
if ct_charset in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
return ct_charset
return self.charset
return "utf-8"
def start_file_streaming(
self, event: File, total_content_length: int | None
@@ -509,7 +383,7 @@ class MultiPartParser:
if not event.more_data:
if isinstance(current_part, Field):
value = b"".join(container).decode(
self.get_part_charset(current_part.headers), self.errors
self.get_part_charset(current_part.headers), "replace"
)
fields.append((current_part.name, value))
else:

View File

@@ -136,11 +136,7 @@ class COOP(Enum):
SAME_ORIGIN = "same-origin"
def quote_header_value(
value: t.Any,
extra_chars: str | None = None,
allow_token: bool = True,
) -> str:
def quote_header_value(value: t.Any, allow_token: bool = True) -> str:
"""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.
@@ -150,33 +146,17 @@ def quote_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:: 3.0
Passing bytes is not supported.
.. versionchanged:: 3.0
The ``extra_chars`` parameter is removed.
.. 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.
.. 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:
@@ -185,9 +165,6 @@ def quote_header_value(
if allow_token:
token_chars = _token_chars
if extra_chars:
token_chars |= set(extra_chars)
if token_chars.issuperset(value):
return value
@@ -195,7 +172,7 @@ def quote_header_value(
return f'"{value}"'
def unquote_header_value(value: str, is_filename: bool | None = None) -> str:
def unquote_header_value(value: str) -> str:
"""Remove double quotes and decode slash-escaped ``"`` and ``\\`` characters in a
header value.
@@ -203,22 +180,12 @@ def unquote_header_value(value: str, is_filename: bool | None = None) -> str:
:param value: The header value to unquote.
.. versionchanged:: 2.3
The ``is_filename`` parameter is deprecated and will be removed in Werkzeug 3.0.
.. versionchanged:: 3.0
The ``is_filename`` parameter is removed.
"""
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 not is_filename:
return value.replace("\\\\", "\\").replace('\\"', '"')
return value.replace("\\\\", "\\").replace('\\"', '"')
return value
@@ -269,10 +236,7 @@ def dump_options_header(header: str | None, options: t.Mapping[str, t.Any]) -> s
return "; ".join(segments)
def dump_header(
iterable: dict[str, t.Any] | t.Iterable[t.Any],
allow_token: bool | None = None,
) -> str:
def dump_header(iterable: dict[str, t.Any] | t.Iterable[t.Any]) -> str:
"""Produce a header value from a list of items or ``key=value`` pairs, separated by
commas ``,``.
@@ -298,22 +262,12 @@ def dump_header(
:param iterable: The items to create a header from.
.. versionchanged:: 2.3
The ``allow_token`` parameter is deprecated and will be removed in Werkzeug 3.0.
.. versionchanged:: 3.0
The ``allow_token`` parameter is removed.
.. 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 = []
@@ -323,11 +277,9 @@ def dump_header(
elif key[-1] == "*":
items.append(f"{key}={value}")
else:
items.append(
f"{key}={quote_header_value(value, allow_token=allow_token)}"
)
items.append(f"{key}={quote_header_value(value)}")
else:
items = [quote_header_value(x, allow_token=allow_token) for x in iterable]
items = [quote_header_value(x) for x in iterable]
return ", ".join(items)
@@ -372,7 +324,7 @@ def parse_list_header(value: str) -> list[str]:
return result
def parse_dict_header(value: str, cls: type[dict] | None = None) -> dict[str, str]:
def parse_dict_header(value: str) -> dict[str, str | None]:
"""Parse a list header using :func:`parse_list_header`, then parse each item as a
``key=value`` pair.
@@ -391,36 +343,19 @@ def parse_dict_header(value: str, cls: type[dict] | None = None) -> dict[str, st
:param value: The header value to parse.
.. versionchanged:: 3.0
Passing bytes is not supported.
.. versionchanged:: 3.0
The ``cls`` argument is removed.
.. versionchanged:: 2.3
Added support for ``key*=charset''value`` encoded items.
.. 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")
result: dict[str, str | None] = {}
for item in parse_list_header(value):
key, has_value, value = item.partition("=")
@@ -815,65 +750,6 @@ def parse_set_header(
return ds.HeaderSet(parse_list_header(value), on_update)
def parse_authorization_header(
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`
object.
: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.
"""
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: 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.
:param value: a WWW-Authenticate header to parse.
:param on_update: an optional callable that is called every time a value
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.
"""
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: str | None) -> ds.IfRange:
"""Parses an if-range header which can be an etag or a date. Returns
a :class:`~werkzeug.datastructures.IfRange` object.
@@ -1284,8 +1160,6 @@ def is_hop_by_hop_header(header: str) -> bool:
def parse_cookie(
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.
@@ -1300,9 +1174,8 @@ def parse_cookie(
: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.
.. versionchanged:: 3.0
Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
.. versionchanged:: 1.0
Returns a :class:`MultiDict` instead of a ``TypeConversionDict``.
@@ -1313,22 +1186,13 @@ def parse_cookie(
"""
if isinstance(header, dict):
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
)
return _sansio_http.parse_cookie(cookie=cookie, cls=cls)
_cookie_no_quote_re = re.compile(r"[\w!#$%&'()*+\-./:<=>?@\[\]^`{|}~]*", re.A)
@@ -1349,7 +1213,6 @@ def dump_cookie(
domain: str | None = None,
secure: bool = False,
httponly: bool = False,
charset: str | None = None,
sync_expires: bool = True,
max_size: int = 4093,
samesite: str | None = None,
@@ -1392,6 +1255,9 @@ def dump_cookie(
.. _`cookie`: http://browsercookielimits.squawky.net/
.. versionchanged:: 3.0
Passing bytes, and the ``charset`` parameter, were removed.
.. versionchanged:: 2.3.3
The ``path`` parameter is ``/`` by default.
@@ -1405,46 +1271,14 @@ def dump_cookie(
.. 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``.
"""
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(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()
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)
path = quote(path, safe="%!$&'()*+,/:=@")
if domain:
domain = domain.partition(":")[0].lstrip(".").encode("idna").decode("ascii")
@@ -1469,7 +1303,7 @@ def dump_cookie(
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)
lambda m: _cookie_slash_map[m.group()], value.encode()
).decode("ascii")
value = f'"{value}"'

View File

@@ -44,11 +44,16 @@ class ProfilerMiddleware:
- ``{method}`` - The request method; GET, POST, etc.
- ``{path}`` - The request path or 'root' should one not exist.
- ``{elapsed}`` - The elapsed time of the request.
- ``{elapsed}`` - The elapsed time of the request in milliseconds.
- ``{time}`` - The time of the request.
If it is a callable, it will be called with the WSGI ``environ``
dict and should return a filename.
If it is a callable, it will be called with the WSGI ``environ`` and
be expected to return a filename string. The ``environ`` dictionary
will also have the ``"werkzeug.profiler"`` key populated with a
dictionary containing the following fields (more may be added in the
future):
- ``{elapsed}`` - The elapsed time of the request in milliseconds.
- ``{time}`` - The time of the request.
:param app: The WSGI application to wrap.
:param stream: Write stats to this stream. Disable with ``None``.
@@ -65,6 +70,10 @@ class ProfilerMiddleware:
from werkzeug.middleware.profiler import ProfilerMiddleware
app = ProfilerMiddleware(app)
.. versionchanged:: 3.0
Added the ``"werkzeug.profiler"`` key to the ``filename_format(environ)``
parameter with the ``elapsed`` and ``time`` fields.
.. versionchanged:: 0.15
Stats are written even if ``profile_dir`` is given, and can be
disable by passing ``stream=None``.
@@ -118,6 +127,10 @@ class ProfilerMiddleware:
if self._profile_dir is not None:
if callable(self._filename_format):
environ["werkzeug.profiler"] = {
"elapsed": elapsed * 1000.0,
"time": time.time(),
}
filename = self._filename_format(environ)
else:
filename = self._filename_format.format(

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import re
import typing as t
import uuid
import warnings
from urllib.parse import quote
if t.TYPE_CHECKING:
@@ -42,17 +41,8 @@ class BaseConverter:
return value
def to_url(self, value: t.Any) -> str:
if isinstance(value, (bytes, bytearray)):
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="!$&'()*+,/:;=@")
return quote(str(value), safe="!$&'()*+,/:;=@")
class UnicodeConverter(BaseConverter):
@@ -129,6 +119,7 @@ class PathConverter(BaseConverter):
:param map: the :class:`Map`.
"""
part_isolating = False
regex = "[^/].*?"
weight = 200

View File

@@ -47,7 +47,6 @@ class Map:
:param rules: sequence of url rules for this map.
:param default_subdomain: The default subdomain for rules without a
subdomain defined.
:param charset: charset of the url. defaults to ``"utf-8"``
:param strict_slashes: If a rule ends with a slash but the matched
URL does not, redirect to the URL with a trailing slash.
:param merge_slashes: Merge consecutive slashes when matching or
@@ -62,15 +61,13 @@ class Map:
:param sort_parameters: If set to `True` the url parameters are sorted.
See `url_encode` for more details.
:param sort_key: The sort key function for `url_encode`.
:param encoding_errors: the error method to use for decoding
:param host_matching: if set to `True` it enables the host matching
feature and disables the subdomain one. If
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:: 3.0
The ``charset`` and ``encoding_errors`` parameters were removed.
.. versionchanged:: 1.0
If ``url_scheme`` is ``ws`` or ``wss``, only WebSocket rules will match.
@@ -97,14 +94,12 @@ class Map:
self,
rules: t.Iterable[RuleFactory] | None = None,
default_subdomain: str = "",
charset: str | None = None,
strict_slashes: bool = True,
merge_slashes: bool = True,
redirect_defaults: bool = True,
converters: t.Mapping[str, type[BaseConverter]] | None = None,
sort_parameters: bool = False,
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)
@@ -113,30 +108,6 @@ class Map:
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
self.redirect_defaults = redirect_defaults
@@ -362,7 +333,7 @@ class Map:
def _get_wsgi_string(name: str) -> str | None:
val = env.get(name)
if val is not None:
return _wsgi_decoding_dance(val, self.charset)
return _wsgi_decoding_dance(val)
return None
script_name = _get_wsgi_string("SCRIPT_NAME")
@@ -629,9 +600,7 @@ class MapAdapter:
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
)
new_path = quote(e.path_info, safe="!$&'()*+,/:;=@")
raise RequestRedirect(
self.make_redirect_url(new_path, query_args)
) from None
@@ -767,7 +736,7 @@ class MapAdapter:
def encode_query_args(self, query_args: t.Mapping[str, t.Any] | str) -> str:
if not isinstance(query_args, str):
return _urlencode(query_args, encoding=self.map.charset)
return _urlencode(query_args)
return query_args
def make_redirect_url(

View File

@@ -583,7 +583,7 @@ class Rule(RuleFactory):
if self.map.sort_parameters:
items = sorted(items, key=self.map.sort_key)
return _urlencode(items, encoding=self.map.charset)
return _urlencode(items)
def _parse_rule(self, rule: str) -> t.Iterable[RulePart]:
content = ""
@@ -739,12 +739,7 @@ class Rule(RuleFactory):
opl.append((False, data))
elif not is_dynamic:
# safe = https://url.spec.whatwg.org/#url-path-segment-string
opl.append(
(
False,
quote(data, safe="!$&'()*+,/:;=@", encoding=self.map.charset),
)
)
opl.append((False, quote(data, safe="!$&'()*+,/:;=@")))
else:
opl.append((True, data))

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import re
import typing as t
import warnings
from datetime import datetime
from .._internal import _dt_as_utc
@@ -123,8 +122,6 @@ def _cookie_unslash_replace(m: t.Match[bytes]) -> bytes:
def parse_cookie(
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.
@@ -138,42 +135,14 @@ def parse_cookie(
: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.
.. versionchanged:: 3.0
Passing bytes, and the ``charset`` and ``errors`` parameters, were removed.
.. versionadded:: 2.2
"""
if cls is None:
cls = ds.MultiDict
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()
@@ -191,7 +160,7 @@ def parse_cookie(
# 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)
).decode(errors="replace")
out.append((ck, cv))

View File

@@ -251,12 +251,20 @@ class MultipartDecoder:
else:
data_start = 0
if self.buffer.find(b"--" + self.boundary) == -1:
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)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import typing as t
import warnings
from datetime import datetime
from urllib.parse import parse_qsl
@@ -59,95 +58,13 @@ class Request:
:param headers: The headers received with the request.
:param remote_addr: The address of the client sending the request.
.. versionchanged:: 3.0
The ``charset``, ``url_charset``, and ``encoding_errors`` attributes
were removed.
.. versionadded:: 2.0
"""
_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
@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
#: multiple values per key. alternatively it makes sense to use an
@@ -209,40 +126,6 @@ class Request:
headers: Headers,
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
@@ -291,7 +174,6 @@ class Request:
parse_qsl(
self.query_string.decode(),
keep_blank_values=True,
encoding=self._url_charset,
errors="werkzeug.url_quote",
)
)
@@ -360,13 +242,8 @@ class Request:
"""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,
charset=charset,
errors=errors,
cls=self.dict_storage_class,
wsgi_combined_cookie, cls=self.dict_storage_class
)
# Common Descriptors

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import typing as t
import warnings
from datetime import datetime
from datetime import timedelta
from datetime import timezone
@@ -81,36 +80,12 @@ class Response:
:param content_type: The full content type of the response.
Overrides building the value from ``mimetype``.
.. versionchanged:: 3.0
The ``charset`` attribute was removed.
.. versionadded:: 2.0
"""
_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
@@ -139,17 +114,6 @@ class Response:
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:
@@ -161,7 +125,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, "utf-8")
content_type = mimetype
if content_type is not None:
self.headers["Content-Type"] = content_type
@@ -255,7 +219,6 @@ 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(
@@ -267,7 +230,6 @@ class Response:
domain=domain,
secure=secure,
httponly=httponly,
charset=charset,
max_size=self.max_cookie_size,
samesite=samesite,
),
@@ -332,7 +294,7 @@ 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, "utf-8")
@property
def mimetype_params(self) -> dict[str, str]:

View File

@@ -5,7 +5,6 @@ import hmac
import os
import posixpath
import secrets
import warnings
SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
DEFAULT_PBKDF2_ITERATIONS = 600000
@@ -24,14 +23,6 @@ def gen_salt(length: int) -> str:
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")
@@ -72,26 +63,20 @@ def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
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,
)
return hmac.new(salt, password, method).hexdigest(), method
raise ValueError(f"Invalid hash method '{method}'.")
def generate_password_hash(
password: str, method: str = "pbkdf2", salt_length: int = 16
password: str, method: str = "scrypt", salt_length: int = 16
) -> str:
"""Securely hash a password for storage. A password can be compared to a stored hash
using :func:`check_password_hash`.
The following methods are supported:
- ``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``,
- ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
- ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
Default parameters may be updated to reflect current guidelines, and methods may be

View File

@@ -154,9 +154,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler):
@property
def server_version(self) -> str: # type: ignore
from . import __version__
return f"Werkzeug/{__version__}"
return self.server._server_version
def make_environ(self) -> WSGIEnvironment:
request_url = urlsplit(self.path)
@@ -764,7 +762,7 @@ class BaseWSGIServer(HTTPServer):
if sys.platform == "darwin" and port == 5000:
print(
"On macOS, try disabling the 'AirPlay Receiver' service"
" from System Preferences -> Sharing.",
" from System Preferences -> General -> AirDrop & Handoff.",
file=sys.stderr,
)
@@ -796,6 +794,10 @@ class BaseWSGIServer(HTTPServer):
else:
self.ssl_context = None
import importlib.metadata
self._server_version = f"Werkzeug/{importlib.metadata.version('werkzeug')}"
def log(self, type: str, message: str, *args: t.Any) -> None:
_log(type, message, *args)

View File

@@ -4,7 +4,6 @@ import dataclasses
import mimetypes
import sys
import typing as t
import warnings
from collections import defaultdict
from datetime import datetime
from io import BytesIO
@@ -17,7 +16,6 @@ from urllib.parse import urlsplit
from urllib.parse import urlunsplit
from ._internal import _get_environ
from ._internal import _make_encode_wrapper
from ._internal import _wsgi_decoding_dance
from ._internal import _wsgi_encoding_dance
from .datastructures import Authorization
@@ -58,24 +56,14 @@ def stream_encode_multipart(
use_tempfile: bool = True,
threshold: int = 1024 * 500,
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
.. versionchanged:: 3.0
The ``charset`` parameter was removed.
"""
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()}"
@@ -144,9 +132,7 @@ def stream_encode_multipart(
if not isinstance(value, str):
value = str(value)
write_binary(encoder.send_event(Field(name=key, headers=Headers())))
write_binary(
encoder.send_event(Data(data=value.encode(charset), more_data=False))
)
write_binary(encoder.send_event(Data(data=value.encode(), more_data=False)))
write_binary(encoder.send_event(Epilogue(data=b"")))
@@ -156,18 +142,16 @@ def stream_encode_multipart(
def encode_multipart(
values: t.Mapping[str, t.Any],
boundary: str | None = None,
charset: str | None = None,
values: t.Mapping[str, t.Any], boundary: 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
.. versionchanged:: 3.0
The ``charset`` parameter was removed.
"""
stream, length, boundary = stream_encode_multipart(
values, use_tempfile=False, boundary=boundary, charset=charset
values, use_tempfile=False, boundary=boundary
)
return boundary, stream.read()
@@ -259,8 +243,8 @@ class EnvironBuilder:
``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:: 3.0
The ``charset`` parameter was removed.
.. versionchanged:: 2.1
``CONTENT_TYPE`` and ``CONTENT_LENGTH`` are not duplicated as
@@ -328,35 +312,20 @@ class EnvironBuilder:
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:
if query_string is not None and "?" in path:
raise ValueError("Query string is defined in the path and as an argument")
request_uri = urlsplit(path)
if query_string is None and path_s("?") in path:
if query_string is None and "?" 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 = iri_to_uri(
base_url, charset=charset if charset != "utf-8" else None
)
base_url = iri_to_uri(base_url)
self.base_url = base_url # type: ignore
if isinstance(query_string, str):
self.query_string = query_string
@@ -409,7 +378,7 @@ class EnvironBuilder:
if hasattr(data, "read"):
data = data.read()
if isinstance(data, str):
data = data.encode(self.charset)
data = data.encode()
if isinstance(data, bytes):
self.input_stream = BytesIO(data)
if self.content_length is None:
@@ -526,7 +495,7 @@ class EnvironBuilder:
@mimetype.setter
def mimetype(self, value: str) -> None:
self.content_type = get_content_type(value, self.charset)
self.content_type = get_content_type(value, "utf-8")
@property
def mimetype_params(self) -> t.Mapping[str, str]:
@@ -628,7 +597,7 @@ class EnvironBuilder:
"""
if self._query_string is None:
if self._args is not None:
return _urlencode(self._args, encoding=self.charset)
return _urlencode(self._args)
return ""
return self._query_string
@@ -716,13 +685,12 @@ 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=charset
CombinedMultiDict([self.form, self.files])
)
content_type = f'{mimetype}; boundary="{boundary}"'
elif mimetype == "application/x-www-form-urlencoded":
form_encoded = _urlencode(self.form, encoding=self.charset).encode("ascii")
form_encoded = _urlencode(self.form).encode("ascii")
content_length = len(form_encoded)
input_stream = BytesIO(form_encoded)
else:
@@ -733,15 +701,15 @@ class EnvironBuilder:
result.update(self.environ_base)
def _path_encode(x: str) -> str:
return _wsgi_encoding_dance(unquote(x, encoding=self.charset), self.charset)
return _wsgi_encoding_dance(unquote(x))
raw_uri = _wsgi_encoding_dance(self.request_uri, self.charset)
raw_uri = _wsgi_encoding_dance(self.request_uri)
result.update(
{
"REQUEST_METHOD": self.method,
"SCRIPT_NAME": _path_encode(self.script_root),
"PATH_INFO": _path_encode(self.path),
"QUERY_STRING": _wsgi_encoding_dance(self.query_string, self.charset),
"QUERY_STRING": _wsgi_encoding_dance(self.query_string),
# Non-standard, added by mod_wsgi, uWSGI
"REQUEST_URI": raw_uri,
# Non-standard, added by gunicorn
@@ -857,20 +825,6 @@ class Client:
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:
@@ -894,7 +848,7 @@ class Client:
self,
key: str,
value: str = "",
*args: t.Any,
*,
domain: str = "localhost",
origin_only: bool = True,
path: str = "/",
@@ -920,34 +874,21 @@ class Client:
or as a prefix.
:param kwargs: Passed to :func:`.dump_cookie`.
.. versionchanged:: 3.0
The parameter ``server_name`` is removed. The first parameter is
``key``. Use the ``domain`` and ``origin_only`` parameters instead.
.. 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.
"""
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)
)
@@ -961,10 +902,9 @@ class Client:
def delete_cookie(
self,
key: str,
*args: t.Any,
*,
domain: str = "localhost",
path: str = "/",
**kwargs: t.Any,
) -> None:
"""Delete a cookie if it exists. Cookies are uniquely identified by
``(domain, path, key)``.
@@ -973,44 +913,21 @@ class Client:
:param domain: The domain the cookie was set for.
:param path: The path the cookie was set for.
.. versionchanged:: 3.0
The ``server_name`` parameter is removed. The first parameter is
``key``. Use the ``domain`` parameter instead.
.. versionchanged:: 3.0
The ``secure``, ``httponly`` and ``samesite`` parameters are removed.
.. 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:

File diff suppressed because it is too large Load Diff

View File

@@ -50,6 +50,10 @@ class Request(_SansIORequest):
prevent consuming the form data in middleware, which would make
it unavailable to the final application.
.. versionchanged:: 3.0
The ``charset``, ``url_charset``, and ``encoding_errors`` parameters
were removed.
.. versionchanged:: 2.1
Old ``BaseRequest`` and mixin classes were removed.
@@ -145,9 +149,6 @@ class Request(_SansIORequest):
"""
from ..test import EnvironBuilder
kwargs.setdefault(
"charset", cls.charset if not isinstance(cls.charset, property) else None
)
builder = EnvironBuilder(*args, **kwargs)
try:
return builder.get_request(cls)
@@ -240,12 +241,8 @@ 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(
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,
@@ -424,7 +421,7 @@ class Request(_SansIORequest):
if cache:
self._cached_data = rv
if as_text:
rv = rv.decode(self._charset, self._encoding_errors)
rv = rv.decode(errors="replace")
return rv
@cached_property

View File

@@ -28,10 +28,10 @@ if t.TYPE_CHECKING:
from .request import Request
def _iter_encoded(iterable: t.Iterable[str | bytes], charset: str) -> t.Iterator[bytes]:
def _iter_encoded(iterable: t.Iterable[str | bytes]) -> t.Iterator[bytes]:
for item in iterable:
if isinstance(item, str):
yield item.encode(charset)
yield item.encode()
else:
yield item
@@ -284,7 +284,7 @@ class Response(_SansIOResponse):
rv = b"".join(self.iter_encoded())
if as_text:
return rv.decode(self._charset)
return rv.decode()
return rv
@@ -296,7 +296,7 @@ class Response(_SansIOResponse):
.. versionadded:: 0.9
"""
if isinstance(value, str):
value = value.encode(self._charset)
value = value.encode()
self.response = [value]
if self.automatically_set_content_length:
self.headers["Content-Length"] = str(len(value))
@@ -366,7 +366,7 @@ class Response(_SansIOResponse):
# 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)
@property
def is_streamed(self) -> bool:
@@ -832,4 +832,4 @@ class ResponseStream:
@property
def encoding(self) -> str:
return self.response._charset
return "utf-8"

View File

@@ -1,16 +1,10 @@
from __future__ import annotations
import io
import re
import typing as t
import warnings
from functools import partial
from functools import update_wrapper
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
@@ -200,45 +194,18 @@ def get_input_stream(
return t.cast(t.IO[bytes], LimitedStream(stream, content_length))
def get_path_info(
environ: WSGIEnvironment,
charset: t.Any = ...,
errors: str | None = None,
) -> str:
def get_path_info(environ: WSGIEnvironment) -> str:
"""Return ``PATH_INFO`` from the WSGI environment.
:param environ: WSGI environment to get the path from.
.. versionchanged:: 2.3
The ``charset`` and ``errors`` parameters are deprecated and will be removed in
Werkzeug 3.0.
.. versionchanged:: 3.0
The ``charset`` and ``errors`` parameters were removed.
.. versionadded:: 0.9
"""
if charset is not ...:
warnings.warn(
"The 'charset' parameter is deprecated and will be removed"
" in Werkzeug 3.0.",
DeprecationWarning,
stacklevel=2,
)
if charset is None:
charset = "utf-8"
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"
path = environ.get("PATH_INFO", "").encode("latin1")
return path.decode(charset, errors) # type: ignore[no-any-return]
path: bytes = environ.get("PATH_INFO", "").encode("latin1")
return path.decode(errors="replace")
class ClosingIterator:
@@ -455,225 +422,6 @@ class _RangeWrapper:
self.iterable.close()
def _make_chunk_iter(
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."
)
if not hasattr(stream, "read"):
for item in stream:
if item:
yield item
return
stream = t.cast(t.IO[bytes], stream)
if not isinstance(stream, LimitedStream) and limit is not None:
stream = t.cast(t.IO[bytes], LimitedStream(stream, limit))
_read = stream.read
while True:
item = _read(buffer_size)
if not item:
break
yield item
def make_line_iter(
stream: t.Iterable[bytes] | t.IO[bytes],
limit: int | None = None,
buffer_size: int = 10 * 1024,
cap_at_buffer: bool = False,
) -> t.Iterator[bytes]:
"""Safely iterates line-based over an input stream. If the input stream
is not a :class:`LimitedStream` the `limit` parameter is mandatory.
This uses the stream's :meth:`~file.read` method internally as opposite
to the :meth:`~file.readline` method that is unsafe and can only be used
in violation of the WSGI specification. The same problem applies to the
`__iter__` function of the input stream which calls :meth:`~file.readline`
without arguments.
If you need line-by-line processing it's strongly recommended to iterate
over the input stream using this helper function.
.. 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.
.. 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
content length. Not necessary if the `stream`
is a :class:`LimitedStream`.
:param buffer_size: The optional buffer size.
:param cap_at_buffer: if this is set chunks are split if they are longer
than the buffer size. Internally this is implemented
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
s = _make_encode_wrapper(first_item)
empty = t.cast(bytes, s(""))
cr = t.cast(bytes, s("\r"))
lf = t.cast(bytes, s("\n"))
crlf = t.cast(bytes, s("\r\n"))
_iter = t.cast(t.Iterator[bytes], chain((first_item,), _iter))
def _iter_basic_lines() -> t.Iterator[bytes]:
_join = empty.join
buffer: list[bytes] = []
while True:
new_data = next(_iter, "")
if not new_data:
break
new_buf: list[bytes] = []
buf_size = 0
for item in t.cast(
t.Iterator[bytes], chain(buffer, new_data.splitlines(True))
):
new_buf.append(item)
buf_size += len(item)
if item and item[-1:] in crlf:
yield _join(new_buf)
new_buf = []
elif cap_at_buffer and buf_size >= buffer_size:
rv = _join(new_buf)
while len(rv) >= buffer_size:
yield rv[:buffer_size]
rv = rv[buffer_size:]
new_buf = [rv]
buffer = new_buf
if buffer:
yield _join(buffer)
# This hackery is necessary to merge 'foo\r' and '\n' into one item
# of 'foo\r\n' if we were unlucky and we hit a chunk boundary.
previous = empty
for item in _iter_basic_lines():
if item == lf and previous[-1:] == cr:
previous += item
item = empty
if previous:
yield previous
previous = item
if previous:
yield previous
def make_chunk_iter(
stream: t.Iterable[bytes] | t.IO[bytes],
separator: bytes,
limit: int | None = None,
buffer_size: int = 10 * 1024,
cap_at_buffer: bool = False,
) -> t.Iterator[bytes]:
"""Works like :func:`make_line_iter` but accepts a separator
which divides chunks. If you want newline based processing
you should use :func:`make_line_iter` instead as it
supports arbitrary newline markers.
.. deprecated:: 2.3
Will be removed in Werkzeug 3.0.
.. versionchanged:: 0.11
added support for the `cap_at_buffer` parameter.
.. versionchanged:: 0.9
added support for iterators as input stream.
.. versionadded:: 0.8
:param stream: the stream or iterate to iterate over.
:param separator: the separator that divides chunks.
:param limit: the limit in bytes for the stream. (Usually
content length. Not necessary if the `stream`
is otherwise already limited).
:param buffer_size: The optional buffer size.
:param cap_at_buffer: if this is set chunks are split if they are longer
than the buffer size. Internally this is implemented
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
_iter = t.cast(t.Iterator[bytes], chain((first_item,), _iter))
if isinstance(first_item, str):
separator = _to_str(separator)
_split = re.compile(f"({re.escape(separator)})").split
_join = "".join
else:
separator = _to_bytes(separator)
_split = re.compile(b"(" + re.escape(separator) + b")").split
_join = b"".join
buffer: list[bytes] = []
while True:
new_data = next(_iter, b"")
if not new_data:
break
chunks = _split(new_data)
new_buf: list[bytes] = []
buf_size = 0
for item in chain(buffer, chunks):
if item == separator:
yield _join(new_buf)
new_buf = []
buf_size = 0
else:
buf_size += len(item)
new_buf.append(item)
if cap_at_buffer and buf_size >= buffer_size:
rv = _join(new_buf)
while len(rv) >= buffer_size:
yield rv[:buffer_size]
rv = rv[buffer_size:]
new_buf = [rv]
buf_size = len(rv)
buffer = new_buf
if buffer:
yield _join(buffer)
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