Skip to content

Commit 82cd228

Browse files
authored
Merge branch 'master' into version/1.0.0
2 parents 06e9809 + b8eebef commit 82cd228

File tree

7 files changed

+75
-11
lines changed

7 files changed

+75
-11
lines changed

docs/overrides/partials/nav.html

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{% import "partials/nav-item.html" as item with context %}
2+
13
<!-- Determine class according to configuration -->
24
{% set class = "md-nav md-nav--primary" %}
35
{% if "navigation.tabs" in features %}
@@ -35,12 +37,11 @@
3537
</div>
3638
{% endif %}
3739

38-
<!-- Render item list -->
40+
<!-- Navigation list -->
3941
<ul class="md-nav__list" data-md-scrollfix>
4042
{% for nav_item in nav %}
4143
{% set path = "__nav_" ~ loop.index %}
42-
{% set level = 1 %}
43-
{% include "partials/nav-item.html" %}
44+
{{ item.render(nav_item, path, 1) }}
4445
{% endfor %}
4546
</ul>
4647

docs/release-notes.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@ All deprecated features have been removed.
1414
* Removed `ExceptionMiddleware` from the `exceptions` module, it can now be found in the `middleware.exceptions` module.
1515
* Removed multiple possible argument sequences from `TemplateResponse`.
1616

17+
## 0.36.1
18+
19+
January 23, 2024
20+
21+
#### Fixed
22+
23+
* Check if "extensions" in scope before checking the extension [#2438](https://github.com/encode/starlette/pull/2438).
24+
25+
## 0.36.0
26+
27+
January 22, 2024
28+
29+
#### Added
30+
31+
* Add support for ASGI `pathsend` extension [#2435](https://github.com/encode/starlette/pull/2435).
32+
* Cancel `WebSocketTestSession` on close [#2427](https://github.com/encode/starlette/pull/2427).
33+
* Raise `WebSocketDisconnect` when `WebSocket.send()` excepts `IOError` [#2425](https://github.com/encode/starlette/pull/2425).
34+
* Raise `FileNotFoundError` when the `env_file` parameter on `Config` is not valid [#2422](https://github.com/encode/starlette/pull/2422).
35+
1736
## 0.35.1
1837

1938
January 11, 2024
@@ -190,7 +209,7 @@ March 9, 2023
190209
March 9, 2023
191210

192211
### Added
193-
* Support [lifespan state](/lifespan/) [#2060](https://github.com/encode/starlette/pull/2060),
212+
* Support [lifespan state](lifespan.md) [#2060](https://github.com/encode/starlette/pull/2060),
194213
[#2065](https://github.com/encode/starlette/pull/2065) and [#2064](https://github.com/encode/starlette/pull/2064).
195214

196215
### Changed

docs/responses.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Signature: `Response(content, status_code=200, headers=None, media_type=None)`
1313

1414
Starlette will automatically include a Content-Length header. It will also
1515
include a Content-Type header, based on the media_type and appending a charset
16-
for text types.
16+
for text types, unless a charset has already been specified in the `media_type`.
1717

1818
Once you've instantiated a response, you can send it by calling it as an
1919
ASGI application instance.

docs/testclient.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ case you should use `client = TestClient(app, raise_server_exceptions=False)`.
6767
If you want the `TestClient` to run the `lifespan` handler,
6868
you will need to use the `TestClient` as a context manager. It will
6969
not be triggered when the `TestClient` is instantiated. You can learn more about it
70-
[here](/lifespan/#running-lifespan-in-tests).
70+
[here](lifespan.md#running-lifespan-in-tests).
7171

7272
### Selecting the Async backend
7373

requirements.txt

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,20 @@
22
-e .[full]
33

44
# Testing
5-
coverage==7.4.0
5+
coverage==7.4.1
66
importlib-metadata==7.0.1
77
mypy==1.8.0
8-
ruff==0.1.13
8+
ruff==0.1.15
99
typing_extensions==4.9.0
1010
types-contextvars==2.4.7.3
1111
types-PyYAML==6.0.12.12
1212
types-dataclasses==0.6.6
13-
pytest==7.4.4
13+
pytest==8.0.0
1414
trio==0.24.0
1515

1616
# Documentation
1717
mkdocs==1.5.3
18-
mkdocs-material==9.5.3
18+
mkdocs-material==9.5.6
1919
mkautodoc==0.2.0
2020

2121
# Packaging

starlette/responses.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ def init_headers(self, headers: typing.Mapping[str, str] | None = None) -> None:
7272

7373
content_type = self.media_type
7474
if content_type is not None and populate_content_type:
75-
if content_type.startswith("text/"):
75+
if (
76+
content_type.startswith("text/")
77+
and "charset=" not in content_type.lower()
78+
):
7679
content_type += "; charset=" + self.charset
7780
raw_headers.append((b"content-type", content_type.encode("latin-1")))
7881

@@ -330,6 +333,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
330333
)
331334
if scope["method"].upper() == "HEAD":
332335
await send({"type": "http.response.body", "body": b"", "more_body": False})
336+
elif "extensions" in scope and "http.response.pathsend" in scope["extensions"]:
337+
await send({"type": "http.response.pathsend", "path": str(self.path)})
333338
else:
334339
async with await anyio.open_file(self.path, mode="rb") as file:
335340
more_body = True

tests/test_responses.py

+39
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,38 @@ def test_file_response_with_inline_disposition(tmpdir, test_client_factory):
324324
assert response.headers["content-disposition"] == expected_disposition
325325

326326

327+
@pytest.mark.anyio
328+
async def test_file_response_with_pathsend(tmpdir: Path):
329+
path = os.path.join(tmpdir, "xyz")
330+
content = b"<file content>" * 1000
331+
with open(path, "wb") as file:
332+
file.write(content)
333+
334+
app = FileResponse(path=path, filename="example.png")
335+
336+
async def receive() -> Message: # type: ignore[empty-body]
337+
... # pragma: no cover
338+
339+
async def send(message: Message) -> None:
340+
if message["type"] == "http.response.start":
341+
assert message["status"] == status.HTTP_200_OK
342+
headers = Headers(raw=message["headers"])
343+
assert headers["content-type"] == "image/png"
344+
assert "content-length" in headers
345+
assert "content-disposition" in headers
346+
assert "last-modified" in headers
347+
assert "etag" in headers
348+
elif message["type"] == "http.response.pathsend":
349+
assert message["path"] == str(path)
350+
351+
# Since the TestClient doesn't support `pathsend`, we need to test this directly.
352+
await app(
353+
{"type": "http", "method": "get", "extensions": {"http.response.pathsend": {}}},
354+
receive,
355+
send,
356+
)
357+
358+
327359
def test_set_cookie(test_client_factory, monkeypatch):
328360
# Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
329361
mocked_now = dt.datetime(2037, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
@@ -436,6 +468,13 @@ def test_non_empty_response(test_client_factory):
436468
assert response.headers["content-length"] == "2"
437469

438470

471+
def test_response_do_not_add_redundant_charset(test_client_factory):
472+
app = Response(media_type="text/plain; charset=utf-8")
473+
client = test_client_factory(app)
474+
response = client.get("/")
475+
assert response.headers["content-type"] == "text/plain; charset=utf-8"
476+
477+
439478
def test_file_response_known_size(tmpdir, test_client_factory):
440479
path = os.path.join(tmpdir, "xyz")
441480
content = b"<file content>" * 1000

0 commit comments

Comments
 (0)