Skip to content

Commit

Permalink
Merge branch 'master' into feat/upload-file-max-size
Browse files Browse the repository at this point in the history
  • Loading branch information
khadrawy authored Dec 15, 2024
2 parents 51910e4 + 28991b7 commit 0ba66c6
Show file tree
Hide file tree
Showing 12 changed files with 51 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ in isolation.
[asgi]: https://asgi.readthedocs.io/en/latest/
[httpx]: https://www.python-httpx.org/
[jinja2]: https://jinja.palletsprojects.com/
[python-multipart]: https://andrew-d.github.io/python-multipart/
[python-multipart]: https://multipart.fastapiexpert.com/
[itsdangerous]: https://itsdangerous.palletsprojects.com/
[sqlalchemy]: https://www.sqlalchemy.org
[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ in isolation.
[asgi]: https://asgi.readthedocs.io/en/latest/
[httpx]: https://www.python-httpx.org/
[jinja2]: https://jinja.palletsprojects.com/
[python-multipart]: https://andrew-d.github.io/python-multipart/
[python-multipart]: https://multipart.fastapiexpert.com/
[itsdangerous]: https://itsdangerous.palletsprojects.com/
[sqlalchemy]: https://www.sqlalchemy.org
[pyyaml]: https://pyyaml.org/wiki/PyYAMLDocumentation
Expand Down
13 changes: 13 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
toc_depth: 2
---

## 0.42.0 (December 14, 2024)

#### Added

* Raise `ClientDisconnect` on `StreamingResponse` [#2732](https://github.com/encode/starlette/pull/2732).

#### Fixed

* Use ETag from headers when parsing If-Range in FileResponse [#2761](https://github.com/encode/starlette/pull/2761).
* Follow directory symlinks in `StaticFiles` when `follow_symlinks=True` [#2711](https://github.com/encode/starlette/pull/2711).
* Bump minimum `python-multipart` version to `0.0.18` [0ba8395](https://github.com/encode/starlette/commit/0ba83959e609bbd460966f092287df1bbd564cc6).
* Bump minimum `httpx` version to `0.27.0` [#2773](https://github.com/encode/starlette/pull/2773).

## 0.41.3 (November 18, 2024)

#### Fixed
Expand Down
11 changes: 7 additions & 4 deletions docs/testclient.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,17 @@ May raise `starlette.websockets.WebSocketDisconnect`.
### Asynchronous tests

Sometimes you will want to do async things outside of your application.
For example, you might want to check the state of your database after calling your app using your existing async database client / infrastructure.
For example, you might want to check the state of your database after calling your app
using your existing async database client/infrastructure.

For these situations, using `TestClient` is difficult because it creates it's own event loop and async resources (like a database connection) often cannot be shared across event loops.
For these situations, using `TestClient` is difficult because it creates it's own event loop and async
resources (like a database connection) often cannot be shared across event loops.
The simplest way to work around this is to just make your entire test async and use an async client, like [httpx.AsyncClient].

Here is an example of such a test:

```python
from httpx import AsyncClient
from httpx import AsyncClient, ASGITransport
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.requests import Request
Expand All @@ -192,7 +194,8 @@ app = Starlette(routes=[Route("/", hello)])
# or install and configure pytest-asyncio (https://github.com/pytest-dev/pytest-asyncio)
async def test_app() -> None:
# note: you _must_ set `base_url` for relative urls like "/" to work
async with AsyncClient(app=app, base_url="http://testserver") as client:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
r = await client.get("/")
assert r.status_code == 200
assert r.text == "Hello World!"
Expand Down
4 changes: 2 additions & 2 deletions docs/third-party-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ Here are some of those third party packages:

### Apitally

<a href="https://github.com/apitally/python-client" target="_blank">GitHub</a> |
<a href="https://github.com/apitally/apitally-py" target="_blank">GitHub</a> |
<a href="https://docs.apitally.io/frameworks/starlette" target="_blank">Documentation</a>

Simple traffic, error and response time monitoring plus API key and permission management for Starlette (and other frameworks).
Analytics, request logging and monitoring for REST APIs built with Starlette (and other frameworks).

### Authlib

Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,20 @@
coverage==7.6.1
importlib-metadata==8.5.0
mypy==1.13.0
ruff==0.7.2
ruff==0.8.1
typing_extensions==4.12.2
types-contextvars==2.4.7.3
types-PyYAML==6.0.12.20240917
types-dataclasses==0.6.6
pytest==8.3.3
pytest==8.3.4
trio==0.27.0

# Documentation
mkdocs==1.6.1
mkdocs-material==9.5.43
mkdocs-material==9.5.47
mkdocstrings-python<1.12.0; python_version < "3.9"
mkdocstrings-python==1.12.2; python_version >= "3.9"

# Packaging
build==1.2.2.post1
twine==5.1.1
twine==6.0.1
2 changes: 1 addition & 1 deletion starlette/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.41.3"
__version__ = "0.42.0"
2 changes: 1 addition & 1 deletion starlette/middleware/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(
HTTPException: self.http_exception,
WebSocketException: self.websocket_exception,
}
if handlers is not None:
if handlers is not None: # pragma: no branch
for key, value in handlers.items():
self.add_exception_handler(key, value)

Expand Down
9 changes: 3 additions & 6 deletions starlette/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
http_range = headers.get("range")
http_if_range = headers.get("if-range")

if http_range is None or (http_if_range is not None and not self._should_use_range(http_if_range, stat_result)):
if http_range is None or (http_if_range is not None and not self._should_use_range(http_if_range)):
await self._handle_simple(send, send_header_only)
else:
try:
Expand Down Expand Up @@ -438,11 +438,8 @@ async def _handle_multiple_ranges(
}
)

@classmethod
def _should_use_range(cls, http_if_range: str, stat_result: os.stat_result) -> bool:
etag_base = str(stat_result.st_mtime) + "-" + str(stat_result.st_size)
etag = f'"{md5_hexdigest(etag_base.encode(), usedforsecurity=False)}"'
return http_if_range == formatdate(stat_result.st_mtime, usegmt=True) or http_if_range == etag
def _should_use_range(self, http_if_range: str) -> bool:
return http_if_range == self.headers["last-modified"] or http_if_range == self.headers["etag"]

@staticmethod
def _parse_range_header(http_range: str, file_size: int) -> list[tuple[int, int]]:
Expand Down
2 changes: 1 addition & 1 deletion starlette/templating.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def __init__(
self.context_processors = context_processors or []
if directory is not None:
self.env = self._create_env(directory, **env_options)
elif env is not None:
elif env is not None: # pragma: no branch
self.env = env

self._setup_env_defaults(self.env)
Expand Down
2 changes: 1 addition & 1 deletion starlette/websockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ async def accept(
) -> None:
headers = headers or []

if self.client_state == WebSocketState.CONNECTING:
if self.client_state == WebSocketState.CONNECTING: # pragma: no branch
# If we haven't yet seen the 'connect' message, then wait for it first.
await self.receive()
await self.send({"type": "websocket.accept", "subprotocol": subprotocol, "headers": headers})
Expand Down
16 changes: 16 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,22 @@ def test_file_response_with_method_warns(tmp_path: Path) -> None:
FileResponse(path=tmp_path, filename="example.png", method="GET")


def test_file_response_with_range_header(tmp_path: Path, test_client_factory: TestClientFactory) -> None:
content = b"file content"
filename = "hello.txt"
path = tmp_path / filename
path.write_bytes(content)
etag = '"a_non_autogenerated_etag"'
app = FileResponse(path=path, filename=filename, headers={"etag": etag})
client = test_client_factory(app)
response = client.get("/", headers={"range": "bytes=0-4", "if-range": etag})
assert response.status_code == status.HTTP_206_PARTIAL_CONTENT
assert response.content == content[:5]
assert response.headers["etag"] == etag
assert response.headers["content-length"] == "5"
assert response.headers["content-range"] == f"bytes 0-4/{len(content)}"


def test_set_cookie(test_client_factory: TestClientFactory, monkeypatch: pytest.MonkeyPatch) -> None:
# 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

0 comments on commit 0ba66c6

Please sign in to comment.