Compare commits

...

11 Commits

Author SHA1 Message Date
Sebastián Ramírez
36985f5f25 🔖 Release version 0.128.3 2026-02-06 17:44:11 +01:00
github-actions[bot]
661cdfb8a4 📝 Update release notes
[skip ci]
2026-02-06 16:38:15 +00:00
Sebastián Ramírez
f6cc650a12 ⬆️ Upgrade Starlette supported version range to starlette>=0.40.0,<1.0.0 (#14853) 2026-02-06 16:37:37 +00:00
github-actions[bot]
201feedd68 📝 Update release notes
[skip ci]
2026-02-06 15:31:16 +00:00
Sebastián Ramírez
19f13ead4c 👷 Run tests with Starlette from git (#14849) 2026-02-06 16:30:48 +01:00
github-actions[bot]
01e85c03bd 📝 Update release notes
[skip ci]
2026-02-06 15:29:27 +00:00
Sebastián Ramírez
08233d7ffc 🌐 Update translations for ru (update-outdated) (#14834)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-06 16:28:59 +01:00
github-actions[bot]
fe8c33ea64 📝 Update release notes
[skip ci]
2026-02-06 15:18:55 +00:00
Sebastián Ramírez
f9f7992604 ♻️ Re-implement on_event in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility (#14851) 2026-02-06 16:18:30 +01:00
github-actions[bot]
8e50c55fd9 📝 Update release notes
[skip ci]
2026-02-06 13:45:47 +00:00
Sebastián Ramírez
3b8b310eda 👷 Run tests with lower bound uv sync, upgrade fastapi[all] minimum dependencies: ujson >=5.8.0, orjson >=3.9.3 (#14846) 2026-02-06 14:45:18 +01:00
13 changed files with 951 additions and 546 deletions

View File

@@ -49,31 +49,45 @@ jobs:
matrix:
os: [ windows-latest, macos-latest ]
python-version: [ "3.14" ]
uv-resolution:
- highest
starlette-src:
- starlette-pypi
- starlette-git
include:
- os: ubuntu-latest
python-version: "3.9"
coverage: coverage
uv-resolution: lowest-direct
- os: macos-latest
python-version: "3.10"
coverage: coverage
uv-resolution: highest
- os: windows-latest
python-version: "3.12"
coverage: coverage
uv-resolution: lowest-direct
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
uv-resolution: highest
# Ubuntu with 3.13 needs coverage for CodSpeed benchmarks
- os: ubuntu-latest
python-version: "3.13"
coverage: coverage
uv-resolution: highest
codspeed: codspeed
- os: ubuntu-latest
python-version: "3.14"
coverage: coverage
uv-resolution: highest
starlette-src: starlette-git
fail-fast: false
runs-on: ${{ matrix.os }}
env:
UV_PYTHON: ${{ matrix.python-version }}
UV_RESOLUTION: ${{ matrix.uv-resolution }}
STARLETTE_SRC: ${{ matrix.starlette-src }}
steps:
- name: Dump GitHub context
env:
@@ -92,11 +106,14 @@ jobs:
pyproject.toml
uv.lock
- name: Install Dependencies
run: uv sync --locked --no-dev --group tests --extra all
run: uv sync --no-dev --group tests --extra all
- name: Install Starlette from source
if: matrix.starlette-src == 'starlette-git'
run: uv pip install "git+https://github.com/Kludex/starlette@main"
- run: mkdir coverage
- name: Test
if: matrix.codspeed != 'codspeed'
run: uv run bash scripts/test.sh
run: uv run --no-sync bash scripts/test.sh
env:
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
@@ -108,7 +125,7 @@ jobs:
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
with:
mode: simulation
run: uv run coverage run -m pytest tests/ --codspeed
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
# Do not store coverage for all possible combinations to avoid file size max errors in Smokeshow
- name: Store coverage files
if: matrix.coverage == 'coverage'

View File

@@ -7,6 +7,25 @@ hide:
## Latest Changes
## 0.128.3
### Refactors
* ♻️ Re-implement `on_event` in FastAPI for compatibility with the next Starlette, while keeping backwards compatibility. PR [#14851](https://github.com/fastapi/fastapi/pull/14851) by [@tiangolo](https://github.com/tiangolo).
### Upgrades
* ⬆️ Upgrade Starlette supported version range to `starlette>=0.40.0,<1.0.0`. PR [#14853](https://github.com/fastapi/fastapi/pull/14853) by [@tiangolo](https://github.com/tiangolo).
### Translations
* 🌐 Update translations for ru (update-outdated). PR [#14834](https://github.com/fastapi/fastapi/pull/14834) by [@tiangolo](https://github.com/tiangolo).
### Internal
* 👷 Run tests with Starlette from git. PR [#14849](https://github.com/fastapi/fastapi/pull/14849) by [@tiangolo](https://github.com/tiangolo).
* 👷 Run tests with lower bound uv sync, upgrade `fastapi[all]` minimum dependencies: `ujson >=5.8.0`, `orjson >=3.9.3`. PR [#14846](https://github.com/fastapi/fastapi/pull/14846) by [@tiangolo](https://github.com/tiangolo).
## 0.128.2
### Features

View File

@@ -48,7 +48,7 @@
checker(q="somequery")
```
…и передаст возвращённое значение как значение зависимости в нашу *функцию-обработчике пути* в параметр `fixed_content_included`:
…и передаст возвращённое значение как значение зависимости в параметр `fixed_content_included` нашей *функции-обработчика пути*:
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *}

View File

@@ -6,13 +6,29 @@
## Использование `WSGIMiddleware` { #using-wsgimiddleware }
Нужно импортировать `WSGIMiddleware`.
/// info | Информация
Для этого требуется установить `a2wsgi`, например с помощью `pip install a2wsgi`.
///
Нужно импортировать `WSGIMiddleware` из `a2wsgi`.
Затем оберните WSGIприложение (например, Flask) в middleware (Промежуточный слой).
После этого смонтируйте его на путь.
{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *}
{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *}
/// note | Примечание
Ранее рекомендовалось использовать `WSGIMiddleware` из `fastapi.middleware.wsgi`, но теперь он помечен как устаревший.
Вместо него рекомендуется использовать пакет `a2wsgi`. Использование остаётся таким же.
Просто убедитесь, что пакет `a2wsgi` установлен, и импортируйте `WSGIMiddleware` из `a2wsgi`.
///
## Проверьте { #check-it }

View File

@@ -145,8 +145,6 @@ Successfully installed fastapi pydantic
* Создайте файл `main.py` со следующим содержимым:
```Python
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@@ -158,7 +156,7 @@ def read_root():
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
```

View File

@@ -161,8 +161,6 @@ $ pip install "fastapi[standard]"
Создайте файл `main.py` со следующим содержимым:
```Python
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@@ -174,7 +172,7 @@ def read_root():
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
```
@@ -183,9 +181,7 @@ def read_item(item_id: int, q: Union[str, None] = None):
Если ваш код использует `async` / `await`, используйте `async def`:
```Python hl_lines="9 14"
from typing import Union
```Python hl_lines="7 12"
from fastapi import FastAPI
app = FastAPI()
@@ -197,7 +193,7 @@ async def read_root():
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: Union[str, None] = None):
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
```
@@ -288,9 +284,7 @@ INFO: Application startup complete.
Объявите тело запроса, используя стандартные типы Python, спасибо Pydantic.
```Python hl_lines="4 9-12 25-27"
from typing import Union
```Python hl_lines="2 7-10 23-25"
from fastapi import FastAPI
from pydantic import BaseModel
@@ -300,7 +294,7 @@ app = FastAPI()
class Item(BaseModel):
name: str
price: float
is_offer: Union[bool, None] = None
is_offer: bool | None = None
@app.get("/")
@@ -309,7 +303,7 @@ def read_root():
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}

View File

@@ -101,13 +101,13 @@
Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так:
```Python
q: Union[str, None] = None
q: str | None = None
```
Или в Python 3.10 и выше:
Или в Python 3.9:
```Python
q: str | None = None
q: Union[str, None] = None
```
Например:
@@ -116,7 +116,7 @@ q: str | None = None
/// info | Информация
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже.
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`, `Path` и других, которые вы увидите позже.
///

View File

@@ -52,7 +52,7 @@
Вы можете добавить параметры `summary` и `description`:
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *}
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *}
## Описание из строк документации { #description-from-docstring }
@@ -70,7 +70,7 @@
Вы можете указать описание ответа с помощью параметра `response_description`:
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *}
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *}
/// info | Дополнительная информация
@@ -78,7 +78,7 @@
///
/// check
/// check | Проверка
OpenAPI указывает, что каждой *операции пути* необходимо описание ответа.

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.128.2"
__version__ = "0.128.3"
from starlette import status as status

View File

@@ -1,22 +1,31 @@
import contextlib
import email.message
import functools
import inspect
import json
import types
from collections.abc import (
AsyncIterator,
Awaitable,
Collection,
Coroutine,
Generator,
Mapping,
Sequence,
)
from contextlib import AsyncExitStack, asynccontextmanager
from contextlib import (
AbstractAsyncContextManager,
AbstractContextManager,
AsyncExitStack,
asynccontextmanager,
)
from enum import Enum, IntEnum
from typing import (
Annotated,
Any,
Callable,
Optional,
TypeVar,
Union,
)
@@ -143,6 +152,50 @@ def websocket_session(
return app
_T = TypeVar("_T")
# Vendored from starlette.routing to avoid importing private symbols
class _AsyncLiftContextManager(AbstractAsyncContextManager[_T]):
"""
Wraps a synchronous context manager to make it async.
This is vendored from Starlette to avoid importing private symbols.
"""
def __init__(self, cm: AbstractContextManager[_T]) -> None:
self._cm = cm
async def __aenter__(self) -> _T:
return self._cm.__enter__()
async def __aexit__(
self,
exc_type: Optional[type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[types.TracebackType],
) -> Optional[bool]:
return self._cm.__exit__(exc_type, exc_value, traceback)
# Vendored from starlette.routing to avoid importing private symbols
def _wrap_gen_lifespan_context(
lifespan_context: Callable[[Any], Generator[Any, Any, Any]],
) -> Callable[[Any], AbstractAsyncContextManager[Any]]:
"""
Wrap a generator-based lifespan context into an async context manager.
This is vendored from Starlette to avoid importing private symbols.
"""
cmgr = contextlib.contextmanager(lifespan_context)
@functools.wraps(cmgr)
def wrapper(app: Any) -> _AsyncLiftContextManager[Any]:
return _AsyncLiftContextManager(cmgr(app))
return wrapper
def _merge_lifespan_context(
original_context: Lifespan[Any], nested_context: Lifespan[Any]
) -> Lifespan[Any]:
@@ -160,6 +213,30 @@ def _merge_lifespan_context(
return merged_lifespan # type: ignore[return-value]
class _DefaultLifespan:
"""
Default lifespan context manager that runs on_startup and on_shutdown handlers.
This is a copy of the Starlette _DefaultLifespan class that was removed
in Starlette. FastAPI keeps it to maintain backward compatibility with
on_startup and on_shutdown event handlers.
Ref: https://github.com/Kludex/starlette/pull/3117
"""
def __init__(self, router: "APIRouter") -> None:
self._router = router
async def __aenter__(self) -> None:
await self._router._startup()
async def __aexit__(self, *exc_info: object) -> None:
await self._router._shutdown()
def __call__(self: _T, app: object) -> _T:
return self
# Cache for endpoint context to avoid re-extracting on every request
_endpoint_context_cache: dict[int, EndpointContext] = {}
@@ -903,13 +980,33 @@ class APIRouter(routing.Router):
),
] = Default(generate_unique_id),
) -> None:
# Handle on_startup/on_shutdown locally since Starlette removed support
# Ref: https://github.com/Kludex/starlette/pull/3117
# TODO: deprecate this once the lifespan (or alternative) interface is improved
self.on_startup: list[Callable[[], Any]] = (
[] if on_startup is None else list(on_startup)
)
self.on_shutdown: list[Callable[[], Any]] = (
[] if on_shutdown is None else list(on_shutdown)
)
# Determine the lifespan context to use
if lifespan is None:
# Use the default lifespan that runs on_startup/on_shutdown handlers
lifespan_context: Lifespan[Any] = _DefaultLifespan(self)
elif inspect.isasyncgenfunction(lifespan):
lifespan_context = asynccontextmanager(lifespan)
elif inspect.isgeneratorfunction(lifespan):
lifespan_context = _wrap_gen_lifespan_context(lifespan)
else:
lifespan_context = lifespan
self.lifespan_context = lifespan_context
super().__init__(
routes=routes,
redirect_slashes=redirect_slashes,
default=default,
on_startup=on_startup,
on_shutdown=on_shutdown,
lifespan=lifespan,
lifespan=lifespan_context,
)
if prefix:
assert prefix.startswith("/"), "A path prefix must start with '/'"
@@ -4473,6 +4570,58 @@ class APIRouter(routing.Router):
generate_unique_id_function=generate_unique_id_function,
)
# TODO: remove this once the lifespan (or alternative) interface is improved
async def _startup(self) -> None:
"""
Run any `.on_startup` event handlers.
This method is kept for backward compatibility after Starlette removed
support for on_startup/on_shutdown handlers.
Ref: https://github.com/Kludex/starlette/pull/3117
"""
for handler in self.on_startup:
if is_async_callable(handler):
await handler()
else:
handler()
# TODO: remove this once the lifespan (or alternative) interface is improved
async def _shutdown(self) -> None:
"""
Run any `.on_shutdown` event handlers.
This method is kept for backward compatibility after Starlette removed
support for on_startup/on_shutdown handlers.
Ref: https://github.com/Kludex/starlette/pull/3117
"""
for handler in self.on_shutdown:
if is_async_callable(handler):
await handler()
else:
handler()
# TODO: remove this once the lifespan (or alternative) interface is improved
def add_event_handler(
self,
event_type: str,
func: Callable[[], Any],
) -> None:
"""
Add an event handler function for startup or shutdown.
This method is kept for backward compatibility after Starlette removed
support for on_startup/on_shutdown handlers.
Ref: https://github.com/Kludex/starlette/pull/3117
"""
assert event_type in ("startup", "shutdown")
if event_type == "startup":
self.on_startup.append(func)
else:
self.on_shutdown.append(func)
@deprecated(
"""
on_event is deprecated, use lifespan event handlers instead.

View File

@@ -43,7 +43,7 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP",
]
dependencies = [
"starlette>=0.40.0,<0.51.0",
"starlette>=0.40.0,<1.0.0",
"pydantic>=2.7.0",
"typing-extensions>=4.8.0",
"typing-inspection>=0.4.2",
@@ -108,9 +108,9 @@ all = [
# For Starlette's schema generation, would not be used with FastAPI
"pyyaml >=5.3.1",
# For UJSONResponse
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
"ujson >=5.8.0",
# For ORJSONResponse
"orjson >=3.2.1",
"orjson >=3.9.3",
# To validate email fields
"email-validator >=2.0.0",
# Uvicorn with uvloop
@@ -167,7 +167,7 @@ tests = [
"anyio[trio]>=3.2.1,<5.0.0",
"coverage[toml]>=6.5.0,<8.0",
"dirty-equals==0.9.0",
"flask>=1.1.2,<4.0.0",
"flask>=3.0.0,<4.0.0",
"inline-snapshot>=0.21.1",
"mypy==1.14.1",
"pwdlib[argon2]>=0.2.1",

View File

@@ -241,3 +241,79 @@ def test_merged_mixed_state_lifespans() -> None:
with TestClient(app) as client:
assert client.app_state == {"router": True}
@pytest.mark.filterwarnings(
r"ignore:\s*on_event is deprecated, use lifespan event handlers instead.*:DeprecationWarning"
)
def test_router_async_shutdown_handler(state: State) -> None:
"""Test that async on_shutdown event handlers are called correctly, for coverage."""
app = FastAPI()
@app.get("/")
def main() -> dict[str, str]:
return {"message": "Hello World"}
@app.on_event("shutdown")
async def app_shutdown() -> None:
state.app_shutdown = True
assert state.app_shutdown is False
with TestClient(app) as client:
assert state.app_shutdown is False
response = client.get("/")
assert response.status_code == 200, response.text
assert state.app_shutdown is True
def test_router_sync_generator_lifespan(state: State) -> None:
"""Test that a sync generator lifespan works via _wrap_gen_lifespan_context."""
from collections.abc import Generator
def lifespan(app: FastAPI) -> Generator[None, None, None]:
state.app_startup = True
yield
state.app_shutdown = True
app = FastAPI(lifespan=lifespan) # type: ignore[arg-type]
@app.get("/")
def main() -> dict[str, str]:
return {"message": "Hello World"}
assert state.app_startup is False
assert state.app_shutdown is False
with TestClient(app) as client:
assert state.app_startup is True
assert state.app_shutdown is False
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello World"}
assert state.app_startup is True
assert state.app_shutdown is True
def test_router_async_generator_lifespan(state: State) -> None:
"""Test that an async generator lifespan (not wrapped) works."""
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
state.app_startup = True
yield
state.app_shutdown = True
app = FastAPI(lifespan=lifespan) # type: ignore[arg-type]
@app.get("/")
def main() -> dict[str, str]:
return {"message": "Hello World"}
assert state.app_startup is False
assert state.app_shutdown is False
with TestClient(app) as client:
assert state.app_startup is True
assert state.app_shutdown is False
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"message": "Hello World"}
assert state.app_startup is True
assert state.app_shutdown is True

1154
uv.lock generated
View File

File diff suppressed because it is too large Load Diff