mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-06 12:21:13 -05:00
Compare commits
2 Commits
master
...
update-out
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df9b78b5d1 | ||
|
|
2d46372a6f |
23
.github/workflows/test.yml
vendored
23
.github/workflows/test.yml
vendored
@@ -49,45 +49,31 @@ 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:
|
||||
@@ -106,14 +92,11 @@ jobs:
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install Dependencies
|
||||
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: uv sync --locked --no-dev --group tests --extra all
|
||||
- run: mkdir coverage
|
||||
- name: Test
|
||||
if: matrix.codspeed != 'codspeed'
|
||||
run: uv run --no-sync bash scripts/test.sh
|
||||
run: uv run bash scripts/test.sh
|
||||
env:
|
||||
COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
@@ -125,7 +108,7 @@ jobs:
|
||||
CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}
|
||||
with:
|
||||
mode: simulation
|
||||
run: uv run --no-sync coverage run -m pytest tests/ --codspeed
|
||||
run: uv run 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'
|
||||
|
||||
@@ -7,29 +7,6 @@ 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
|
||||
|
||||
@@ -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 body { #mix-path-query-and-body-parameters }
|
||||
## Mélanger les paramètres `Path`, `Query` et du corps de la requête { #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 body, **FastAPI** saura quoi faire.
|
||||
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.
|
||||
|
||||
Et vous pouvez également déclarer des paramètres du body comme étant optionnels, en leur assignant une valeur par défaut à `None` :
|
||||
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` :
|
||||
|
||||
{* ../../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 body est optionnel. Comme sa valeur par défaut est `None`.
|
||||
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`.
|
||||
|
||||
///
|
||||
|
||||
## Paramètres multiples du body { #multiple-body-parameters }
|
||||
## Paramètres multiples du corps de la requête { #multiple-body-parameters }
|
||||
|
||||
Dans l'exemple précédent, les chemins d'accès attendraient un body JSON avec les attributs d'un `Item`, par exemple :
|
||||
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 :
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -29,13 +29,13 @@ Dans l'exemple précédent, les chemins d'accès attendraient un body JSON avec
|
||||
}
|
||||
```
|
||||
|
||||
Mais vous pouvez également déclarer plusieurs paramètres provenant du body, par exemple `item` et `user` :
|
||||
Mais vous pouvez également déclarer plusieurs paramètres provenant du corps de la requête, 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 body 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 corps de la requête 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 body, et s'attendra à recevoir un body semblable à :
|
||||
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 à :
|
||||
|
||||
```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 body 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 corps de la requête 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 body { #singular-values-in-body }
|
||||
## Valeurs singulières dans le corps de la requête { #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 body, 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 corps de la requête, 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 body en utilisant `Body` :
|
||||
Mais vous pouvez indiquer à **FastAPI** de la traiter comme une autre clé du corps de la requête en utilisant `Body` :
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial003_an_py310.py hl[23] *}
|
||||
|
||||
Dans ce cas, **FastAPI** s'attendra à un body semblable à :
|
||||
Dans ce cas, **FastAPI** s'attendra à un corps de la requête semblable à :
|
||||
|
||||
```JSON
|
||||
{
|
||||
@@ -94,9 +94,9 @@ Dans ce cas, **FastAPI** s'attendra à un body semblable à :
|
||||
|
||||
Encore une fois, il convertira les types de données, validera, documentera, etc.
|
||||
|
||||
## Paramètres multiples du body et paramètres de requête { #multiple-body-params-and-query }
|
||||
## Paramètres multiples du corps de la requête 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 body.
|
||||
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.
|
||||
|
||||
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 body { #embed-a-single-body-parameter }
|
||||
## Intégrer un seul paramètre du corps de la requête { #embed-a-single-body-parameter }
|
||||
|
||||
Supposons que vous n'ayez qu'un seul paramètre `item` dans le body, provenant d'un modèle Pydantic `Item`.
|
||||
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`.
|
||||
|
||||
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 body, 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 corps de la requête, 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 body semblable à :
|
||||
Dans ce cas **FastAPI** s'attendra à un corps de la requête semblable à :
|
||||
|
||||
```JSON hl_lines="2"
|
||||
{
|
||||
@@ -162,10 +162,10 @@ au lieu de :
|
||||
|
||||
## Récapitulatif { #recap }
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 body.
|
||||
Vous pouvez également déclarer des valeurs singulières à recevoir dans le corps de la requête.
|
||||
|
||||
Et vous pouvez indiquer à **FastAPI** d'intégrer le body sous une clé même lorsqu'un seul paramètre est déclaré.
|
||||
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é.
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
checker(q="somequery")
|
||||
```
|
||||
|
||||
…и передаст возвращённое значение как значение зависимости в параметр `fixed_content_included` нашей *функции-обработчика пути*:
|
||||
…и передаст возвращённое значение как значение зависимости в нашу *функцию-обработчике пути* в параметр `fixed_content_included`:
|
||||
|
||||
{* ../../docs_src/dependencies/tutorial011_an_py39.py hl[22] *}
|
||||
|
||||
|
||||
@@ -6,29 +6,13 @@
|
||||
|
||||
## Использование `WSGIMiddleware` { #using-wsgimiddleware }
|
||||
|
||||
/// info | Информация
|
||||
|
||||
Для этого требуется установить `a2wsgi`, например с помощью `pip install a2wsgi`.
|
||||
|
||||
///
|
||||
|
||||
Нужно импортировать `WSGIMiddleware` из `a2wsgi`.
|
||||
Нужно импортировать `WSGIMiddleware`.
|
||||
|
||||
Затем оберните WSGI‑приложение (например, Flask) в middleware (Промежуточный слой).
|
||||
|
||||
После этого смонтируйте его на путь.
|
||||
|
||||
{* ../../docs_src/wsgi/tutorial001_py39.py hl[1,3,23] *}
|
||||
|
||||
/// note | Примечание
|
||||
|
||||
Ранее рекомендовалось использовать `WSGIMiddleware` из `fastapi.middleware.wsgi`, но теперь он помечен как устаревший.
|
||||
|
||||
Вместо него рекомендуется использовать пакет `a2wsgi`. Использование остаётся таким же.
|
||||
|
||||
Просто убедитесь, что пакет `a2wsgi` установлен, и импортируйте `WSGIMiddleware` из `a2wsgi`.
|
||||
|
||||
///
|
||||
{* ../../docs_src/wsgi/tutorial001_py39.py hl[2:3,3] *}
|
||||
|
||||
## Проверьте { #check-it }
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ Successfully installed fastapi pydantic
|
||||
* Создайте файл `main.py` со следующим содержимым:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -156,7 +158,7 @@ def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: str | None = None):
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
|
||||
@@ -161,6 +161,8 @@ $ pip install "fastapi[standard]"
|
||||
Создайте файл `main.py` со следующим содержимым:
|
||||
|
||||
```Python
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -172,7 +174,7 @@ def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: str | None = None):
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
@@ -181,7 +183,9 @@ def read_item(item_id: int, q: str | None = None):
|
||||
|
||||
Если ваш код использует `async` / `await`, используйте `async def`:
|
||||
|
||||
```Python hl_lines="7 12"
|
||||
```Python hl_lines="9 14"
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
@@ -193,7 +197,7 @@ async def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
async def read_item(item_id: int, q: str | None = None):
|
||||
async def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
```
|
||||
|
||||
@@ -284,7 +288,9 @@ INFO: Application startup complete.
|
||||
|
||||
Объявите тело запроса, используя стандартные типы Python, спасибо Pydantic.
|
||||
|
||||
```Python hl_lines="2 7-10 23-25"
|
||||
```Python hl_lines="4 9-12 25-27"
|
||||
from typing import Union
|
||||
|
||||
from fastapi import FastAPI
|
||||
from pydantic import BaseModel
|
||||
|
||||
@@ -294,7 +300,7 @@ app = FastAPI()
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: bool | None = None
|
||||
is_offer: Union[bool, None] = None
|
||||
|
||||
|
||||
@app.get("/")
|
||||
@@ -303,7 +309,7 @@ def read_root():
|
||||
|
||||
|
||||
@app.get("/items/{item_id}")
|
||||
def read_item(item_id: int, q: str | None = None):
|
||||
def read_item(item_id: int, q: Union[str, None] = None):
|
||||
return {"item_id": item_id, "q": q}
|
||||
|
||||
|
||||
|
||||
@@ -101,13 +101,13 @@
|
||||
Поскольку по умолчанию, отдельные значения интерпретируются как query-параметры, вам не нужно явно добавлять `Query`, вы можете просто сделать так:
|
||||
|
||||
```Python
|
||||
q: str | None = None
|
||||
q: Union[str, None] = None
|
||||
```
|
||||
|
||||
Или в Python 3.9:
|
||||
Или в Python 3.10 и выше:
|
||||
|
||||
```Python
|
||||
q: Union[str, None] = None
|
||||
q: str | None = None
|
||||
```
|
||||
|
||||
Например:
|
||||
@@ -116,7 +116,7 @@ q: Union[str, None] = None
|
||||
|
||||
/// info | Информация
|
||||
|
||||
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`, `Path` и других, которые вы увидите позже.
|
||||
`Body` также имеет все те же дополнительные параметры валидации и метаданных, как у `Query`,`Path` и других, которые вы увидите позже.
|
||||
|
||||
///
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
Вы можете добавить параметры `summary` и `description`:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[17:18] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial003_py310.py hl[18:19] *}
|
||||
|
||||
## Описание из строк документации { #description-from-docstring }
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
|
||||
Вы можете указать описание ответа с помощью параметра `response_description`:
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *}
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[19] *}
|
||||
|
||||
/// info | Дополнительная информация
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
///
|
||||
|
||||
/// check | Проверка
|
||||
/// check
|
||||
|
||||
OpenAPI указывает, что каждой *операции пути* необходимо описание ответа.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.128.3"
|
||||
__version__ = "0.128.2"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -1,31 +1,22 @@
|
||||
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 (
|
||||
AbstractAsyncContextManager,
|
||||
AbstractContextManager,
|
||||
AsyncExitStack,
|
||||
asynccontextmanager,
|
||||
)
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from enum import Enum, IntEnum
|
||||
from typing import (
|
||||
Annotated,
|
||||
Any,
|
||||
Callable,
|
||||
Optional,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
@@ -152,50 +143,6 @@ 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]:
|
||||
@@ -213,30 +160,6 @@ 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] = {}
|
||||
|
||||
@@ -980,33 +903,13 @@ 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,
|
||||
lifespan=lifespan_context,
|
||||
on_startup=on_startup,
|
||||
on_shutdown=on_shutdown,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
if prefix:
|
||||
assert prefix.startswith("/"), "A path prefix must start with '/'"
|
||||
@@ -4570,58 +4473,6 @@ 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.
|
||||
|
||||
@@ -43,7 +43,7 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP",
|
||||
]
|
||||
dependencies = [
|
||||
"starlette>=0.40.0,<1.0.0",
|
||||
"starlette>=0.40.0,<0.51.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 >=5.8.0",
|
||||
"ujson >=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0",
|
||||
# For ORJSONResponse
|
||||
"orjson >=3.9.3",
|
||||
"orjson >=3.2.1",
|
||||
# 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 >=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",
|
||||
"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",
|
||||
]
|
||||
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]
|
||||
|
||||
@@ -241,79 +241,3 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user