diff --git a/docs/overrides/partials/nav.html b/docs/overrides/partials/nav.html index d4684d0a6..93d5c8c86 100644 --- a/docs/overrides/partials/nav.html +++ b/docs/overrides/partials/nav.html @@ -1,3 +1,5 @@ +{% import "partials/nav-item.html" as item with context %} + {% set class = "md-nav md-nav--primary" %} {% if "navigation.tabs" in features %} @@ -35,12 +37,11 @@ {% endif %} - + diff --git a/docs/release-notes.md b/docs/release-notes.md index d68f29a69..020413ce2 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,3 +1,12 @@ +## 0.36.2 + +February 3, 2024 + +#### Fixed + +* Upgrade `python-multipart` to `0.0.7` [13e5c26](13e5c26a27f4903924624736abd6131b2da80cc5). +* Avoid duplicate charset on `Content-Type` [#2443](https://github.com/encode/starlette/2443). + ## 0.36.1 January 23, 2024 diff --git a/docs/responses.md b/docs/responses.md index b76d483b1..ae48d2f14 100644 --- a/docs/responses.md +++ b/docs/responses.md @@ -13,7 +13,7 @@ Signature: `Response(content, status_code=200, headers=None, media_type=None)` Starlette will automatically include a Content-Length header. It will also include a Content-Type header, based on the media_type and appending a charset -for text types. +for text types, unless a charset has already been specified in the `media_type`. Once you've instantiated a response, you can send it by calling it as an ASGI application instance. diff --git a/pyproject.toml b/pyproject.toml index 9909fb86a..48562e714 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ dependencies = [ full = [ "itsdangerous", "jinja2", - "python-multipart", + "python-multipart>=0.0.7", "pyyaml", "httpx>=0.22.0", ] diff --git a/requirements.txt b/requirements.txt index 5fd635b07..d864321a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,20 +2,20 @@ -e .[full] # Testing -coverage==7.4.0 +coverage==7.4.1 importlib-metadata==7.0.1 mypy==1.8.0 -ruff==0.1.13 +ruff==0.1.15 typing_extensions==4.9.0 types-contextvars==2.4.7.3 types-PyYAML==6.0.12.12 types-dataclasses==0.6.6 -pytest==7.4.4 +pytest==8.0.0 trio==0.24.0 # Documentation mkdocs==1.5.3 -mkdocs-material==9.1.17 +mkdocs-material==9.5.6 mkautodoc==0.2.0 # Packaging diff --git a/starlette/__init__.py b/starlette/__init__.py index ef5853c9e..012876414 100644 --- a/starlette/__init__.py +++ b/starlette/__init__.py @@ -1 +1 @@ -__version__ = "0.36.1" +__version__ = "0.36.2" diff --git a/starlette/responses.py b/starlette/responses.py index 4fb7697b8..e613e98b2 100644 --- a/starlette/responses.py +++ b/starlette/responses.py @@ -73,7 +73,10 @@ def init_headers(self, headers: typing.Mapping[str, str] | None = None) -> None: content_type = self.media_type if content_type is not None and populate_content_type: - if content_type.startswith("text/"): + if ( + content_type.startswith("text/") + and "charset=" not in content_type.lower() + ): content_type += "; charset=" + self.charset raw_headers.append((b"content-type", content_type.encode("latin-1"))) diff --git a/tests/test_responses.py b/tests/test_responses.py index 625a12956..291c46e6d 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -473,6 +473,13 @@ def test_non_empty_response(test_client_factory): assert response.headers["content-length"] == "2" +def test_response_do_not_add_redundant_charset(test_client_factory): + app = Response(media_type="text/plain; charset=utf-8") + client = test_client_factory(app) + response = client.get("/") + assert response.headers["content-type"] == "text/plain; charset=utf-8" + + def test_file_response_known_size(tmpdir, test_client_factory): path = os.path.join(tmpdir, "xyz") content = b"" * 1000 diff --git a/tests/test_routing.py b/tests/test_routing.py index 128f06674..dd7083e81 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -684,27 +684,30 @@ def run_shutdown(): # pragma: no cover nonlocal shutdown_called shutdown_called = True - with pytest.warns( - UserWarning, - match=( - "The `lifespan` parameter cannot be used with `on_startup` or `on_shutdown`." # noqa: E501 - ), + with pytest.deprecated_call( + match="The on_startup and on_shutdown parameters are deprecated" ): - app = Router( - on_startup=[run_startup], on_shutdown=[run_shutdown], lifespan=lifespan - ) + with pytest.warns( + UserWarning, + match=( + "The `lifespan` parameter cannot be used with `on_startup` or `on_shutdown`." # noqa: E501 + ), + ): + app = Router( + on_startup=[run_startup], on_shutdown=[run_shutdown], lifespan=lifespan + ) - assert not lifespan_called - assert not startup_called - assert not shutdown_called + assert not lifespan_called + assert not startup_called + assert not shutdown_called - # Triggers the lifespan events - with test_client_factory(app): - ... + # Triggers the lifespan events + with test_client_factory(app): + ... - assert lifespan_called - assert not startup_called - assert not shutdown_called + assert lifespan_called + assert not startup_called + assert not shutdown_called def test_lifespan_sync(test_client_factory):