mirror of
https://github.com/fastapi/fastapi.git
synced 2026-06-15 11:01:13 -04:00
Compare commits
1 Commits
fix-respon
...
translate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4a32bd3f |
@@ -7,13 +7,6 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.137.1 (2026-06-15)
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🚨 Fix typing checks for APIRoute. PR [#15765](https://github.com/fastapi/fastapi/pull/15765) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🐛 Fix bug, allow empty path in path operation in prefixless router. PR [#15763](https://github.com/fastapi/fastapi/pull/15763) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.137.0 (2026-06-14)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -143,7 +143,7 @@ Les principales fonctionnalités sont :
|
||||
|
||||
---
|
||||
|
||||
« _Si quelqu’un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **magnifiquement conçu**, **simple à utiliser** et **hautement scalable** — il est devenu un **composant clé** de notre stratégie de développement API-first._ »
|
||||
« _Si quelqu’un cherche à construire une API Python de production, je recommande vivement **FastAPI**. Il est **magnifiquement conçu**, **simple à utiliser** et **hautement scalable**, il est devenu un **composant clé** de notre stratégie de développement API-first et alimente de nombreuses automatisations et services tels que notre Virtual TAC Engineer._ »
|
||||
|
||||
<div style="text-align: right; margin-right: 10%;">Deon Pillsbury - <strong>Cisco</strong> <a href="https://www.linkedin.com/posts/deonpillsbury_cisco-cx-python-activity-6963242628536487936-trAp/"><small>(ref)</small></a></div>
|
||||
|
||||
@@ -492,9 +492,7 @@ Pour un exemple plus complet comprenant plus de fonctionnalités, voir le <a hre
|
||||
|
||||
### Déployer votre application (optionnel) { #deploy-your-app-optional }
|
||||
|
||||
Vous pouvez, si vous le souhaitez, déployer votre application FastAPI sur [FastAPI Cloud](https://fastapicloud.com), allez vous inscrire sur la liste d'attente si ce n'est pas déjà fait. 🚀
|
||||
|
||||
Si vous avez déjà un compte **FastAPI Cloud** (nous vous avons invité depuis la liste d'attente 😉), vous pouvez déployer votre application avec une seule commande.
|
||||
Vous pouvez, si vous le souhaitez, déployer votre application FastAPI sur [FastAPI Cloud](https://fastapicloud.com) avec une seule commande. 🚀
|
||||
|
||||
<div class="termy">
|
||||
|
||||
@@ -510,6 +508,8 @@ Deploying to FastAPI Cloud...
|
||||
|
||||
</div>
|
||||
|
||||
La CLI détectera automatiquement votre application FastAPI et la déploiera dans le cloud. Si vous n'êtes pas connecté, votre navigateur s'ouvrira pour terminer le processus d'authentification.
|
||||
|
||||
C'est tout ! Vous pouvez maintenant accéder à votre application à cette URL. ✨
|
||||
|
||||
#### À propos de FastAPI Cloud { #about-fastapi-cloud }
|
||||
|
||||
@@ -108,7 +108,7 @@ Par exemple :
|
||||
|
||||
{* ../../docs_src/body_multiple_params/tutorial004_an_py310.py hl[28] *}
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
`Body` possède également les mêmes paramètres supplémentaires de validation et de métadonnées que `Query`, `Path` et d'autres que vous verrez plus tard.
|
||||
|
||||
@@ -123,7 +123,7 @@ 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` :
|
||||
|
||||
```Python
|
||||
item: Item = Body(embed=True)
|
||||
item: Annotated[Item, Body(embed=True)]
|
||||
```
|
||||
|
||||
comme dans :
|
||||
|
||||
@@ -24,13 +24,13 @@ Mais rappelez-vous que lorsque vous importez `Query`, `Path`, `Cookie` et d'autr
|
||||
|
||||
///
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Pour déclarer des cookies, vous devez utiliser `Cookie`, sinon les paramètres seraient interprétés comme des paramètres de requête.
|
||||
|
||||
///
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Gardez à l'esprit que, comme **les navigateurs gèrent les cookies** de manière particulière et en coulisses, ils **n'autorisent pas** facilement **JavaScript** à y accéder.
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ Ainsi, la ligne :
|
||||
|
||||
ne sera pas exécutée.
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Pour plus d'informations, consultez [la documentation officielle de Python](https://docs.python.org/3/library/__main__.html).
|
||||
|
||||
|
||||
@@ -72,13 +72,13 @@ Vous pouvez spécifier la description de la réponse avec le paramètre `respons
|
||||
|
||||
{* ../../docs_src/path_operation_configuration/tutorial005_py310.py hl[18] *}
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Notez que `response_description` se réfère spécifiquement à la réponse, tandis que `description` se réfère au *chemin d'accès* en général.
|
||||
|
||||
///
|
||||
|
||||
/// check | Vérifications
|
||||
/// tip | Astuce
|
||||
|
||||
OpenAPI spécifie que chaque *chemin d'accès* requiert une description de réponse.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Tout d'abord, importez `Path` de `fastapi`, et importez `Annotated` :
|
||||
|
||||
{* ../../docs_src/path_params_numeric_validations/tutorial001_an_py310.py hl[1,3] *}
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
FastAPI a ajouté le support pour `Annotated` (et a commencé à le recommander) dans la version 0.95.0.
|
||||
|
||||
@@ -131,7 +131,7 @@ Et vous pouvez également déclarer des validations numériques :
|
||||
* `lt` : `l`ess `t`han
|
||||
* `le` : `l`ess than or `e`qual
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
`Query`, `Path`, et d'autres classes que vous verrez plus tard sont des sous-classes d'une classe commune `Param`.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Vous pouvez définir des fichiers à téléverser par le client en utilisant `File`.
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Pour recevoir des fichiers téléversés, installez d'abord [`python-multipart`](https://github.com/Kludex/python-multipart).
|
||||
|
||||
@@ -28,7 +28,7 @@ Créez des paramètres de fichier de la même manière que pour `Body` ou `Form`
|
||||
|
||||
{* ../../docs_src/request_files/tutorial001_an_py310.py hl[9] *}
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
`File` est une classe qui hérite directement de `Form`.
|
||||
|
||||
@@ -44,7 +44,7 @@ Pour déclarer des fichiers dans le corps de la requête, vous devez utiliser `F
|
||||
|
||||
Les fichiers seront téléversés en « données de formulaire ».
|
||||
|
||||
Si vous déclarez le type de votre paramètre de *fonction de chemin d'accès* comme `bytes`, **FastAPI** lira le fichier pour vous et vous recevrez le contenu sous forme de `bytes`.
|
||||
Si vous déclarez le type de votre *fonction de chemin d'accès* comme `bytes`, **FastAPI** lira le fichier pour vous et vous recevrez le contenu sous forme de `bytes`.
|
||||
|
||||
Gardez à l'esprit que cela signifie que tout le contenu sera stocké en mémoire. Cela fonctionnera bien pour de petits fichiers.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Vous pouvez définir des fichiers et des champs de formulaire en même temps à l'aide de `File` et `Form`.
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Pour recevoir des fichiers téléversés et/ou des données de formulaire, installez d'abord [`python-multipart`](https://github.com/Kludex/python-multipart).
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Remarquez que `status_code` est un paramètre de la méthode « decorator » (`g
|
||||
|
||||
Le paramètre `status_code` reçoit un nombre correspondant au code d'état HTTP.
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
`status_code` peut aussi recevoir un `IntEnum`, comme le [`http.HTTPStatus`](https://docs.python.org/3/library/http.html#http.HTTPStatus) de Python.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Avec cela, vous pouvez utiliser [pytest](https://docs.pytest.org/) directement a
|
||||
|
||||
## Utiliser `TestClient` { #using-testclient }
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Pour utiliser `TestClient`, installez d’abord [`httpx`](https://www.python-httpx.org).
|
||||
|
||||
@@ -144,7 +144,7 @@ Par exemple :
|
||||
|
||||
Pour plus d’informations sur la manière de transmettre des données au backend (en utilisant `httpx` ou le `TestClient`), consultez la [documentation HTTPX](https://www.python-httpx.org).
|
||||
|
||||
/// info
|
||||
/// note | Remarque
|
||||
|
||||
Notez que le `TestClient` reçoit des données qui peuvent être converties en JSON, pas des modèles Pydantic.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.137.1"
|
||||
__version__ = "0.137.0"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -930,6 +930,28 @@ def _populate_api_route_state(
|
||||
route.path = path
|
||||
route.endpoint = endpoint
|
||||
route.stream_item_type = None
|
||||
if isinstance(response_model, DefaultPlaceholder):
|
||||
return_annotation = get_typed_return_annotation(endpoint)
|
||||
if lenient_issubclass(return_annotation, Response):
|
||||
response_model = None
|
||||
else:
|
||||
stream_item = get_stream_item_type(return_annotation)
|
||||
if stream_item is not None:
|
||||
# Extract item type for JSONL or SSE streaming when
|
||||
# response_class is DefaultPlaceholder (JSONL) or
|
||||
# EventSourceResponse (SSE).
|
||||
# ServerSentEvent is excluded: it's a transport
|
||||
# wrapper, not a data model, so it shouldn't feed
|
||||
# into validation or OpenAPI schema generation.
|
||||
if (
|
||||
isinstance(response_class, DefaultPlaceholder)
|
||||
or lenient_issubclass(response_class, EventSourceResponse)
|
||||
) and not lenient_issubclass(stream_item, ServerSentEvent):
|
||||
route.stream_item_type = stream_item
|
||||
response_model = None
|
||||
else:
|
||||
response_model = return_annotation
|
||||
route.response_model = response_model
|
||||
route.summary = summary
|
||||
route.response_description = response_description
|
||||
route.deprecated = deprecated
|
||||
@@ -965,6 +987,27 @@ def _populate_api_route_state(
|
||||
if isinstance(status_code, IntEnum):
|
||||
status_code = int(status_code)
|
||||
route.status_code = status_code
|
||||
if route.response_model:
|
||||
assert is_body_allowed_for_status_code(status_code), (
|
||||
f"Status code {status_code} must not have a response body"
|
||||
)
|
||||
response_name = "Response_" + route.unique_id
|
||||
route.response_field = create_model_field(
|
||||
name=response_name,
|
||||
type_=route.response_model,
|
||||
mode="serialization",
|
||||
)
|
||||
else:
|
||||
route.response_field = None
|
||||
if route.stream_item_type:
|
||||
stream_item_name = "StreamItem_" + route.unique_id
|
||||
route.stream_item_field = create_model_field(
|
||||
name=stream_item_name,
|
||||
type_=route.stream_item_type,
|
||||
mode="serialization",
|
||||
)
|
||||
else:
|
||||
route.stream_item_field = None
|
||||
route.dependencies = list(dependencies or [])
|
||||
route.description = description or inspect.cleandoc(route.endpoint.__doc__ or "")
|
||||
# if a "form feed" character (page break) is found in the description text,
|
||||
@@ -1016,88 +1059,9 @@ def _populate_api_route_state(
|
||||
route.is_json_stream = is_generator and isinstance(
|
||||
response_class, DefaultPlaceholder
|
||||
)
|
||||
if isinstance(response_model, DefaultPlaceholder):
|
||||
return_annotation = get_typed_return_annotation(endpoint)
|
||||
if lenient_issubclass(return_annotation, Response):
|
||||
response_model = None
|
||||
else:
|
||||
stream_item = get_stream_item_type(return_annotation)
|
||||
if stream_item is not None and is_generator:
|
||||
# Extract item type for JSONL or SSE streaming for
|
||||
# generator endpoints when response_class is
|
||||
# DefaultPlaceholder (JSONL) or EventSourceResponse
|
||||
# (SSE).
|
||||
# ServerSentEvent is excluded: it's a transport
|
||||
# wrapper, not a data model, so it shouldn't feed
|
||||
# into validation or OpenAPI schema generation.
|
||||
if (
|
||||
isinstance(response_class, DefaultPlaceholder)
|
||||
or lenient_issubclass(response_class, EventSourceResponse)
|
||||
) and not lenient_issubclass(stream_item, ServerSentEvent):
|
||||
route.stream_item_type = stream_item
|
||||
response_model = None
|
||||
else:
|
||||
response_model = return_annotation
|
||||
route.response_model = response_model
|
||||
if route.response_model:
|
||||
assert is_body_allowed_for_status_code(status_code), (
|
||||
f"Status code {status_code} must not have a response body"
|
||||
)
|
||||
response_name = "Response_" + route.unique_id
|
||||
route.response_field = create_model_field(
|
||||
name=response_name,
|
||||
type_=route.response_model,
|
||||
mode="serialization",
|
||||
)
|
||||
else:
|
||||
route.response_field = None
|
||||
if route.stream_item_type:
|
||||
stream_item_name = "StreamItem_" + route.unique_id
|
||||
route.stream_item_field = create_model_field(
|
||||
name=stream_item_name,
|
||||
type_=route.stream_item_type,
|
||||
mode="serialization",
|
||||
)
|
||||
else:
|
||||
route.stream_item_field = None
|
||||
|
||||
|
||||
class APIRoute(routing.Route):
|
||||
stream_item_type: Any | None
|
||||
response_model: Any
|
||||
summary: str | None
|
||||
response_description: str
|
||||
deprecated: bool | None
|
||||
operation_id: str | None
|
||||
response_model_include: IncEx | None
|
||||
response_model_exclude: IncEx | None
|
||||
response_model_by_alias: bool
|
||||
response_model_exclude_unset: bool
|
||||
response_model_exclude_defaults: bool
|
||||
response_model_exclude_none: bool
|
||||
include_in_schema: bool
|
||||
response_class: type[Response] | DefaultPlaceholder
|
||||
dependency_overrides_provider: Any | None
|
||||
callbacks: list[BaseRoute] | None
|
||||
openapi_extra: dict[str, Any] | None
|
||||
generate_unique_id_function: Callable[[Any], str] | DefaultPlaceholder
|
||||
strict_content_type: bool | DefaultPlaceholder
|
||||
tags: list[str | Enum]
|
||||
responses: dict[int | str, dict[str, Any]]
|
||||
unique_id: str
|
||||
status_code: int | None
|
||||
response_field: ModelField | None
|
||||
stream_item_field: ModelField | None
|
||||
dependencies: list[params.Depends]
|
||||
description: str
|
||||
response_fields: dict[int | str, ModelField]
|
||||
dependant: Dependant
|
||||
_flat_dependant: Dependant
|
||||
_embed_body_fields: bool
|
||||
body_field: ModelField | None
|
||||
is_sse_stream: bool
|
||||
is_json_stream: bool
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
@@ -2471,16 +2435,9 @@ class APIRouter(routing.Router):
|
||||
"A path prefix must not end with '/', as the routes will start with '/'"
|
||||
)
|
||||
else:
|
||||
for route, route_context in _iter_routes_with_context(router.routes):
|
||||
if route_context is None:
|
||||
path = getattr(route, "path", None)
|
||||
name = getattr(route, "name", "unknown")
|
||||
elif route_context.starlette_route is not None:
|
||||
path = getattr(route_context.starlette_route, "path", None)
|
||||
name = getattr(route_context.starlette_route, "name", "unknown")
|
||||
else:
|
||||
path = route_context.path
|
||||
name = route_context.name
|
||||
for r in _iter_included_route_candidates(router.routes):
|
||||
path = getattr(r, "path", None)
|
||||
name = getattr(r, "name", "unknown")
|
||||
if path is not None and not path:
|
||||
raise FastAPIError(
|
||||
f"Prefix and path cannot be both empty (path operation: {name})"
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Annotated, cast
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, Body, Depends, FastAPI, Request
|
||||
from fastapi.exceptions import FastAPIError
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from fastapi.routing import (
|
||||
APIRoute,
|
||||
@@ -808,60 +807,6 @@ def test_no_prefix_include_validation_sees_effective_starlette_route_candidates(
|
||||
assert cast(Route, candidates[0]).path == "/child/items"
|
||||
|
||||
|
||||
def test_no_prefix_include_validation_sees_effective_api_route_path():
|
||||
leaf_router = APIRouter()
|
||||
|
||||
@leaf_router.get("")
|
||||
def read_items():
|
||||
return []
|
||||
|
||||
parent_router = APIRouter()
|
||||
parent_router.include_router(leaf_router, prefix="/items")
|
||||
|
||||
# for coverage
|
||||
candidates = list(_iter_included_route_candidates(parent_router.routes))
|
||||
assert cast(APIRoute, candidates[0]).path == ""
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(parent_router)
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/items")
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
def test_no_prefix_include_validation_sees_effective_starlette_route_path():
|
||||
def endpoint(request):
|
||||
return PlainTextResponse("ok")
|
||||
|
||||
child_router = APIRouter(routes=[Route("/items", endpoint, name="read_items")])
|
||||
parent_router = APIRouter()
|
||||
parent_router.include_router(child_router, prefix="/child")
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(parent_router)
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/child/items")
|
||||
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.text == "ok"
|
||||
|
||||
|
||||
def test_no_prefix_include_validation_rejects_empty_effective_api_route_path():
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("")
|
||||
def read_items(): # pragma: no cover
|
||||
return []
|
||||
|
||||
app = FastAPI()
|
||||
with pytest.raises(FastAPIError):
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
def test_apirouter_matches_fallback_without_include_context():
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from collections.abc import Iterable
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import BaseModel
|
||||
@@ -67,21 +65,6 @@ def get_exclude_unset_none() -> ModelDefaults:
|
||||
return ModelDefaults(x=None, y="y")
|
||||
|
||||
|
||||
@app.get("/iterable_exclude_unset", response_model_exclude_unset=True)
|
||||
def get_iterable_exclude_unset() -> Iterable[ModelDefaults]:
|
||||
return [ModelDefaults(x=None, y="y")]
|
||||
|
||||
|
||||
@app.get("/iterable_exclude_defaults", response_model_exclude_defaults=True)
|
||||
def get_iterable_exclude_defaults() -> Iterable[ModelDefaults]:
|
||||
return [ModelDefaults(x=None, y="y")]
|
||||
|
||||
|
||||
@app.get("/iterable_exclude_none", response_model_exclude_none=True)
|
||||
def get_iterable_exclude_none() -> Iterable[ModelDefaults]:
|
||||
return [ModelDefaults(x=None, y="y")]
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@@ -108,18 +91,3 @@ def test_return_exclude_none():
|
||||
def test_return_exclude_unset_none():
|
||||
response = client.get("/exclude_unset_none")
|
||||
assert response.json() == {"y": "y"}
|
||||
|
||||
|
||||
def test_return_iterable_exclude_unset():
|
||||
response = client.get("/iterable_exclude_unset")
|
||||
assert response.json() == [{"x": None, "y": "y"}]
|
||||
|
||||
|
||||
def test_return_iterable_exclude_defaults():
|
||||
response = client.get("/iterable_exclude_defaults")
|
||||
assert response.json() == [{}]
|
||||
|
||||
|
||||
def test_return_iterable_exclude_none():
|
||||
response = client.get("/iterable_exclude_none")
|
||||
assert response.json() == [{"y": "y", "z": "z"}]
|
||||
|
||||
Reference in New Issue
Block a user