Skip to content

Commit

Permalink
Merge branch 'stable'
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism committed Nov 8, 2024
2 parents 9a69323 + 6b56ed5 commit 7868bef
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 28 deletions.
14 changes: 14 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------

Expand Down
27 changes: 14 additions & 13 deletions src/werkzeug/datastructures/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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)
Expand Down
29 changes: 15 additions & 14 deletions src/werkzeug/datastructures/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions src/werkzeug/sansio/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""

Expand All @@ -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]}"

Expand Down
4 changes: 4 additions & 0 deletions tests/sansio/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
],
Expand Down
17 changes: 16 additions & 1 deletion tests/test_datastructures.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 7868bef

Please sign in to comment.