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 %}
-
+
{% for nav_item in nav %}
{% set path = "__nav_" ~ loop.index %}
- {% set level = 1 %}
- {% include "partials/nav-item.html" %}
+ {{ item.render(nav_item, path, 1) }}
{% endfor %}
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):