Skip to content

Commit

Permalink
Merge branch 'stable'
Browse files Browse the repository at this point in the history
  • Loading branch information
davidism committed Oct 24, 2024
2 parents ff507c5 + 5661b96 commit 07cf537
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 8 deletions.
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ Unreleased
some typing issues on cache control. :issue:`2881`


Version 3.0.5
-------------

Unreleased

- The Watchdog reloader ignores file closed no write events. :issue:`2945`
- Logging works with client addresses containing an IPv6 scope :issue:`2952`
- Ignore invalid authorization parameters. :issue:`2955`
- Improve type annotation fore ``SharedDataMiddleware``. :issue:`2958`


Version 3.0.4
-------------

Expand Down
15 changes: 13 additions & 2 deletions src/werkzeug/_reloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,11 @@ def run_step(self) -> None:

class WatchdogReloaderLoop(ReloaderLoop):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
from watchdog.events import EVENT_TYPE_OPENED
from watchdog.events import EVENT_TYPE_CLOSED
from watchdog.events import EVENT_TYPE_CREATED
from watchdog.events import EVENT_TYPE_DELETED
from watchdog.events import EVENT_TYPE_MODIFIED
from watchdog.events import EVENT_TYPE_MOVED
from watchdog.events import FileModifiedEvent
from watchdog.events import PatternMatchingEventHandler
from watchdog.observers import Observer
Expand All @@ -322,7 +326,14 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:

class EventHandler(PatternMatchingEventHandler):
def on_any_event(self, event: FileModifiedEvent): # type: ignore
if event.event_type == EVENT_TYPE_OPENED:
if event.event_type not in {
EVENT_TYPE_CLOSED,
EVENT_TYPE_CREATED,
EVENT_TYPE_DELETED,
EVENT_TYPE_MODIFIED,
EVENT_TYPE_MOVED,
}:
# skip events that don't involve changes to the file
return

trigger_reload(event.src_path)
Expand Down
4 changes: 4 additions & 0 deletions src/werkzeug/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,10 @@ def parse_dict_header(value: str) -> dict[str, str | None]:
key, has_value, value = item.partition("=")
key = key.strip()

if not key:
# =value is not valid
continue

if not has_value:
result[key] = None
continue
Expand Down
5 changes: 3 additions & 2 deletions src/werkzeug/middleware/shared_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from __future__ import annotations

