Skip to content

Commit

Permalink
Merge branch 'master' into version/1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
Kludex committed Feb 3, 2024
2 parents 06e9809 + b8eebef commit 82cd228
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 11 deletions.
7 changes: 4 additions & 3 deletions docs/overrides/partials/nav.html
@@ -1,3 +1,5 @@
{% import "partials/nav-item.html" as item with context %}

<!-- Determine class according to configuration -->
{% set class = "md-nav md-nav--primary" %}
{% if "navigation.tabs" in features %}
Expand Down Expand Up @@ -35,12 +37,11 @@
</div>
{% endif %}

<!-- Render item list -->
<!-- Navigation list -->
<ul class="md-nav__list" data-md-scrollfix>
{% 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 %}
</ul>

Expand Down
21 changes: 20 additions & 1 deletion docs/release-notes.md
Expand Up @@ -14,6 +14,25 @@ All deprecated features have been removed.
* Removed `ExceptionMiddleware` from the `exceptions` module, it can now be found in the `middleware.exceptions` module.
* Removed multiple possible argument sequences from `TemplateResponse`.

## 0.36.1

January 23, 2024

#### Fixed

* Check if "extensions" in scope before checking the extension [#2438](http://github.com/encode/starlette/pull/2438).

## 0.36.0

January 22, 2024

#### Added

* Add support for ASGI `pathsend` extension [#2435](http://github.com/encode/starlette/pull/2435).
* Cancel `WebSocketTestSession` on close [#2427](http://github.com/encode/starlette/pull/2427).
* Raise `WebSocketDisconnect` when `WebSocket.send()` excepts `IOError` [#2425](http://github.com/encode/starlette/pull/2425).
* Raise `FileNotFoundError` when the `env_file` parameter on `Config` is not valid [#2422](http://github.com/encode/starlette/pull/2422).

## 0.35.1

January 11, 2024
Expand Down Expand Up @@ -190,7 +209,7 @@ March 9, 2023
March 9, 2023

### Added
* Support [lifespan state](/lifespan/) [#2060](https://github.com/encode/starlette/pull/2060),
* Support [lifespan state](lifespan.md) [#2060](https://github.com/encode/starlette/pull/2060),
[#2065](https://github.com/encode/starlette/pull/2065) and [#2064](https://github.com/encode/starlette/pull/2064).

### Changed
Expand Down
2 changes: 1 addition & 1 deletion docs/responses.md
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/testclient.md
Expand Up @@ -67,7 +67,7 @@ case you should use `client = TestClient(app, raise_server_exceptions=False)`.
If you want the `TestClient` to run the `lifespan` handler,
you will need to use the `TestClient` as a context manager. It will
not be triggered when the `TestClient` is instantiated. You can learn more about it
[here](/lifespan/#running-lifespan-in-tests).
[here](lifespan.md#running-lifespan-in-tests).

### Selecting the Async backend

Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Expand Up @@ -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.5.3
mkdocs-material==9.5.6
mkautodoc==0.2.0

# Packaging
Expand Down
7 changes: 6 additions & 1 deletion starlette/responses.py
Expand Up @@ -72,7 +72,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")))

Expand Down Expand Up @@ -330,6 +333,8 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
)
if scope["method"].upper() == "HEAD":
await send({"type": "http.response.body", "body": b"", "more_body": False})
elif "extensions" in scope and "http.response.pathsend" in scope["extensions"]:
await send({"type": "http.response.pathsend", "path": str(self.path)})
else:
async with await anyio.open_file(self.path, mode="rb") as file:
more_body = True
Expand Down
39 changes: 39 additions & 0 deletions tests/test_responses.py
Expand Up @@ -324,6 +324,38 @@ def test_file_response_with_inline_disposition(tmpdir, test_client_factory):
assert response.headers["content-disposition"] == expected_disposition


@pytest.mark.anyio
async def test_file_response_with_pathsend(tmpdir: Path):
path = os.path.join(tmpdir, "xyz")
content = b"<file content>" * 1000
with open(path, "wb") as file:
file.write(content)

app = FileResponse(path=path, filename="example.png")

async def receive() -> Message: # type: ignore[empty-body]
... # pragma: no cover

async def send(message: Message) -> None:
if message["type"] == "http.response.start":
assert message["status"] == status.HTTP_200_OK
headers = Headers(raw=message["headers"])
assert headers["content-type"] == "image/png"
assert "content-length" in headers
assert "content-disposition" in headers
assert "last-modified" in headers
assert "etag" in headers
elif message["type"] == "http.response.pathsend":
assert message["path"] == str(path)

# Since the TestClient doesn't support `pathsend`, we need to test this directly.
await app(
{"type": "http", "method": "get", "extensions": {"http.response.pathsend": {}}},
receive,
send,
)


def test_set_cookie(test_client_factory, monkeypatch):
# Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
mocked_now = dt.datetime(2037, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
Expand Down Expand Up @@ -436,6 +468,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"<file content>" * 1000
Expand Down

0 comments on commit 82cd228

Please sign in to comment.