Skip to content

Commit

Permalink
Merge branch '3.0.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
pgjones committed Apr 1, 2024
2 parents 32e6951 + d70dcea commit 6f461e9
Show file tree
Hide file tree
Showing 13 changed files with 79 additions and 14 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/publish.yaml
Expand Up @@ -33,7 +33,7 @@ jobs:
id-token: write
contents: write
# Can't pin with hash due to how this workflow works.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0
with:
base64-subjects: ${{ needs.build.outputs.hash }}
create-release:
Expand Down
14 changes: 14 additions & 0 deletions CHANGES.rst
Expand Up @@ -7,6 +7,20 @@ Unreleased

- Support Cookie CHIPS (Partitioned Cookies). :issue:`2797`

Version 3.0.2
-------------

Released 2024-04-01

- Ensure setting merge_slashes to False results in NotFound for
repeated-slash requests against single slash routes. :issue:`2834`
- Fix handling of TypeError in TypeConversionDict.get() to match
ValueErrors. :issue:`2843`
- Fix response_wrapper type check in test client. :issue:`2831`
- Make the return type of ``MultiPartParser.parse`` more
precise. :issue:`2840`
- Raise an error if converter arguments cannot be
parsed. :issue:`2822`

Version 3.0.1
-------------
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "Werkzeug"
version = "3.0.1"
version = "3.0.2"
description = "The comprehensive WSGI web application library."
readme = "README.rst"
license = {file = "LICENSE.rst"}
Expand Down
10 changes: 7 additions & 3 deletions src/werkzeug/datastructures/structures.py
Expand Up @@ -70,8 +70,12 @@ def get(self, key, default=None, type=None):
be looked up. If not further specified `None` is
returned.
:param type: A callable that is used to cast the value in the
:class:`MultiDict`. If a :exc:`ValueError` is raised
by this callable the default value is returned.
:class:`MultiDict`. If a :exc:`ValueError` or a
:exc:`TypeError` is raised by this callable the default
value is returned.
.. versionchanged:: 3.0.2
Returns the default value on :exc:`TypeError`, too.
"""
try:
rv = self[key]
Expand All @@ -80,7 +84,7 @@ def get(self, key, default=None, type=None):
if type is not None:
try:
rv = type(rv)
except ValueError:
except (ValueError, TypeError):
rv = default
return rv

Expand Down
2 changes: 1 addition & 1 deletion src/werkzeug/formparser.py
Expand Up @@ -352,7 +352,7 @@ def start_file_streaming(

def parse(
self, stream: t.IO[bytes], boundary: bytes, content_length: int | None
) -> tuple[MultiDict, MultiDict]:
) -> tuple[MultiDict[str, str], MultiDict[str, FileStorage]]:
current_part: Field | File
container: t.IO[bytes] | list[bytes]
_write: t.Callable[[bytes], t.Any]
Expand Down
9 changes: 8 additions & 1 deletion src/werkzeug/routing/map.py
Expand Up @@ -109,7 +109,6 @@ def __init__(

self.default_subdomain = default_subdomain
self.strict_slashes = strict_slashes
self.merge_slashes = merge_slashes
self.redirect_defaults = redirect_defaults
self.host_matching = host_matching

Expand All @@ -123,6 +122,14 @@ def __init__(
for rulefactory in rules or ():
self.add(rulefactory)

@property
def merge_slashes(self) -> bool:
return self._matcher.merge_slashes

@merge_slashes.setter
def merge_slashes(self, value: bool) -> None:
self._matcher.merge_slashes = value

def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool:
"""Iterate over all rules and check if the endpoint expects
the arguments provided. This is for example useful if you have
Expand Down
2 changes: 1 addition & 1 deletion src/werkzeug/routing/matcher.py
Expand Up @@ -177,7 +177,7 @@ def _match(
rv = _match(self._root, [domain, *path.split("/")], [])
except SlashRequired:
raise RequestPath(f"{path}/") from None
if rv is None:
if rv is None or rv[0].merge_slashes is False:
raise NoMatch(have_match_for, websocket_mismatch)
else:
raise RequestPath(f"{path}")
Expand Down
8 changes: 8 additions & 0 deletions src/werkzeug/routing/rules.py
Expand Up @@ -67,6 +67,7 @@ class RulePart:
_simple_rule_re = re.compile(r"<([^>]+)>")
_converter_args_re = re.compile(
r"""
\s*
((?P<name>\w+)\s*=\s*)?
(?P<value>
True|False|
Expand Down Expand Up @@ -112,8 +113,14 @@ def parse_converter_args(argstr: str) -> tuple[tuple[t.Any, ...], dict[str, t.An
argstr += ","
args = []
kwargs = {}
position = 0

for item in _converter_args_re.finditer(argstr):
if item.start() != position:
raise ValueError(
f"Cannot parse converter argument '{argstr[position:item.start()]}'"
)

value = item.group("stringval")
if value is None:
value = item.group("value")
Expand All @@ -123,6 +130,7 @@ def parse_converter_args(argstr: str) -> tuple[tuple[t.Any, ...], dict[str, t.An
else:
name = item.group("name")
kwargs[name] = value
position = item.end()

return tuple(args), kwargs

Expand Down
6 changes: 4 additions & 2 deletions src/werkzeug/test.py
Expand Up @@ -809,10 +809,12 @@ def __init__(

if response_wrapper in {None, Response}:
response_wrapper = TestResponse
elif not isinstance(response_wrapper, TestResponse):
elif response_wrapper is not None and not issubclass(
response_wrapper, TestResponse
):
response_wrapper = type(
"WrapperTestResponse",
(TestResponse, response_wrapper), # type: ignore
(TestResponse, response_wrapper),
{},
)

Expand Down
3 changes: 2 additions & 1 deletion tests/test_datastructures.py
Expand Up @@ -550,8 +550,9 @@ def test_value_conversion(self):
assert d.get("foo", type=int) == 1

def test_return_default_when_conversion_is_not_possible(self):
d = self.storage_class(foo="bar")
d = self.storage_class(foo="bar", baz=None)
assert d.get("foo", default=-1, type=int) == -1
assert d.get("baz", default=-1, type=int) == -1

def test_propagate_exceptions_in_conversion(self):
d = self.storage_class(foo="bar")
Expand Down
6 changes: 3 additions & 3 deletions tests/test_exceptions.py
Expand Up @@ -7,7 +7,7 @@
from werkzeug import exceptions
from werkzeug.datastructures import Headers
from werkzeug.datastructures import WWWAuthenticate
from werkzeug.exceptions import HTTPException
from werkzeug.exceptions import default_exceptions, HTTPException
from werkzeug.wrappers import Response


Expand Down Expand Up @@ -138,7 +138,7 @@ def test_retry_after_mixin(cls, value, expect):
@pytest.mark.parametrize(
"cls",
sorted(
(e for e in HTTPException.__subclasses__() if e.code and e.code >= 400),
(e for e in default_exceptions.values() if e.code and e.code >= 400),
key=lambda e: e.code, # type: ignore
),
)
Expand All @@ -158,7 +158,7 @@ def test_description_none():
@pytest.mark.parametrize(
"cls",
sorted(
(e for e in HTTPException.__subclasses__() if e.code),
(e for e in default_exceptions.values() if e.code),
key=lambda e: e.code, # type: ignore
),
)
Expand Down
7 changes: 7 additions & 0 deletions tests/test_routing.py
Expand Up @@ -95,6 +95,7 @@ def test_merge_slashes_match():
r.Rule("/yes/tail/", endpoint="yes_tail"),
r.Rule("/with/<path:path>", endpoint="with_path"),
r.Rule("/no//merge", endpoint="no_merge", merge_slashes=False),
r.Rule("/no/merging", endpoint="no_merging", merge_slashes=False),
]
)
adapter = url_map.bind("localhost", "/")
Expand Down Expand Up @@ -124,6 +125,9 @@ def test_merge_slashes_match():

assert adapter.match("/no//merge")[0] == "no_merge"

assert adapter.match("/no/merging")[0] == "no_merging"
pytest.raises(NotFound, lambda: adapter.match("/no//merging"))


@pytest.mark.parametrize(
("path", "expected"),
Expand Down Expand Up @@ -1072,6 +1076,9 @@ def test_converter_parser():
args, kwargs = r.parse_converter_args('"foo", "bar"')
assert args == ("foo", "bar")

with pytest.raises(ValueError):
r.parse_converter_args("min=0;max=500")


def test_alias_redirects():
m = r.Map(
Expand Down
22 changes: 22 additions & 0 deletions tests/test_test.py
Expand Up @@ -16,6 +16,7 @@
from werkzeug.test import EnvironBuilder
from werkzeug.test import run_wsgi_app
from werkzeug.test import stream_encode_multipart
from werkzeug.test import TestResponse
from werkzeug.utils import redirect
from werkzeug.wrappers import Request
from werkzeug.wrappers import Response
Expand Down Expand Up @@ -903,3 +904,24 @@ def test_no_content_type_header_addition():
c = Client(no_response_headers_app)
response = c.open()
assert response.headers == Headers([("Content-Length", "8")])


def test_client_response_wrapper():
class CustomResponse(Response):
pass

class CustomTestResponse(TestResponse, Response):
pass

c1 = Client(Response(), CustomResponse)
r1 = c1.open()

assert isinstance(r1, CustomResponse)
assert type(r1) is not CustomResponse # Got subclassed
assert issubclass(type(r1), CustomResponse)

c2 = Client(Response(), CustomTestResponse)
r2 = c2.open()

assert isinstance(r2, CustomTestResponse)
assert type(r2) is CustomTestResponse # Did not get subclassed

0 comments on commit 6f461e9

Please sign in to comment.