import collections.abc as cabc
import importlib.util
import mimetypes
import os
Expand Down Expand Up @@ -103,7 +104,7 @@ def __init__(
self,
app: WSGIApplication,
exports: (
dict[str, str | tuple[str, str]]
cabc.Mapping[str, str | tuple[str, str]]
| t.Iterable[tuple[str, str | tuple[str, str]]]
),
disallow: None = None,
Expand All @@ -116,7 +117,7 @@ def __init__(
self.cache = cache
self.cache_timeout = cache_timeout

if isinstance(exports, dict):
if isinstance(exports, cabc.Mapping):
exports = exports.items()

for key, value in exports:
Expand Down
4 changes: 3 additions & 1 deletion src/werkzeug/serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,11 @@ def log_message(self, format: str, *args: t.Any) -> None:
self.log("info", format, *args)

def log(self, type: str, message: str, *args: t.Any) -> None:
# an IPv6 scoped address contains "%" which breaks logging
address_string = self.address_string().replace("%", "%%")
_log(
type,
f"{self.address_string()} - - [{self.log_date_time_string()}] {message}\n",
f"{address_string} - - [{self.log_date_time_string()}] {message}\n",
*args,
)

Expand Down
5 changes: 5 additions & 0 deletions tests/live_apps/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from werkzeug.serving import generate_adhoc_ssl_context
from werkzeug.serving import run_simple
from werkzeug.serving import WSGIRequestHandler
from werkzeug.wrappers import Request
from werkzeug.wrappers import Response

Expand All @@ -23,10 +24,14 @@ def app(request):
kwargs.update(hostname="127.0.0.1", port=5000, application=app)
kwargs.update(json.loads(sys.argv[2]))
ssl_context = kwargs.get("ssl_context")
override_client_addr = kwargs.pop("override_client_addr", None)

if ssl_context == "custom":
kwargs["ssl_context"] = generate_adhoc_ssl_context()
elif isinstance(ssl_context, list):
kwargs["ssl_context"] = tuple(ssl_context)

if override_client_addr:
WSGIRequestHandler.address_string = lambda _: override_client_addr

run_simple(**kwargs)
17 changes: 14 additions & 3 deletions tests/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,16 @@ def test_set_header(self):
def test_list_header(self, value, expect):
assert http.parse_list_header(value) == expect

def test_dict_header(self):
d = http.parse_dict_header('foo="bar baz", blah=42')
assert d == {"foo": "bar baz", "blah": "42"}
@pytest.mark.parametrize(
("value", "expect"),
[
('foo="bar baz", blah=42', {"foo": "bar baz", "blah": "42"}),
("foo, bar=", {"foo": None, "bar": ""}),
("=foo, =", {}),
],
)
def test_dict_header(self, value, expect):
assert http.parse_dict_header(value) == expect

def test_cache_control_header(self):
cc = http.parse_cache_control_header("max-age=0, no-cache")
Expand Down Expand Up @@ -204,6 +211,10 @@ def test_authorization_header(self):
assert Authorization.from_header(None) is None
assert Authorization.from_header("foo").type == "foo"

def test_authorization_ignore_invalid_parameters(self):
a = Authorization.from_header("Digest foo, bar=, =qux, =")
assert a.to_header() == 'Digest foo, bar=""'

def test_authorization_token_padding(self):
# padded with =
token = base64.b64encode(b"This has base64 padding").decode()
Expand Down
34 changes: 34 additions & 0 deletions tests/test_serving.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from unittest.mock import patch

import pytest
from watchdog import version as watchdog_version
from watchdog.events import EVENT_TYPE_MODIFIED
from watchdog.events import EVENT_TYPE_OPENED
from watchdog.events import FileModifiedEvent
Expand Down Expand Up @@ -136,6 +137,28 @@ def test_watchdog_reloader_ignores_opened(mock_trigger_reload):
reloader.trigger_reload.assert_not_called()


@pytest.mark.skipif(
watchdog_version.VERSION_MAJOR < 5,
reason="'closed no write' event introduced in watchdog 5.0",
)
@patch.object(WatchdogReloaderLoop, "trigger_reload")
def test_watchdog_reloader_ignores_closed_no_write(mock_trigger_reload):
from watchdog.events import EVENT_TYPE_CLOSED_NO_WRITE

reloader = WatchdogReloaderLoop()
modified_event = FileModifiedEvent("")
modified_event.event_type = EVENT_TYPE_MODIFIED
reloader.event_handler.on_any_event(modified_event)
mock_trigger_reload.assert_called_once()

reloader.trigger_reload.reset_mock()

opened_event = FileModifiedEvent("")
opened_event.event_type = EVENT_TYPE_CLOSED_NO_WRITE
reloader.event_handler.on_any_event(opened_event)
reloader.trigger_reload.assert_not_called()


@pytest.mark.skipif(sys.version_info >= (3, 10), reason="not needed on >= 3.10")
def test_windows_get_args_for_reloading(monkeypatch, tmp_path):
argv = [str(tmp_path / "test.exe"), "run"]
Expand Down Expand Up @@ -314,3 +337,14 @@ def test_streaming_chunked_truncation(dev_server):
"""
with pytest.raises(http.client.IncompleteRead):
dev_server("streaming", threaded=True).request("/crash")


@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
@pytest.mark.dev_server
def test_host_with_ipv6_scope(dev_server):
client = dev_server(override_client_addr="fe80::1ff:fe23:4567:890a%eth2")
r = client.request("/crash")

assert r.status == 500
assert b"Internal Server Error" in r.data
assert "Logging error" not in client.log.read()

0 comments on commit 07cf537

Please sign in to comment.