Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] main from pallets:main #304

Merged
merged 9 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@ Unreleased
- ``Request.max_form_memory_size`` defaults to 500kB instead of unlimited.
Non-file form fields over this size will cause a ``RequestEntityTooLarge``
error. :issue:`2964`
- Support Cookie CHIPS (Partitioned Cookies). :issue:`2797`
- ``OrderedMultiDict`` and ``ImmutableOrderedMultiDict`` are deprecated.
Use ``MultiDict`` and ``ImmutableMultiDict`` instead. :issue:`2968`
- Support Cookie CHIPS (Partitioned Cookies). :issue:`2797`
- ``CacheControl.no_transform`` is a boolean when present. ``min_fresh`` is
``None`` when not present. Added the ``must_understand`` attribute. Fixed
some typing issues on cache control. :issue:`2881`
- Add ``stale_while_revalidate`` and ``stale_if_error`` properties to
``ResponseCacheControl``. :issue:`2948`
- Add 421 ``MisdirectedRequest`` HTTP exception. :issue:`2850`
- Increase default work factor for PBKDF2 to 1,000,000 iterations. :issue:`2969`

- Increase default work factor for PBKDF2 to 1,000,000 iterations.
:issue:`2969`
- Inline annotations for ``datastructures``, removing stub files.
:issue:`2970`
- ``MultiDict.getlist`` catches ``TypeError`` in addition to ``ValueError``
when doing type conversion. :issue:`2976`
- Implement ``|`` and ``|=`` operators for ``MultiDict``, ``Headers``, and
``CallbackDict``, and disallow ``|=`` on immutable types. :issue:`2977`


Version 3.0.6
Expand Down
29 changes: 26 additions & 3 deletions docs/datastructures.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,33 @@ General Purpose
:members:
:inherited-members:

.. autoclass:: OrderedMultiDict
.. class:: OrderedMultiDict

.. autoclass:: ImmutableMultiDict
:members: copy
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.

.. deprecated:: 3.1
Will be removed in Werkzeug 3.2. Use ``MultiDict`` instead.

.. class:: ImmutableMultiDict

An immutable :class:`OrderedMultiDict`.

.. deprecated:: 3.1
Will be removed in Werkzeug 3.2. Use ``ImmutableMultiDict`` instead.

.. versionadded:: 0.6

.. autoclass:: ImmutableOrderedMultiDict
:members: copy
Expand Down
34 changes: 32 additions & 2 deletions src/werkzeug/datastructures/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
from __future__ import annotations

import typing as t

from .accept import Accept as Accept
from .accept import CharsetAccept as CharsetAccept
from .accept import LanguageAccept as LanguageAccept
Expand Down Expand Up @@ -26,9 +30,35 @@
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


def __getattr__(name: str) -> t.Any:
import warnings

if name == "OrderedMultiDict":
from .structures import _OrderedMultiDict

warnings.warn(
"'OrderedMultiDict' is deprecated and will be removed in Werkzeug"
" 3.2. Use 'MultiDict' instead.",
DeprecationWarning,
stacklevel=2,
)
return _OrderedMultiDict

if name == "ImmutableOrderedMultiDict":
from .structures import _ImmutableOrderedMultiDict

warnings.warn(
"'OrderedMultiDict' is deprecated and will be removed in Werkzeug"
" 3.2. Use 'ImmutableMultiDict' instead.",
DeprecationWarning,
stacklevel=2,
)
return _ImmutableOrderedMultiDict

raise AttributeError(name)
98 changes: 61 additions & 37 deletions src/werkzeug/datastructures/accept.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import annotations

import codecs
import collections.abc as cabc
import re
import typing as t

from .structures import ImmutableList


class Accept(ImmutableList):
class Accept(ImmutableList[tuple[str, float]]):
"""An :class:`Accept` object is just a list subclass for lists of
``(value, quality)`` tuples. It is automatically sorted by specificity
and quality.
Expand Down Expand Up @@ -42,29 +44,39 @@ class Accept(ImmutableList):

"""

def __init__(self, values=()):
def __init__(
self, values: Accept | cabc.Iterable[tuple[str, float]] | None = ()
) -> None:
if values is None:
list.__init__(self)
super().__init__()
self.provided = False
elif isinstance(values, Accept):
self.provided = values.provided
list.__init__(self, values)
super().__init__(values)
else:
self.provided = True
values = sorted(
values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True
)
list.__init__(self, values)
super().__init__(values)

def _specificity(self, value):
def _specificity(self, value: str) -> tuple[bool, ...]:
"""Returns a tuple describing the value's specificity."""
return (value != "*",)

def _value_matches(self, value, item):
def _value_matches(self, value: str, item: str) -> bool:
"""Check if a value matches a given accept item."""
return item == "*" or item.lower() == value.lower()

def __getitem__(self, key):
@t.overload
def __getitem__(self, key: str) -> float: ...
@t.overload
def __getitem__(self, key: t.SupportsIndex) -> tuple[str, float]: ...
@t.overload
def __getitem__(self, key: slice) -> list[tuple[str, float]]: ...
def __getitem__(
self, key: str | t.SupportsIndex | slice
) -> float | tuple[str, float] | list[tuple[str, float]]:
"""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``.
Expand All @@ -73,7 +85,7 @@ def __getitem__(self, key):
return self.quality(key)
return list.__getitem__(self, key)

def quality(self, key):
def quality(self, key: str) -> float:
"""Returns the quality of the key.

