From 1a1728ed88939ca68928dade168e1989be062c6f Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 6 Nov 2024 08:15:46 -0800 Subject: [PATCH 1/4] start version 3.1.3 --- CHANGES.rst | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8388dc4c4..75f01962b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,11 @@ .. currentmodule:: werkzeug +Version 3.1.3 +------------- + +Unreleased + + Version 3.1.2 ------------- diff --git a/pyproject.toml b/pyproject.toml index fabf98579..8d4e687eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Werkzeug" -version = "3.1.2" +version = "3.1.3.dev" description = "The comprehensive WSGI web application library." readme = "README.md" license = {file = "LICENSE.txt"} From 598bb1de78678107404493e56d68003910e1dbea Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 6 Nov 2024 11:49:36 -0800 Subject: [PATCH 2/4] restrict containers accepted by multi --- CHANGES.rst | 5 ++++ src/werkzeug/datastructures/headers.py | 27 +++++++++++---------- src/werkzeug/datastructures/structures.py | 29 ++++++++++++----------- tests/test_datastructures.py | 17 ++++++++++++- 4 files changed, 50 insertions(+), 28 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 75f01962b..6a2fd2ed4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,11 @@ Version 3.1.3 Unreleased +- 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` + 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/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 From d99f72d12698d86e7ebcb894f3c6c729e2b6c067 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 7 Nov 2024 08:01:56 -0800 Subject: [PATCH 3/4] wrap IPv6 SERVER_NAME in [] --- CHANGES.rst | 3 +++ src/werkzeug/sansio/utils.py | 8 ++++++++ tests/sansio/test_utils.py | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6a2fd2ed4..27a89e83e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,9 @@ Unreleased ``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/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"), ], From 6389612fd1ee1bd93579eed5026e8fd471d04abd Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 8 Nov 2024 07:46:09 -0800 Subject: [PATCH 4/4] release version 3.1.3 --- CHANGES.rst | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 27a89e83e..de3f2b7c9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ Version 3.1.3 ------------- -Unreleased +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 diff --git a/pyproject.toml b/pyproject.toml index 8d4e687eb..2d5a6cee2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "Werkzeug" -version = "3.1.3.dev" +version = "3.1.3" description = "The comprehensive WSGI web application library." readme = "README.md" license = {file = "LICENSE.txt"}