Skip to content

Commit

Permalink
Merge branch 'master' into fix-resource-warning-anyio
Browse files Browse the repository at this point in the history
  • Loading branch information
graingert authored Dec 29, 2024
2 parents e056671 + 27b6f4c commit de4b745
Show file tree
Hide file tree
Showing 40 changed files with 234 additions and 330 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-suite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:

strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]

steps:
- uses: "actions/checkout@v4"
Expand Down
18 changes: 9 additions & 9 deletions docs/applications.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@

??? abstract "API Reference"
::: starlette.applications.Starlette
options:
parameter_headings: false
show_root_heading: true
heading_level: 3
filters:
- "__init__"

Starlette includes an application class `Starlette` that nicely ties together all of
its other functionality.

Expand Down Expand Up @@ -45,15 +54,6 @@ routes = [
app = Starlette(debug=True, routes=routes, lifespan=lifespan)
```

??? abstract "API Reference"
::: starlette.applications.Starlette
options:
parameter_headings: false
show_root_heading: true
heading_level: 3
filters:
- "__init__"

### Storing state on the app instance

You can store arbitrary extra state on the application instance, using the
Expand Down
8 changes: 2 additions & 6 deletions docs/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,11 @@ which provides SQLAlchemy core support against a range of different database dri
Here's a complete example, that includes table definitions, configuring a `database.Database`
instance, and a couple of endpoints that interact with the database.

**.env**

```ini
```ini title=".env"
DATABASE_URL=sqlite:///test.db
```

**app.py**

```python
```python title="app.py"
import contextlib

import databases
Expand Down
4 changes: 2 additions & 2 deletions docs/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ The `ExceptionMiddleware` implementation defaults to returning plain-text HTTP r
You should only raise `HTTPException` inside routing or endpoints.
Middleware classes should instead just return appropriate responses directly.

You can use an `HTTPException` on a WebSocket endpoint in case it's raised before `websocket.accept()`.
The connection is not upgraded to a WebSocket connection, and the proper HTTP response is returned.
You can use an `HTTPException` on a WebSocket endpoint. In case it's raised before `websocket.accept()`
the connection is not upgraded to a WebSocket connection, and the proper HTTP response is returned.

```python
from starlette.applications import Starlette
Expand Down
15 changes: 15 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
toc_depth: 2
---

## 0.45.0 (December 29, 2024)

#### Removed

* Drop Python 3.8 support [#2823](https://github.com/encode/starlette/pull/2823).
* Remove `ExceptionMiddleware` import proxy from `starlette.exceptions` module [#2826](https://github.com/encode/starlette/pull/2826).
* Remove deprecated `WS_1004_NO_STATUS_RCVD` and `WS_1005_ABNORMAL_CLOSURE` [#2827](https://github.com/encode/starlette/pull/2827).

## 0.44.0 (December 28, 2024)

#### Added

* Add `client` parameter to `TestClient` [#2810](https://github.com/encode/starlette/pull/2810).
* Add `max_part_size` parameter to `Request.form()` [#2815](https://github.com/encode/starlette/pull/2815).

## 0.43.0 (December 25, 2024)

#### Removed
Expand Down
16 changes: 8 additions & 8 deletions docs/requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ You can also access the request body as a stream, using the `async for` syntax:
from starlette.requests import Request
from starlette.responses import Response


async def app(scope, receive, send):
assert scope['type'] == 'http'
request = Request(scope, receive)
Expand All @@ -113,12 +113,12 @@ state with `disconnected = await request.is_disconnected()`.

Request files are normally sent as multipart form data (`multipart/form-data`).

Signature: `request.form(max_files=1000, max_fields=1000)`
Signature: `request.form(max_files=1000, max_fields=1000, max_part_size=1024*1024)`

You can configure the number of maximum fields or files with the parameters `max_files` and `max_fields`:
You can configure the number of maximum fields or files with the parameters `max_files` and `max_fields`; and part size using `max_part_size`:

```python
async with request.form(max_files=1000, max_fields=1000):
async with request.form(max_files=1000, max_fields=1000, max_part_size=1024*1024):
...
```

Expand Down Expand Up @@ -155,11 +155,11 @@ async with request.form() as form:
```

!!! info
As settled in [RFC-7578: 4.2](https://www.ietf.org/rfc/rfc7578.txt), form-data content part that contains file
As settled in [RFC-7578: 4.2](https://www.ietf.org/rfc/rfc7578.txt), form-data content part that contains file
assumed to have `name` and `filename` fields in `Content-Disposition` header: `Content-Disposition: form-data;
name="user"; filename="somefile"`. Though `filename` field is optional according to RFC-7578, it helps
Starlette to differentiate which data should be treated as file. If `filename` field was supplied, `UploadFile`
object will be created to access underlying file, otherwise form-data part will be parsed and available as a raw
name="user"; filename="somefile"`. Though `filename` field is optional according to RFC-7578, it helps
Starlette to differentiate which data should be treated as file. If `filename` field was supplied, `UploadFile`
object will be created to access underlying file, otherwise form-data part will be parsed and available as a raw
string.

#### Application
Expand Down
23 changes: 22 additions & 1 deletion docs/testclient.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@

??? abstract "API Reference"
::: starlette.testclient.TestClient
options:
parameter_headings: false
show_bases: false
show_root_heading: true
heading_level: 3
filters:
- "__init__"

The test client allows you to make requests against your ASGI application,
using the `httpx` library.

Expand Down Expand Up @@ -69,10 +79,21 @@ case you should use `client = TestClient(app, raise_server_exceptions=False)`.
not be triggered when the `TestClient` is instantiated. You can learn more about it
[here](lifespan.md#running-lifespan-in-tests).

### Change client address

By default, the TestClient will set the client host to `"testserver"` and the port to `50000`.

You can change the client address by setting the `client` attribute of the `TestClient` instance:

```python
client = TestClient(app, client=('http://localhost', 8000))
```

### Selecting the Async backend

`TestClient` takes arguments `backend` (a string) and `backend_options` (a dictionary).
These options are passed to `anyio.start_blocking_portal()`. See the [anyio documentation](https://anyio.readthedocs.io/en/stable/basics.html#backend-options)
These options are passed to `anyio.start_blocking_portal()`.
See the [anyio documentation](https://anyio.readthedocs.io/en/stable/basics.html#backend-options)
for more information about the accepted backend options.
By default, `asyncio` is used with default options.

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ plugins:
merge_init_into_class: true
parameter_headings: true
show_signature_annotations: true
show_source: false
signature_crossrefs: true
import:
- url: https://docs.python.org/3/objects.inv
6 changes: 2 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dynamic = ["version"]
description = "The little ASGI library that shines."
readme = "README.md"
license = "BSD-3-Clause"
requires-python = ">=3.8"
requires-python = ">=3.9"
authors = [{ name = "Tom Christie", email = "[email protected]" }]
classifiers = [
"Development Status :: 3 - Alpha",
Expand All @@ -18,7 +18,6 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
Expand Down Expand Up @@ -69,8 +68,7 @@ combine-as-imports = true

[tool.mypy]
strict = true
ignore_missing_imports = true
python_version = "3.8"
python_version = "3.9"

[[tool.mypy.overrides]]
module = "starlette.testclient.*"
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ pytest==8.3.4
trio==0.27.0

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

# Packaging
build==1.2.2.post1
Expand Down
5 changes: 4 additions & 1 deletion scripts/test
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ if [ -z $GITHUB_ACTIONS ]; then
scripts/check
fi

${PREFIX}coverage run -m pytest $@
# TODO: Remove this custom logic, and add `branch = true` to the `[coverage.run]` when we drop support for Python 3.9.
# See https://github.com/encode/starlette/issues/2452.
branch_option=$(python -c 'import sys; print("--branch" if sys.version_info >= (3, 10) else "")')
${PREFIX}coverage run $branch_option -m pytest $@

if [ -z $GITHUB_ACTIONS ]; then
scripts/coverage
Expand Down
2 changes: 1 addition & 1 deletion starlette/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.43.0"
__version__ = "0.45.0"
26 changes: 0 additions & 26 deletions starlette/_compat.py

This file was deleted.

4 changes: 2 additions & 2 deletions starlette/_exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from starlette.types import ASGIApp, ExceptionHandler, Message, Receive, Scope, Send
from starlette.websockets import WebSocket

ExceptionHandlers = typing.Dict[typing.Any, ExceptionHandler]
StatusHandlers = typing.Dict[int, ExceptionHandler]
ExceptionHandlers = dict[typing.Any, ExceptionHandler]
StatusHandlers = dict[int, ExceptionHandler]


def _lookup_exception_handler(exc_handlers: ExceptionHandlers, exc: Exception) -> ExceptionHandler | None:
Expand Down
6 changes: 3 additions & 3 deletions starlette/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
has_exceptiongroups = True
if sys.version_info < (3, 11): # pragma: no cover
try:
from exceptiongroup import BaseExceptionGroup
from exceptiongroup import BaseExceptionGroup # type: ignore[unused-ignore,import-not-found]
except ImportError:
has_exceptiongroups = False

Expand Down Expand Up @@ -75,9 +75,9 @@ def collapse_excgroups() -> typing.Generator[None, None, None]:
try:
yield
except BaseException as exc:
if has_exceptiongroups:
if has_exceptiongroups: # pragma: no cover
while isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
exc = exc.exceptions[0] # pragma: no cover
exc = exc.exceptions[0]

raise exc

Expand Down
33 changes: 2 additions & 31 deletions starlette/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
from __future__ import annotations

import http
import typing
import warnings

__all__ = ("HTTPException", "WebSocketException")
from collections.abc import Mapping


class HTTPException(Exception):
def __init__(
self,
status_code: int,
detail: str | None = None,
headers: typing.Mapping[str, str] | None = None,
) -> None:
def __init__(self, status_code: int, detail: str | None = None, headers: Mapping[str, str] | None = None) -> None:
if detail is None:
detail = http.HTTPStatus(status_code).phrase
self.status_code = status_code
Expand All @@ -39,24 +31,3 @@ def __str__(self) -> str:
def __repr__(self) -> str:
class_name = self.__class__.__name__
return f"{class_name}(code={self.code!r}, reason={self.reason!r})"


__deprecated__ = "ExceptionMiddleware"


def __getattr__(name: str) -> typing.Any: # pragma: no cover
if name == __deprecated__:
from starlette.middleware.exceptions import ExceptionMiddleware

warnings.warn(
f"{__deprecated__} is deprecated on `starlette.exceptions`. "
f"Import it from `starlette.middleware.exceptions` instead.",
category=DeprecationWarning,
stacklevel=3,
)
return ExceptionMiddleware
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")


def __dir__() -> list[str]:
return sorted(list(__all__) + [__deprecated__]) # pragma: no cover
7 changes: 4 additions & 3 deletions starlette/formparsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from starlette.datastructures import FormData, Headers, UploadFile

if typing.TYPE_CHECKING:
import multipart
from multipart.multipart import MultipartCallbacks, QuerystringCallbacks, parse_options_header
import python_multipart as multipart
from python_multipart.multipart import MultipartCallbacks, QuerystringCallbacks, parse_options_header
else:
try:
try:
Expand Down Expand Up @@ -123,7 +123,6 @@ async def parse(self) -> FormData:

class MultiPartParser:
max_file_size = 1024 * 1024 # 1MB
max_part_size = 1024 * 1024 # 1MB

def __init__(
self,
Expand All @@ -132,6 +131,7 @@ def __init__(
*,
max_files: int | float = 1000,
max_fields: int | float = 1000,
max_part_size: int = 1024 * 1024, # 1MB
) -> None:
assert multipart is not None, "The `python-multipart` library must be installed to use form parsing."
self.headers = headers
Expand All @@ -148,6 +148,7 @@ def __init__(
self._file_parts_to_write: list[tuple[MultipartPart, bytes]] = []
self._file_parts_to_finish: list[MultipartPart] = []
self._files_to_close_on_error: list[SpooledTemporaryFile[bytes]] = []
self.max_part_size = max_part_size

def on_part_begin(self) -> None:
self._current_part = MultipartPart()
Expand Down
3 changes: 2 additions & 1 deletion starlette/middleware/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from __future__ import annotations

import sys
from typing import Any, Iterator, Protocol
from collections.abc import Iterator
from typing import Any, Protocol

if sys.version_info >= (3, 10): # pragma: no cover
from typing import ParamSpec
Expand Down
Loading

0 comments on commit de4b745

Please sign in to comment.