mirror of
https://github.com/fastapi/fastapi.git
synced 2026-02-06 12:21:13 -05:00
Compare commits
13 Commits
update-out
...
0.128.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79406a4b04 | ||
|
|
de56c96c64 | ||
|
|
570e592a03 | ||
|
|
110b45d9b2 | ||
|
|
72325f698f | ||
|
|
8bdbd3725f | ||
|
|
23ddf09dd1 | ||
|
|
6646e2b94f | ||
|
|
c5fd75a321 | ||
|
|
54f8aeeb9a | ||
|
|
97145588f5 | ||
|
|
0dd42b746e | ||
|
|
b49435becd |
6
.github/workflows/translate.yml
vendored
6
.github/workflows/translate.yml
vendored
@@ -35,6 +35,11 @@ on:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
max:
|
||||
description: Maximum number of items to translate (e.g. 10)
|
||||
type: number
|
||||
required: false
|
||||
default: 10
|
||||
|
||||
jobs:
|
||||
langs:
|
||||
@@ -115,3 +120,4 @@ jobs:
|
||||
EN_PATH: ${{ github.event.inputs.en_path }}
|
||||
COMMAND: ${{ matrix.command }}
|
||||
COMMIT_IN_PLACE: ${{ github.event.inputs.commit_in_place }}
|
||||
MAX: ${{ github.event.inputs.max }}
|
||||
|
||||
@@ -7,6 +7,17 @@ hide:
|
||||
|
||||
## Latest Changes
|
||||
|
||||
## 0.128.2
|
||||
|
||||
### Features
|
||||
|
||||
* ✨ Add support for PEP695 `TypeAliasType`. PR [#13920](https://github.com/fastapi/fastapi/pull/13920) by [@cstruct](https://github.com/cstruct).
|
||||
* ✨ Allow `Response` type hint as dependency annotation. PR [#14794](https://github.com/fastapi/fastapi/pull/14794) by [@jonathan-fulton](https://github.com/jonathan-fulton).
|
||||
|
||||
### Fixes
|
||||
|
||||
* 🐛 Fix using `Json[list[str]]` type (issue #10997). PR [#14616](https://github.com/fastapi/fastapi/pull/14616) by [@mkanetsuna](https://github.com/mkanetsuna).
|
||||
|
||||
### Docs
|
||||
|
||||
* 📝 Update docs for translations. PR [#14830](https://github.com/fastapi/fastapi/pull/14830) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -14,6 +25,8 @@ hide:
|
||||
|
||||
### Translations
|
||||
|
||||
* 🌐 Enable Traditional Chinese translations. PR [#14842](https://github.com/fastapi/fastapi/pull/14842) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Enable French docs translations. PR [#14841](https://github.com/fastapi/fastapi/pull/14841) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for fr (translate-page). PR [#14837](https://github.com/fastapi/fastapi/pull/14837) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for de (update-outdated). PR [#14836](https://github.com/fastapi/fastapi/pull/14836) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🌐 Update translations for pt (update-outdated). PR [#14833](https://github.com/fastapi/fastapi/pull/14833) by [@tiangolo](https://github.com/tiangolo).
|
||||
@@ -26,6 +39,10 @@ hide:
|
||||
* 🌐 Update translations for uk (update-outdated). PR [#14822](https://github.com/fastapi/fastapi/pull/14822) by [@tiangolo](https://github.com/tiangolo).
|
||||
* 🔨 Update docs and translations scripts, enable Turkish. PR [#14824](https://github.com/fastapi/fastapi/pull/14824) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
### Internal
|
||||
|
||||
* 🔨 Add max pages to translate to configs. PR [#14840](https://github.com/fastapi/fastapi/pull/14840) by [@tiangolo](https://github.com/tiangolo).
|
||||
|
||||
## 0.128.1
|
||||
|
||||
### Features
|
||||
|
||||
@@ -317,6 +317,8 @@ extra:
|
||||
name: de - Deutsch
|
||||
- link: /es/
|
||||
name: es - español
|
||||
- link: /fr/
|
||||
name: fr - français
|
||||
- link: /ja/
|
||||
name: ja - 日本語
|
||||
- link: /ko/
|
||||
@@ -329,6 +331,8 @@ extra:
|
||||
name: tr - Türkçe
|
||||
- link: /uk/
|
||||
name: uk - українська мова
|
||||
- link: /zh-hant/
|
||||
name: zh-hant - 繁體中文
|
||||
extra_css:
|
||||
- css/termynal.css
|
||||
- css/custom.css
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
|
||||
|
||||
__version__ = "0.128.1"
|
||||
__version__ = "0.128.2"
|
||||
|
||||
from starlette import status as status
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ from fastapi.logger import logger
|
||||
from fastapi.security.oauth2 import SecurityScopes
|
||||
from fastapi.types import DependencyCacheKey
|
||||
from fastapi.utils import create_model_field, get_path_param_names
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Json
|
||||
from pydantic.fields import FieldInfo
|
||||
from starlette.background import BackgroundTasks as StarletteBackgroundTasks
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
@@ -66,6 +66,7 @@ from starlette.requests import HTTPConnection, Request
|
||||
from starlette.responses import Response
|
||||
from starlette.websockets import WebSocket
|
||||
from typing_extensions import Literal, get_args, get_origin
|
||||
from typing_inspection.typing_objects import is_typealiastype
|
||||
|
||||
multipart_not_installed_error = (
|
||||
'Form data requires "python-multipart" to be installed. \n'
|
||||
@@ -370,6 +371,9 @@ def analyze_param(
|
||||
depends = None
|
||||
type_annotation: Any = Any
|
||||
use_annotation: Any = Any
|
||||
if is_typealiastype(annotation):
|
||||
# unpack in case PEP 695 type syntax is used
|
||||
annotation = annotation.__value__
|
||||
if annotation is not inspect.Signature.empty:
|
||||
use_annotation = annotation
|
||||
type_annotation = annotation
|
||||
@@ -449,7 +453,9 @@ def analyze_param(
|
||||
depends = dataclasses.replace(depends, dependency=type_annotation)
|
||||
|
||||
# Handle non-param type annotations like Request
|
||||
if lenient_issubclass(
|
||||
# Only apply special handling when there's no explicit Depends - if there's a Depends,
|
||||
# the dependency will be called and its return value used instead of the special injection
|
||||
if depends is None and lenient_issubclass(
|
||||
type_annotation,
|
||||
(
|
||||
Request,
|
||||
@@ -460,7 +466,6 @@ def analyze_param(
|
||||
SecurityScopes,
|
||||
),
|
||||
):
|
||||
assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}"
|
||||
assert field_info is None, (
|
||||
f"Cannot specify FastAPI annotation for type {type_annotation!r}"
|
||||
)
|
||||
@@ -721,11 +726,19 @@ def _validate_value_with_model_field(
|
||||
return v_, []
|
||||
|
||||
|
||||
def _is_json_field(field: ModelField) -> bool:
|
||||
return any(type(item) is Json for item in field.field_info.metadata)
|
||||
|
||||
|
||||
def _get_multidict_value(
|
||||
field: ModelField, values: Mapping[str, Any], alias: Union[str, None] = None
|
||||
) -> Any:
|
||||
alias = alias or get_validation_alias(field)
|
||||
if is_sequence_field(field) and isinstance(values, (ImmutableMultiDict, Headers)):
|
||||
if (
|
||||
(not _is_json_field(field))
|
||||
and is_sequence_field(field)
|
||||
and isinstance(values, (ImmutableMultiDict, Headers))
|
||||
):
|
||||
value = values.getlist(alias)
|
||||
else:
|
||||
value = values.get(alias, None)
|
||||
|
||||
@@ -46,6 +46,7 @@ dependencies = [
|
||||
"starlette>=0.40.0,<0.51.0",
|
||||
"pydantic>=2.7.0",
|
||||
"typing-extensions>=4.8.0",
|
||||
"typing-inspection>=0.4.2",
|
||||
"annotated-doc>=0.0.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ SUPPORTED_LANGS = {
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
# "fr",
|
||||
"fr",
|
||||
"ja",
|
||||
"ko",
|
||||
"pt",
|
||||
@@ -31,7 +31,7 @@ SUPPORTED_LANGS = {
|
||||
"tr",
|
||||
"uk",
|
||||
# "zh",
|
||||
# "zh-hant",
|
||||
"zh-hant",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -347,9 +347,12 @@ def list_outdated(language: str) -> list[Path]:
|
||||
|
||||
|
||||
@app.command()
|
||||
def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None:
|
||||
def update_outdated(
|
||||
language: Annotated[str, typer.Option(envvar="LANGUAGE")],
|
||||
max: Annotated[int, typer.Option(envvar="MAX")] = 10,
|
||||
) -> None:
|
||||
outdated_paths = list_outdated(language)
|
||||
for path in outdated_paths:
|
||||
for path in outdated_paths[:max]:
|
||||
print(f"Updating lang: {language} path: {path}")
|
||||
translate_page(language=language, en_path=path)
|
||||
print(f"Done updating: {path}")
|
||||
@@ -357,9 +360,12 @@ def update_outdated(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -
|
||||
|
||||
|
||||
@app.command()
|
||||
def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None:
|
||||
def add_missing(
|
||||
language: Annotated[str, typer.Option(envvar="LANGUAGE")],
|
||||
max: Annotated[int, typer.Option(envvar="MAX")] = 10,
|
||||
) -> None:
|
||||
missing_paths = list_missing(language)
|
||||
for path in missing_paths:
|
||||
for path in missing_paths[:max]:
|
||||
print(f"Adding lang: {language} path: {path}")
|
||||
translate_page(language=language, en_path=path)
|
||||
print(f"Done adding: {path}")
|
||||
@@ -367,11 +373,14 @@ def add_missing(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> No
|
||||
|
||||
|
||||
@app.command()
|
||||
def update_and_add(language: Annotated[str, typer.Option(envvar="LANGUAGE")]) -> None:
|
||||
def update_and_add(
|
||||
language: Annotated[str, typer.Option(envvar="LANGUAGE")],
|
||||
max: Annotated[int, typer.Option(envvar="MAX")] = 10,
|
||||
) -> None:
|
||||
print(f"Updating outdated translations for {language}")
|
||||
update_outdated(language=language)
|
||||
update_outdated(language=language, max=max)
|
||||
print(f"Adding missing translations for {language}")
|
||||
add_missing(language=language)
|
||||
add_missing(language=language, max=max)
|
||||
print(f"Done updating and adding for {language}")
|
||||
|
||||
|
||||
|
||||
27
tests/test_dependency_pep695.py
Normal file
27
tests/test_dependency_pep695.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
|
||||
async def some_value() -> int:
|
||||
return 123
|
||||
|
||||
|
||||
DependedValue = TypeAliasType(
|
||||
"DependedValue", Annotated[int, Depends(some_value)], type_params=()
|
||||
)
|
||||
|
||||
|
||||
def test_pep695_type_dependencies():
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
async def get_with_dep(value: DependedValue) -> str: # noqa
|
||||
return f"value: {value}"
|
||||
|
||||
client = TestClient(app)
|
||||
response = client.get("/")
|
||||
assert response.status_code == 200
|
||||
assert response.text == '"value: 123"'
|
||||
63
tests/test_json_type.py
Normal file
63
tests/test_json_type.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import json
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Cookie, FastAPI, Form, Header, Query
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import Json
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.post("/form-json-list")
|
||||
def form_json_list(items: Annotated[Json[list[str]], Form()]) -> list[str]:
|
||||
return items
|
||||
|
||||
|
||||
@app.get("/query-json-list")
|
||||
def query_json_list(items: Annotated[Json[list[str]], Query()]) -> list[str]:
|
||||
return items
|
||||
|
||||
|
||||
@app.get("/header-json-list")
|
||||
def header_json_list(x_items: Annotated[Json[list[str]], Header()]) -> list[str]:
|
||||
return x_items
|
||||
|
||||
|
||||
@app.get("/cookie-json-list")
|
||||
def cookie_json_list(items: Annotated[Json[list[str]], Cookie()]) -> list[str]:
|
||||
return items
|
||||
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
def test_form_json_list():
|
||||
response = client.post(
|
||||
"/form-json-list", data={"items": json.dumps(["abc", "def"])}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == ["abc", "def"]
|
||||
|
||||
|
||||
def test_query_json_list():
|
||||
response = client.get(
|
||||
"/query-json-list", params={"items": json.dumps(["abc", "def"])}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == ["abc", "def"]
|
||||
|
||||
|
||||
def test_header_json_list():
|
||||
response = client.get(
|
||||
"/header-json-list", headers={"x-items": json.dumps(["abc", "def"])}
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == ["abc", "def"]
|
||||
|
||||
|
||||
def test_cookie_json_list():
|
||||
client.cookies.set("items", json.dumps(["abc", "def"]))
|
||||
response = client.get("/cookie-json-list")
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json() == ["abc", "def"]
|
||||
client.cookies.clear()
|
||||
173
tests/test_response_dependency.py
Normal file
173
tests/test_response_dependency.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Test using special types (Response, Request, BackgroundTasks) as dependency annotations.
|
||||
|
||||
These tests verify that special FastAPI types can be used with Depends() annotations
|
||||
and that the dependency injection system properly handles them.
|
||||
"""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import BackgroundTasks, Depends, FastAPI, Request, Response
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
def test_response_with_depends_annotated():
|
||||
"""Response type hint should work with Annotated[Response, Depends(...)]."""
|
||||
app = FastAPI()
|
||||
|
||||
def modify_response(response: Response) -> Response:
|
||||
response.headers["X-Custom"] = "modified"
|
||||
return response
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(response: Annotated[Response, Depends(modify_response)]):
|
||||
return {"status": "ok"}
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"status": "ok"}
|
||||
assert resp.headers.get("X-Custom") == "modified"
|
||||
|
||||
|
||||
def test_response_with_depends_default():
|
||||
"""Response type hint should work with Response = Depends(...)."""
|
||||
app = FastAPI()
|
||||
|
||||
def modify_response(response: Response) -> Response:
|
||||
response.headers["X-Custom"] = "modified"
|
||||
return response
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(response: Response = Depends(modify_response)):
|
||||
return {"status": "ok"}
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"status": "ok"}
|
||||
assert resp.headers.get("X-Custom") == "modified"
|
||||
|
||||
|
||||
def test_response_without_depends():
|
||||
"""Regular Response injection should still work."""
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(response: Response):
|
||||
response.headers["X-Direct"] = "set"
|
||||
return {"status": "ok"}
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"status": "ok"}
|
||||
assert resp.headers.get("X-Direct") == "set"
|
||||
|
||||
|
||||
def test_response_dependency_chain():
|
||||
"""Response dependency should work in a chain of dependencies."""
|
||||
app = FastAPI()
|
||||
|
||||
def first_modifier(response: Response) -> Response:
|
||||
response.headers["X-First"] = "1"
|
||||
return response
|
||||
|
||||
def second_modifier(
|
||||
response: Annotated[Response, Depends(first_modifier)],
|
||||
) -> Response:
|
||||
response.headers["X-Second"] = "2"
|
||||
return response
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(response: Annotated[Response, Depends(second_modifier)]):
|
||||
return {"status": "ok"}
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.headers.get("X-First") == "1"
|
||||
assert resp.headers.get("X-Second") == "2"
|
||||
|
||||
|
||||
def test_response_dependency_returns_different_response_instance():
|
||||
"""Dependency that returns a different Response instance should work.
|
||||
|
||||
When a dependency returns a new Response object (e.g., JSONResponse) instead
|
||||
of modifying the injected one, the returned response should be used and any
|
||||
modifications to it in the endpoint should be preserved.
|
||||
"""
|
||||
app = FastAPI()
|
||||
|
||||
def default_response() -> Response:
|
||||
response = JSONResponse(content={"status": "ok"})
|
||||
response.headers["X-Custom"] = "initial"
|
||||
return response
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(response: Annotated[Response, Depends(default_response)]):
|
||||
response.headers["X-Custom"] = "modified"
|
||||
return response
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"status": "ok"}
|
||||
assert resp.headers.get("X-Custom") == "modified"
|
||||
|
||||
|
||||
# Tests for Request type hint with Depends
|
||||
def test_request_with_depends_annotated():
|
||||
"""Request type hint should work in dependency chain."""
|
||||
app = FastAPI()
|
||||
|
||||
def extract_request_info(request: Request) -> dict:
|
||||
return {
|
||||
"path": request.url.path,
|
||||
"user_agent": request.headers.get("user-agent", "unknown"),
|
||||
}
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(
|
||||
info: Annotated[dict, Depends(extract_request_info)],
|
||||
):
|
||||
return info
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/", headers={"user-agent": "test-agent"})
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"path": "/", "user_agent": "test-agent"}
|
||||
|
||||
|
||||
# Tests for BackgroundTasks type hint with Depends
|
||||
def test_background_tasks_with_depends_annotated():
|
||||
"""BackgroundTasks type hint should work with Annotated[BackgroundTasks, Depends(...)]."""
|
||||
app = FastAPI()
|
||||
task_results = []
|
||||
|
||||
def background_task(message: str):
|
||||
task_results.append(message)
|
||||
|
||||
def add_background_task(background_tasks: BackgroundTasks) -> BackgroundTasks:
|
||||
background_tasks.add_task(background_task, "from dependency")
|
||||
return background_tasks
|
||||
|
||||
@app.get("/")
|
||||
def endpoint(
|
||||
background_tasks: Annotated[BackgroundTasks, Depends(add_background_task)],
|
||||
):
|
||||
background_tasks.add_task(background_task, "from endpoint")
|
||||
return {"status": "ok"}
|
||||
|
||||
client = TestClient(app)
|
||||
resp = client.get("/")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert "from dependency" in task_results
|
||||
assert "from endpoint" in task_results
|
||||
2
uv.lock
generated
2
uv.lock
generated
@@ -1021,6 +1021,7 @@ dependencies = [
|
||||
{ name = "starlette", version = "0.49.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "starlette", version = "0.50.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -1202,6 +1203,7 @@ requires-dist = [
|
||||
{ name = "pyyaml", marker = "extra == 'all'", specifier = ">=5.3.1" },
|
||||
{ name = "starlette", specifier = ">=0.40.0,<0.51.0" },
|
||||
{ name = "typing-extensions", specifier = ">=4.8.0" },
|
||||
{ name = "typing-inspection", specifier = ">=0.4.2" },
|
||||
{ name = "ujson", marker = "extra == 'all'", specifier = ">=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'all'", specifier = ">=0.12.0" },
|
||||
{ name = "uvicorn", extras = ["standard"], marker = "extra == 'standard'", specifier = ">=0.12.0" },
|
||||
|
||||
Reference in New Issue
Block a user