Compare commits

..

13 Commits

Author SHA1 Message Date
github-actions[bot]
266a3138b5 📝 Update release notes
[skip ci]
2026-02-06 17:18:53 +00:00
Sebastián Ramírez
5a31b37cc7 ⬆️ Upgrade development dependencies (#14854) 2026-02-06 17:18:30 +00:00
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
14 changed files with 3669 additions and 879 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,29 @@ hide:
## Latest Changes
### Internal
* ⬆️ Upgrade development dependencies. PR [#14854](https://github.com/fastapi/fastapi/pull/14854) by [@tiangolo](https://github.com/tiangolo).
## 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

@@ -2,23 +2,23 @@
Maintenant que nous avons vu comment utiliser `Path` et `Query`, voyons des usages plus avancés des déclarations de paramètres du corps de la requête.
## Mélanger les paramètres `Path`, `Query` et du corps de la requête { #mix-path-query-and-body-parameters }
## Mélanger les paramètres `Path`, `Query` et body { #mix-path-query-and-body-parameters }
Tout d'abord, sachez que vous pouvez mélanger librement les déclarations des paramètres `Path`, `Query` et du corps de la requête, **FastAPI** saura quoi faire.
Tout d'abord, sachez que vous pouvez mélanger librement les déclarations des paramètres `Path`, `Query` et du body, **FastAPI** saura quoi faire.
Et vous pouvez également déclarer des paramètres du corps de la requête comme étant optionnels, en leur assignant une valeur par défaut à `None` :
Et vous pouvez également déclarer des paramètres du body comme étant optionnels, en leur assignant une valeur par défaut à `None` :
{* ../../docs_src/body_multiple_params/tutorial001_an_py310.py hl[18:20] *}
/// note | Remarque
Notez que, dans ce cas, l'élément `item` récupéré depuis le corps de la requête est optionnel. Comme sa valeur par défaut est `None`.
Notez que, dans ce cas, l'élément `item` récupéré depuis le body est optionnel. Comme sa valeur par défaut est `None`.
///
## Paramètres multiples du corps de la requête { #multiple-body-parameters }
## Paramètres multiples du body { #multiple-body-parameters }
Dans l'exemple précédent, les chemins d'accès attendraient un corps de la requête JSON avec les attributs d'un `Item`, par exemple :
Dans l'exemple précédent, les chemins d'accès attendraient un body JSON avec les attributs d'un `Item`, par exemple :
```JSON
{
@@ -29,13 +29,13 @@ Dans l'exemple précédent, les chemins d'accès attendraient un corps de la req
}
```
Mais vous pouvez également déclarer plusieurs paramètres provenant du corps de la requête, par exemple `item` et `user` :
Mais vous pouvez également déclarer plusieurs paramètres provenant du body, par exemple `item` et `user` :
{* ../../docs_src/body_multiple_params/tutorial002_py310.py hl[20] *}
Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre du corps de la requête dans la fonction (il y a deux paramètres qui sont des modèles Pydantic).
Dans ce cas, **FastAPI** détectera qu'il y a plus d'un paramètre du body dans la fonction (il y a deux paramètres qui sont des modèles Pydantic).
Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le corps de la requête, et s'attendra à recevoir un corps de la requête semblable à :
Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le body, et s'attendra à recevoir un body semblable à :
```JSON
{
@@ -54,7 +54,7 @@ Il utilisera alors les noms des paramètres comme clés (noms de champs) dans le
/// note | Remarque
Notez que, bien que `item` ait été déclaré de la même manière qu'auparavant, il est désormais attendu à l'intérieur du corps de la requête sous la clé `item`.
Notez que, bien que `item` ait été déclaré de la même manière qu'auparavant, il est désormais attendu à l'intérieur du body sous la clé `item`.
///
@@ -62,19 +62,19 @@ Notez que, bien que `item` ait été déclaré de la même manière qu'auparavan
Il effectuera la validation des données composées, et les documentera ainsi pour le schéma OpenAPI et la documentation automatique.
## Valeurs singulières dans le corps de la requête { #singular-values-in-body }
## Valeurs singulières dans le body { #singular-values-in-body }
De la même façon qu'il existe `Query` et `Path` pour définir des données supplémentaires pour les paramètres de requête et de chemin, **FastAPI** fournit un équivalent `Body`.
Par exemple, en étendant le modèle précédent, vous pourriez décider d'avoir une autre clé `importance` dans le même corps de la requête, en plus de `item` et `user`.
Par exemple, en étendant le modèle précédent, vous pourriez décider d'avoir une autre clé `importance` dans le même body, en plus de `item` et `user`.
Si vous le déclarez tel quel, comme c'est une valeur singulière, **FastAPI** supposera qu'il s'agit d'un paramètre de requête.
Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du corps de la requête en utilisant `Body` :
Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du body en utilisant `Body` :
{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *}
Dans ce cas, **FastAPI** s'attendra à un corps de la requête semblable à :
Dans ce cas, **FastAPI** s'attendra à un body semblable à :
```JSON
{
@@ -94,9 +94,9 @@ Dans ce cas, **FastAPI** s'attendra à un corps de la requête semblable à :
Encore une fois, il convertira les types de données, validera, documentera, etc.
## Paramètres multiples du corps de la requête et paramètres de requête { #multiple-body-params-and-query }
## Paramètres multiples du body et paramètres de requête { #multiple-body-params-and-query }
Bien entendu, vous pouvez également déclarer des paramètres de requête supplémentaires quand vous en avez besoin, en plus de tout paramètre du corps de la requête.
Bien entendu, vous pouvez également déclarer des paramètres de requête supplémentaires quand vous en avez besoin, en plus de tout paramètre du body.
Comme, par défaut, les valeurs singulières sont interprétées comme des paramètres de requête, vous n'avez pas besoin d'ajouter explicitement `Query`, vous pouvez simplement écrire :
@@ -120,13 +120,13 @@ Par exemple :
///
## Intégrer un seul paramètre du corps de la requête { #embed-a-single-body-parameter }
## Intégrer un seul paramètre du body { #embed-a-single-body-parameter }
Supposons que vous n'ayez qu'un seul paramètre `item` dans le corps de la requête, provenant d'un modèle Pydantic `Item`.
Supposons que vous n'ayez qu'un seul paramètre `item` dans le body, provenant d'un modèle Pydantic `Item`.
Par défaut, **FastAPI** attendra alors son contenu directement.
Mais si vous voulez qu'il attende un JSON avec une clé `item` contenant le contenu du modèle, comme lorsqu'on déclare des paramètres supplémentaires du corps de la requête, vous pouvez utiliser le paramètre spécial `embed` de `Body` :
Mais si vous voulez qu'il attende un JSON avec une clé `item` contenant le contenu du modèle, comme lorsqu'on déclare des paramètres supplémentaires du body, vous pouvez utiliser le paramètre spécial `embed` de `Body` :
```Python
item: Item = Body(embed=True)
@@ -136,7 +136,7 @@ comme dans :
{* ../../docs_src/body_multiple_params/tutorial005_an_py310.py hl[17] *}
Dans ce cas **FastAPI** s'attendra à un corps de la requête semblable à :
Dans ce cas **FastAPI** s'attendra à un body semblable à :
```JSON hl_lines="2"
{
@@ -162,10 +162,10 @@ au lieu de :
## Récapitulatif { #recap }
Vous pouvez ajouter plusieurs paramètres du corps de la requête à votre fonction de chemin d'accès, même si une requête ne peut avoir qu'un seul corps de la requête.
Vous pouvez ajouter plusieurs paramètres du body à votre fonction de chemin d'accès, même si une requête ne peut avoir qu'un seul body.
Mais **FastAPI** s'en chargera, vous fournira les bonnes données dans votre fonction, et validera et documentera le schéma correct dans le chemin d'accès.
Vous pouvez également déclarer des valeurs singulières à recevoir dans le corps de la requête.
Vous pouvez également déclarer des valeurs singulières à recevoir dans le body.
Et vous pouvez indiquer à **FastAPI** d'intégrer le corps de la requête sous une clé même lorsqu'un seul paramètre est déclaré.
Et vous pouvez indiquer à **FastAPI** d'intégrer le body sous une clé même lorsqu'un seul paramètre est déclaré.

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
@@ -129,62 +129,62 @@ dev = [
{ include-group = "tests" },
{ include-group = "docs" },
{ include-group = "translations" },
"playwright>=1.57.0",
"prek==0.2.22",
"playwright >=1.57.0",
"prek >=0.2.22",
]
docs = [
{ include-group = "docs-tests" },
"black==25.1.0",
"cairosvg==2.8.2",
"griffe-typingdoc==0.3.0",
"griffe-warnings-deprecated==1.1.0",
"jieba==0.42.1",
"markdown-include-variants==0.0.8",
"mdx-include>=1.4.1,<2.0.0",
"mkdocs-macros-plugin==1.5.0",
"mkdocs-material==9.7.0",
"mkdocs-redirects>=1.2.1,<1.3.0",
"mkdocstrings[python]==0.30.1",
"pillow==11.3.0",
"python-slugify==8.0.4",
"pyyaml>=5.3.1,<7.0.0",
"typer==0.21.1",
"black >=25.1.0",
"cairosvg >=2.8.2",
"griffe-typingdoc >=0.3.0",
"griffe-warnings-deprecated >=1.1.0",
"jieba >=0.42.1",
"markdown-include-variants >=0.0.8",
"mdx-include >=1.4.1,<2.0.0",
"mkdocs-macros-plugin >=1.5.0",
"mkdocs-material >=9.7.0",
"mkdocs-redirects >=1.2.1,<1.3.0",
"mkdocstrings[python] >=0.30.1",
"pillow >=11.3.0",
"python-slugify >=8.0.4",
"pyyaml >=5.3.1,<7.0.0",
"typer >=0.21.1",
]
docs-tests = [
"httpx>=0.23.0,<1.0.0",
"ruff==0.14.14",
"httpx >=0.23.0,<1.0.0",
"ruff >=0.14.14",
]
github-actions = [
"httpx>=0.27.0,<1.0.0",
"pydantic>=2.5.3,<3.0.0",
"pydantic-settings>=2.1.0,<3.0.0",
"pygithub>=2.3.0,<3.0.0",
"pyyaml>=5.3.1,<7.0.0",
"smokeshow>=0.5.0",
"httpx >=0.27.0,<1.0.0",
"pydantic >=2.5.3,<3.0.0",
"pydantic-settings >=2.1.0,<3.0.0",
"pygithub >=2.3.0,<3.0.0",
"pyyaml >=5.3.1,<7.0.0",
"smokeshow >=0.5.0",
]
tests = [
{ include-group = "docs-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",
"inline-snapshot>=0.21.1",
"mypy==1.14.1",
"pwdlib[argon2]>=0.2.1",
"pyjwt==2.9.0",
"pytest>=7.1.3,<9.0.0",
"pytest-codspeed==4.2.0",
"pyyaml>=5.3.1,<7.0.0",
"sqlmodel==0.0.31",
"strawberry-graphql>=0.200.0,<1.0.0",
"types-orjson==3.6.2",
"types-ujson==5.10.0.20240515",
"a2wsgi>=1.9.0,<=2.0.0",
"anyio[trio] >=3.2.1,<5.0.0",
"coverage[toml] >=6.5.0,<8.0",
"dirty-equals >=0.9.0",
"flask >=3.0.0,<4.0.0",
"inline-snapshot >=0.21.1",
"mypy >=1.14.1",
"pwdlib[argon2] >=0.2.1",
"pyjwt >=2.9.0",
"pytest >=7.1.3,<9.0.0",
"pytest-codspeed >=4.2.0",
"pyyaml >=5.3.1,<7.0.0",
"sqlmodel >=0.0.31",
"strawberry-graphql >=0.200.0,<1.0.0",
"types-orjson >=3.6.2",
"types-ujson >=5.10.0.20240515",
"a2wsgi >=1.9.0,<=2.0.0",
]
translations = [
"gitpython==3.1.46",
"pydantic-ai==0.4.10",
"pygithub==2.8.1",
"gitpython >=3.1.46",
"pydantic-ai >=0.4.10",
"pygithub >=2.8.1",
]
[tool.pdm]

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

4069
uv.lock generated
View File

File diff suppressed because it is too large Load Diff