diff --git a/CHANGES.rst b/CHANGES.rst index c0afdb481..de9c3a205 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,20 @@ Version 3.2.0 Unreleased +Version 3.1.3 +------------- + +Released 2024-11-08 + +- Initial data passed to ``MultiDict`` and similar interfaces only accepts + ``list``, ``tuple``, or ``set`` when passing multiple values. It had been + changed to accept any ``Collection``, but this matched types that should be + treated as single values, such as ``bytes``. :issue:`2994` +- When the ``Host`` header is not set and ``Request.host`` falls back to the + WSGI ``SERVER_NAME`` value, if that value is an IPv6 address it is wrapped + in ``[]`` to match the ``Host`` header. :issue:`2993` + + Version 3.1.2 ------------- diff --git a/src/werkzeug/datastructures/headers.py b/src/werkzeug/datastructures/headers.py index e8cbdd8e8..1088e3bc9 100644 --- a/src/werkzeug/datastructures/headers.py +++ b/src/werkzeug/datastructures/headers.py @@ -62,7 +62,7 @@ def __init__( defaults: ( Headers | MultiDict[str, t.Any] - | cabc.Mapping[str, t.Any | cabc.Collection[t.Any]] + | cabc.Mapping[str, t.Any | list[t.Any] | tuple[t.Any, ...] | set[t.Any]] | cabc.Iterable[tuple[str, t.Any]] | None ) = None, @@ -227,7 +227,7 @@ def extend( arg: ( Headers | MultiDict[str, t.Any] - | cabc.Mapping[str, t.Any | cabc.Collection[t.Any]] + | cabc.Mapping[str, t.Any | list[t.Any] | tuple[t.Any, ...] | set[t.Any]] | cabc.Iterable[tuple[str, t.Any]] | None ) = None, @@ -491,12 +491,14 @@ def update( arg: ( Headers | MultiDict[str, t.Any] - | cabc.Mapping[str, t.Any | cabc.Collection[t.Any]] + | cabc.Mapping[ + str, t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any] + ] | cabc.Iterable[tuple[str, t.Any]] | None ) = None, /, - **kwargs: t.Any | cabc.Collection[t.Any], + **kwargs: t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any], ) -> None: """Replace headers in this object with items from another headers object and keyword arguments. @@ -516,9 +518,7 @@ def update( self.setlist(key, arg.getlist(key)) elif isinstance(arg, cabc.Mapping): for key, value in arg.items(): - if isinstance(value, cabc.Collection) and not isinstance( - value, str - ): + if isinstance(value, (list, tuple, set)): self.setlist(key, value) else: self.set(key, value) @@ -527,13 +527,16 @@ def update( self.set(key, value) for key, value in kwargs.items(): - if isinstance(value, cabc.Collection) and not isinstance(value, str): + if isinstance(value, (list, tuple, set)): self.setlist(key, value) else: self.set(key, value) def __or__( - self, other: cabc.Mapping[str, t.Any | cabc.Collection[t.Any]] + self, + other: cabc.Mapping[ + str, t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any] + ], ) -> te.Self: if not isinstance(other, cabc.Mapping): return NotImplemented @@ -545,13 +548,11 @@ def __or__( def __ior__( self, other: ( - cabc.Mapping[str, t.Any | cabc.Collection[t.Any]] + cabc.Mapping[str, t.Any | list[t.Any] | tuple[t.Any, ...] | cabc.Set[t.Any]] | cabc.Iterable[tuple[str, t.Any]] ), ) -> te.Self: - if not isinstance(other, (cabc.Mapping, cabc.Iterable)) or isinstance( - other, str - ): + if not isinstance(other, (cabc.Mapping, cabc.Iterable)): return NotImplemented self.update(other) diff --git a/src/werkzeug/datastructures/structures.py b/src/werkzeug/datastructures/structures.py index fcfa160da..dbb7e8048 100644 --- a/src/werkzeug/datastructures/structures.py +++ b/src/werkzeug/datastructures/structures.py @@ -22,7 +22,7 @@ def iter_multi_items( mapping: ( MultiDict[K, V] - | cabc.Mapping[K, V | cabc.Collection[V]] + | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] | cabc.Iterable[tuple[K, V]] ), ) -> cabc.Iterator[tuple[K, V]]: @@ -33,11 +33,11 @@ def iter_multi_items( yield from mapping.items(multi=True) elif isinstance(mapping, cabc.Mapping): for key, value in mapping.items(): - if isinstance(value, cabc.Collection) and not isinstance(value, str): + if isinstance(value, (list, tuple, set)): for v in value: yield key, v else: - yield key, value # type: ignore[misc] + yield key, value else: yield from mapping @@ -182,7 +182,7 @@ def __init__( self, mapping: ( MultiDict[K, V] - | cabc.Mapping[K, V | cabc.Collection[V]] + | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] | cabc.Iterable[tuple[K, V]] | None ) = None, @@ -194,7 +194,7 @@ def __init__( elif isinstance(mapping, cabc.Mapping): tmp = {} for key, value in mapping.items(): - if isinstance(value, cabc.Collection) and not isinstance(value, str): + if isinstance(value, (list, tuple, set)): value = list(value) if not value: @@ -419,7 +419,7 @@ def update( # type: ignore[override] self, mapping: ( MultiDict[K, V] - | cabc.Mapping[K, V | cabc.Collection[V]] + | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] | cabc.Iterable[tuple[K, V]] ), ) -> None: @@ -444,7 +444,7 @@ def update( # type: ignore[override] self.add(key, value) def __or__( # type: ignore[override] - self, other: cabc.Mapping[K, V | cabc.Collection[V]] + self, other: cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] ) -> MultiDict[K, V]: if not isinstance(other, cabc.Mapping): return NotImplemented @@ -455,11 +455,12 @@ def __or__( # type: ignore[override] def __ior__( # type: ignore[override] self, - other: cabc.Mapping[K, V | cabc.Collection[V]] | cabc.Iterable[tuple[K, V]], + other: ( + cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] + | cabc.Iterable[tuple[K, V]] + ), ) -> te.Self: - if not isinstance(other, (cabc.Mapping, cabc.Iterable)) or isinstance( - other, str - ): + if not isinstance(other, (cabc.Mapping, cabc.Iterable)): return NotImplemented self.update(other) @@ -600,7 +601,7 @@ def __init__( self, mapping: ( MultiDict[K, V] - | cabc.Mapping[K, V | cabc.Collection[V]] + | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] | cabc.Iterable[tuple[K, V]] | None ) = None, @@ -744,7 +745,7 @@ def update( # type: ignore[override] self, mapping: ( MultiDict[K, V] - | cabc.Mapping[K, V | cabc.Collection[V]] + | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] | cabc.Iterable[tuple[K, V]] ), ) -> None: @@ -1009,7 +1010,7 @@ def __init__( self, mapping: ( MultiDict[K, V] - | cabc.Mapping[K, V | cabc.Collection[V]] + | cabc.Mapping[K, V | list[V] | tuple[V, ...] | set[V]] | cabc.Iterable[tuple[K, V]] | None ) = None, diff --git a/src/werkzeug/sansio/utils.py b/src/werkzeug/sansio/utils.py index 14fa0ac88..ff7ceda34 100644 --- a/src/werkzeug/sansio/utils.py +++ b/src/werkzeug/sansio/utils.py @@ -71,6 +71,9 @@ def get_host( :return: Host, with port if necessary. :raise ~werkzeug.exceptions.SecurityError: If the host is not trusted. + + .. versionchanged:: 3.1.3 + If ``SERVER_NAME`` is IPv6, it is wrapped in ``[]``. """ host = "" @@ -79,6 +82,11 @@ def get_host( elif server is not None: host = server[0] + # If SERVER_NAME is IPv6, wrap it in [] to match Host header. + # Check for : because domain or IPv4 can't have that. + if ":" in host and host[0] != "[": + host = f"[{host}]" + if server[1] is not None: host = f"{host}:{server[1]}" diff --git a/tests/sansio/test_utils.py b/tests/sansio/test_utils.py index d43de66c2..a63e7c660 100644 --- a/tests/sansio/test_utils.py +++ b/tests/sansio/test_utils.py @@ -14,12 +14,16 @@ ("https", "spam", None, "spam"), ("https", "spam:443", None, "spam"), ("http", "spam:8080", None, "spam:8080"), + ("http", "127.0.0.1:8080", None, "127.0.0.1:8080"), + ("http", "[::1]:8080", None, "[::1]:8080"), ("ws", "spam", None, "spam"), ("ws", "spam:80", None, "spam"), ("wss", "spam", None, "spam"), ("wss", "spam:443", None, "spam"), ("http", None, ("spam", 80), "spam"), ("http", None, ("spam", 8080), "spam:8080"), + ("http", None, ("127.0.0.1", 8080), "127.0.0.1:8080"), + ("http", None, ("::1", 8080), "[::1]:8080"), ("http", None, ("unix/socket", None), "unix/socket"), ("http", "spam", ("eggs", 80), "spam"), ], diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py index dcbc79697..0cd497438 100644 --- a/tests/test_datastructures.py +++ b/tests/test_datastructures.py @@ -1,6 +1,9 @@ +from __future__ import annotations + import io import pickle import tempfile +import typing as t from contextlib import contextmanager from copy import copy from copy import deepcopy @@ -43,7 +46,7 @@ def items(self, multi=1): class _MutableMultiDictTests: - storage_class: type["ds.MultiDict"] + storage_class: type[ds.MultiDict] def test_pickle(self): cls = self.storage_class @@ -1280,3 +1283,15 @@ def test_range_to_header(ranges): def test_range_validates_ranges(ranges): with pytest.raises(ValueError): ds.Range("bytes", ranges) + + +@pytest.mark.parametrize( + ("value", "expect"), + [ + ({"a": "ab"}, [("a", "ab")]), + ({"a": ["a", "b"]}, [("a", "a"), ("a", "b")]), + ({"a": b"ab"}, [("a", b"ab")]), + ], +) +def test_iter_multi_data(value: t.Any, expect: list[tuple[t.Any, t.Any]]) -> None: + assert list(ds.iter_multi_items(value)) == expect