Compare commits

...
9 Commits
Author SHA1 Message Date
Sebastián Ramírezandgithub-actions[bot] 0f3d3b2f9f 🔖 Release version 0.140.10 (#16093)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-28 16:19:13 +02:00
github-actions[bot] 584efa0981 📝 Update release notes
[skip ci]
2026-07-28 14:04:58 +00:00
Yurii MotovandSebastián Ramírez 65e42bd5ec 🐛 Fix handling sequences with nested Annotated types (#14874)
Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
2026-07-28 16:04:16 +02:00
github-actions[bot] 9db320278c 📝 Update release notes
[skip ci]
2026-07-28 13:48:40 +00:00
Sebastián Ramírez d3cd6054e4 🐛 Accept any base test failure as regression (#16092) 2026-07-28 13:47:58 +00:00
github-actions[bot] 19a461a19e 📝 Update release notes
[skip ci]
2026-07-28 13:31:47 +00:00
Sebastián Ramírez 0a4cd1c78f 🐛 Preserve pytest exit code in regression check (#16091) 2026-07-28 15:31:02 +02:00
github-actions[bot] 64ae6c977c 📝 Update release notes
[skip ci]
2026-07-28 13:16:59 +00:00
Sebastián Ramírez 7d123d9537 Test PR regressions against base code (#16090) 2026-07-28 13:16:17 +00:00
5 changed files with 237 additions and 1 deletions

No files matched your search

+73
View File
@@ -201,6 +201,78 @@ jobs:
mode: memory
run: uv run --no-sync pytest tests/memory_benchmarks --codspeed
regression-test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Check out the pull request
if: github.event_name == 'pull_request'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.sha }}
path: pr
persist-credentials: false
fetch-depth: 0
- name: Find changed tests
if: github.event_name == 'pull_request'
id: changed-tests
working-directory: pr
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
git diff --name-only --diff-filter=AM -z "$BASE_SHA" "$HEAD_SHA" -- tests \
| while IFS= read -r -d '' file; do
case "$(basename "$file")" in
test_*.py) printf '%s\0' "$file" ;;
esac
done > "$RUNNER_TEMP/changed-tests"
if [ -s "$RUNNER_TEMP/changed-tests" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
git diff --binary "$BASE_SHA" "$HEAD_SHA" -- tests \
> "$RUNNER_TEMP/tests.patch"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No added or modified test files; regression proof is not applicable."
fi
- name: Check out the base revision
if: steps.changed-tests.outputs.found == 'true'
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
persist-credentials: false
- name: Set up Python
if: steps.changed-tests.outputs.found == 'true'
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version-file: "base/.python-version"
- name: Setup uv
if: steps.changed-tests.outputs.found == 'true'
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
with:
# Before upgrading uv version, make sure astral-sh/setup-uv knows its checksum.
# See: https://github.com/astral-sh/setup-uv/issues/851#issuecomment-4282017837
version: "0.11.18"
enable-cache: true
- name: Run the changed tests against the base code
if: steps.changed-tests.outputs.found == 'true'
working-directory: base
run: |
git apply "$RUNNER_TEMP/tests.patch"
uv sync --locked --no-dev --group tests --extra all
set +e
xargs -0 uv run --no-sync pytest -- < "$RUNNER_TEMP/changed-tests"
status=$?
set -e
if [ "$status" -eq 0 ]; then
echo "::warning::The changed tests already pass on the base revision. Check whether the fix is still needed."
echo "### Regression proof: base already passes :warning:" >> "$GITHUB_STEP_SUMMARY"
echo "The changed tests pass without the pull request's code changes." >> "$GITHUB_STEP_SUMMARY"
else
echo "The changed tests fail on the base revision as expected (pytest exit code $status)."
echo "### Regression proof: base fails as expected :white_check_mark:" >> "$GITHUB_STEP_SUMMARY"
fi
coverage-combine:
needs:
- test
@@ -253,6 +325,7 @@ jobs:
- test
- coverage-combine
- benchmark
- regression-test
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
+12
View File
@@ -7,6 +7,18 @@ hide:
## Latest Changes
## 0.140.10 (2026-07-28)
### Fixes
* 🐛 Fix handling sequences with nested Annotated types. PR [#14874](https://github.com/fastapi/fastapi/pull/14874) by [@YuriiMotov](https://github.com/YuriiMotov).
### Internal
* 🐛 Accept any base test failure as regression. PR [#16092](https://github.com/fastapi/fastapi/pull/16092) by [@tiangolo](https://github.com/tiangolo).
* 🐛 Preserve pytest exit code in regression check. PR [#16091](https://github.com/fastapi/fastapi/pull/16091) by [@tiangolo](https://github.com/tiangolo).
* ✅ Test PR regressions against base code. PR [#16090](https://github.com/fastapi/fastapi/pull/16090) by [@tiangolo](https://github.com/tiangolo).
## 0.140.9 (2026-07-28)
### Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.140.9"
__version__ = "0.140.10"
from starlette import status as status
+8
View File
@@ -63,6 +63,10 @@ def _annotation_is_sequence(annotation: type[Any] | None) -> bool:
def field_annotation_is_sequence(annotation: type[Any] | None) -> bool:
origin = get_origin(annotation)
if origin is Annotated:
return field_annotation_is_sequence(get_args(annotation)[0])
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if field_annotation_is_sequence(arg):
@@ -108,6 +112,10 @@ def field_annotation_is_scalar(annotation: Any) -> bool:
def field_annotation_is_scalar_sequence(annotation: type[Any] | None) -> bool:
origin = get_origin(annotation)
if origin is Annotated:
return field_annotation_is_scalar_sequence(get_args(annotation)[0])
if origin is Union or origin is UnionType:
at_least_one_scalar_sequence = False
for arg in get_args(annotation):
+143
View File
@@ -0,0 +1,143 @@
from typing import Annotated
from dirty_equals import IsList
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from pydantic import Field
MaxSizedSet = Annotated[set[str], Field(max_length=3)]
app = FastAPI()
@app.get("/")
def read_root(foo: Annotated[MaxSizedSet | None, Query()] = None):
return {"foo": foo}
client = TestClient(app)
def test_endpoint_none():
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"foo": None}
def test_endpoint_valid():
response = client.get("/", params={"foo": ["a", "b"]})
assert response.status_code == 200
assert response.json() == {"foo": IsList("a", "b", check_order=False)}
def test_endpoint_too_long():
response = client.get("/", params={"foo": ["a", "b", "c", "d"]})
assert response.status_code == 422
assert response.json() == snapshot(
{
"detail": [
{
"type": "too_long",
"loc": ["query", "foo"],
"msg": "Set should have at most 3 items after validation, not more",
"input": IsList("a", "b", "c", "d", check_order=False),
"ctx": {
"actual_length": None,
"field_type": "Set",
"max_length": 3,
},
}
]
}
)
def test_openapi():
assert app.openapi() == snapshot(
{
"components": {
"schemas": {
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"title": "Detail",
"type": "array",
},
},
"title": "HTTPValidationError",
"type": "object",
},
"ValidationError": {
"properties": {
"ctx": {"title": "Context", "type": "object"},
"input": {"title": "Input"},
"loc": {
"items": {
"anyOf": [{"type": "string"}, {"type": "integer"}],
},
"title": "Location",
"type": "array",
},
"msg": {"title": "Message", "type": "string"},
"type": {"title": "Error Type", "type": "string"},
},
"required": ["loc", "msg", "type"],
"title": "ValidationError",
"type": "object",
},
},
},
"info": {
"title": "FastAPI",
"version": "0.1.0",
},
"openapi": "3.1.0",
"paths": {
"/": {
"get": {
"operationId": "read_root__get",
"parameters": [
{
"in": "query",
"name": "foo",
"required": False,
"schema": {
"anyOf": [
{
"items": {"type": "string"},
"maxItems": 3,
"type": "array",
"uniqueItems": True,
},
{"type": "null"},
],
"title": "Foo",
},
},
],
"responses": {
"200": {
"content": {"application/json": {"schema": {}}},
"description": "Successful Response",
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError",
},
},
},
"description": "Validation Error",
},
},
"summary": "Read Root",
},
},
},
}
)