.. versionadded:: 0.6
Expand All @@ -85,17 +97,17 @@ def quality(self, key):
return quality
return 0

def __contains__(self, value):
def __contains__(self, value: str) -> bool: # type: ignore[override]
for item, _quality in self:
if self._value_matches(value, item):
return True
return False

def __repr__(self):
def __repr__(self) -> str:
pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self)
return f"{type(self).__name__}([{pairs_str}])"

def index(self, key):
def index(self, key: str | tuple[str, float]) -> int: # type: ignore[override]
"""Get the position of an entry or raise :exc:`ValueError`.

:param key: The key to be looked up.
Expand All @@ -111,7 +123,7 @@ def index(self, key):
raise ValueError(key)
return list.index(self, key)

def find(self, key):
def find(self, key: str | tuple[str, float]) -> int:
"""Get the position of an entry or return -1.

:param key: The key to be looked up.
Expand All @@ -121,12 +133,12 @@ def find(self, key):
except ValueError:
return -1

def values(self):
def values(self) -> cabc.Iterator[str]:
"""Iterate over all values."""
for item in self:
yield item[0]

def to_header(self):
def to_header(self) -> str:
"""Convert the header set into an HTTP header string."""
result = []
for value, quality in self:
Expand All @@ -135,17 +147,23 @@ def to_header(self):
result.append(value)
return ",".join(result)

def __str__(self):
def __str__(self) -> str:
return self.to_header()

def _best_single_match(self, match):
def _best_single_match(self, match: str) -> tuple[str, float] | None:
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):
@t.overload
def best_match(self, matches: cabc.Iterable[str]) -> str | None: ...
@t.overload
def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ...
def best_match(
self, matches: cabc.Iterable[str], default: str | None = None
) -> str | 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.
Expand All @@ -154,8 +172,8 @@ def best_match(self, matches, default=None):
:param default: the value that is returned if none match
"""
result = default
best_quality = -1
best_specificity = (-1,)
best_quality: float = -1
best_specificity: tuple[float, ...] = (-1,)
for server_item in matches:
match = self._best_single_match(server_item)
if not match:
Expand All @@ -172,16 +190,18 @@ def best_match(self, matches, default=None):
return result

@property
def best(self):
def best(self) -> str | None:
"""The best match as value."""
if self:
return self[0][0]

return None


_mime_split_re = re.compile(r"/|(?:\s*;\s*)")


def _normalize_mime(value):
def _normalize_mime(value: str) -> list[str]:
return _mime_split_re.split(value.lower())


Expand All @@ -190,10 +210,10 @@ class MIMEAccept(Accept):
mimetypes.
"""

def _specificity(self, value):
def _specificity(self, value: str) -> tuple[bool, ...]:
return tuple(x != "*" for x in _mime_split_re.split(value))

def _value_matches(self, value, item):
def _value_matches(self, value: str, item: str) -> bool:
# item comes from the client, can't match if it's invalid.
if "/" not in item:
return False
Expand Down Expand Up @@ -234,38 +254,42 @@ def _value_matches(self, value, item):
)

@property
def accept_html(self):
def accept_html(self) -> bool:
"""True if this object accepts HTML."""
return (
"text/html" in self or "application/xhtml+xml" in self or self.accept_xhtml
)
return "text/html" in self or self.accept_xhtml # type: ignore[comparison-overlap]

@property
def accept_xhtml(self):
def accept_xhtml(self) -> bool:
"""True if this object accepts XHTML."""
return "application/xhtml+xml" in self or "application/xml" in self
return "application/xhtml+xml" in self or "application/xml" in self # type: ignore[comparison-overlap]

@property
def accept_json(self):
def accept_json(self) -> bool:
"""True if this object accepts JSON."""
return "application/json" in self
return "application/json" in self # type: ignore[comparison-overlap]


_locale_delim_re = re.compile(r"[_-]")


def _normalize_lang(value):
def _normalize_lang(value: str) -> list[str]:
"""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):
def _value_matches(self, value: str, item: str) -> bool:
return item == "*" or _normalize_lang(value) == _normalize_lang(item)

def best_match(self, matches, default=None):
@t.overload
def best_match(self, matches: cabc.Iterable[str]) -> str | None: ...
@t.overload
def best_match(self, matches: cabc.Iterable[str], default: str = ...) -> str: ...
def best_match(
self, matches: cabc.Iterable[str], default: str | None = None
) -> str | None:
"""Given a list of supported values, finds the best match from
the list of accepted values.

Expand Down Expand Up @@ -316,8 +340,8 @@ def best_match(self, matches, default=None):
class CharsetAccept(Accept):
"""Like :class:`Accept` but with normalization for charsets."""

def _value_matches(self, value, item):
def _normalize(name):
def _value_matches(self, value: str, item: str) -> bool:
def _normalize(name: str) -> str:
try:
return codecs.lookup(name).name
except LookupError:
Expand Down
Loading
Loading