Compare commits

...

8 Commits

Author SHA1 Message Date
github-actions[bot]
e94028ab60 📝 Update release notes
[skip ci]
2026-02-09 17:39:11 +00:00
Motov Yurii
8fd291465b 🔧 Configure test workflow to run tests with inline-snapshot=review (#14876) 2026-02-09 18:38:48 +01:00
Sebastián Ramírez
fbca586c1d 📝 Update release notes 2026-02-09 18:25:04 +01:00
github-actions[bot]
4e879799dd 📝 Update release notes
[skip ci]
2026-02-09 17:21:32 +00:00
Sebastián Ramírez
0a4033aeee 🔖 Release version 0.128.6 2026-02-09 18:19:22 +01:00
Motov Yurii
ed2512a5ec 🐛 Fix on_startup and on_shutdown parameters of APIRouter (#14873)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-02-09 17:31:57 +01:00
github-actions[bot]
0c0f6332e2 📝 Update release notes
[skip ci]
2026-02-09 15:36:09 +00:00
Motov Yurii
227cb85a03 Fix parameterized tests with snapshots (#14875) 2026-02-09 16:35:43 +01:00
10 changed files with 115 additions and 40 deletions

View File

@@ -14,6 +14,7 @@ on:
env:
UV_NO_SYNC: true
INLINE_SNAPSHOT_DEFAULT_FLAGS: review
jobs:
changes:

View File

@@ -7,10 +7,24 @@ hide:
## Latest Changes
### Internal
* 🔧 Configure `test` workflow to run tests with `inline-snapshot=review`. PR [#14876](https://github.com/fastapi/fastapi/pull/14876) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.128.6
### Fixes
* 🐛 Fix `on_startup` and `on_shutdown` parameters of `APIRouter`. PR [#14873](https://github.com/fastapi/fastapi/pull/14873) by [@YuriiMotov](https://github.com/YuriiMotov).
### Translations
* 🌐 Update translations for zh (update-outdated). PR [#14843](https://github.com/fastapi/fastapi/pull/14843) by [@tiangolo](https://github.com/tiangolo).
### Internal
* ✅ Fix parameterized tests with snapshots. PR [#14875](https://github.com/fastapi/fastapi/pull/14875) by [@YuriiMotov](https://github.com/YuriiMotov).
## 0.128.5
### Refactors

View File

@@ -1,6 +1,6 @@
"""FastAPI framework, high performance, easy to learn, fast to code, ready for production"""
__version__ = "0.128.5"
__version__ = "0.128.6"
from starlette import status as status

View File

@@ -952,16 +952,6 @@ 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
@@ -985,6 +975,17 @@ class APIRouter(routing.Router):
assert not prefix.endswith("/"), (
"A path prefix must not end with '/', as the routes will start with '/'"
)
# 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)
)
self.prefix = prefix
self.tags: list[Union[str, Enum]] = tags or []
self.dependencies = list(dependencies or [])

View File

@@ -3,7 +3,7 @@ from typing import Annotated
import pytest
from fastapi import FastAPI, Path
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from inline_snapshot import Is, snapshot
app = FastAPI()
@@ -58,8 +58,8 @@ def test_schema(path: str, expected_name: str, expected_title: str):
[
{
"required": True,
"schema": {"title": expected_title, "type": "string"},
"name": expected_name,
"schema": {"title": Is(expected_title), "type": "string"},
"name": Is(expected_name),
"in": "path",
}
]

View File

@@ -317,3 +317,63 @@ def test_router_async_generator_lifespan(state: State) -> None:
assert response.json() == {"message": "Hello World"}
assert state.app_startup is True
assert state.app_shutdown is True
def test_startup_shutdown_handlers_as_parameters(state: State) -> None:
"""Test that startup/shutdown handlers passed as parameters to FastAPI are called correctly."""
def app_startup() -> None:
state.app_startup = True
def app_shutdown() -> None:
state.app_shutdown = True
app = FastAPI(on_startup=[app_startup], on_shutdown=[app_shutdown])
@app.get("/")
def main() -> dict[str, str]:
return {"message": "Hello World"}
def router_startup() -> None:
state.router_startup = True
def router_shutdown() -> None:
state.router_shutdown = True
router = APIRouter(on_startup=[router_startup], on_shutdown=[router_shutdown])
def sub_router_startup() -> None:
state.sub_router_startup = True
def sub_router_shutdown() -> None:
state.sub_router_shutdown = True
sub_router = APIRouter(
on_startup=[sub_router_startup], on_shutdown=[sub_router_shutdown]
)
router.include_router(sub_router)
app.include_router(router)
assert state.app_startup is False
assert state.router_startup is False
assert state.sub_router_startup is False
assert state.app_shutdown is False
assert state.router_shutdown is False
assert state.sub_router_shutdown is False
with TestClient(app) as client:
assert state.app_startup is True
assert state.router_startup is True
assert state.sub_router_startup is True
assert state.app_shutdown is False
assert state.router_shutdown is False
assert state.sub_router_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.router_startup is True
assert state.sub_router_startup is True
assert state.app_shutdown is True
assert state.router_shutdown is True
assert state.sub_router_shutdown is True

View File

@@ -3,7 +3,7 @@ import importlib
import pytest
from dirty_equals import IsList
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from inline_snapshot import Is, snapshot
from ...utils import needs_py310
@@ -212,7 +212,7 @@ def test_openapi_schema(client: TestClient, mod_name: str):
"title": "Tax",
"anyOf": [{"type": "number"}, {"type": "null"}],
},
"tags": tags_schema,
"tags": Is(tags_schema),
},
"required": [
"name",

View File

@@ -2,7 +2,7 @@ import importlib
import pytest
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from inline_snapshot import Is, snapshot
@pytest.fixture(
@@ -59,7 +59,7 @@ def test_openapi_schema(client: TestClient, mod_name: str):
"responses": {
"200": {
"description": "Successful Response",
"content": response_content,
"content": Is(response_content),
}
},
"summary": "Read Items",

View File

@@ -4,7 +4,7 @@ from textwrap import dedent
import pytest
from dirty_equals import IsList
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from inline_snapshot import Is, snapshot
from ...utils import needs_py310
@@ -75,7 +75,7 @@ def test_openapi_schema(client: TestClient, mod_name: str):
"/items/": {
"post": {
"summary": "Create an item",
"description": DESCRIPTIONS[mod_name],
"description": Is(DESCRIPTIONS[mod_name]),
"operationId": "create_item_items__post",
"requestBody": {
"content": {

View File

@@ -3,7 +3,7 @@ import importlib
import pytest
from fastapi._compat import PYDANTIC_VERSION_MINOR_TUPLE
from fastapi.testclient import TestClient
from inline_snapshot import snapshot
from inline_snapshot import Is, snapshot
from ...utils import needs_py310
@@ -66,6 +66,23 @@ def test_query_params_str_validations_item_query_nonregexquery(client: TestClien
def test_openapi_schema(client: TestClient):
response = client.get("/openapi.json")
assert response.status_code == 200, response.text
parameters_schema = {
"anyOf": [
{
"type": "string",
"minLength": 3,
"maxLength": 50,
"pattern": "^fixedquery$",
},
{"type": "null"},
],
"title": "Query string",
"description": "Query string for the items to search in the database that have a good match",
# See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34.
**({"deprecated": True} if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10) else {}),
}
assert response.json() == snapshot(
{
"openapi": "3.1.0",
@@ -96,25 +113,7 @@ def test_openapi_schema(client: TestClient):
"description": "Query string for the items to search in the database that have a good match",
"required": False,
"deprecated": True,
"schema": {
"anyOf": [
{
"type": "string",
"minLength": 3,
"maxLength": 50,
"pattern": "^fixedquery$",
},
{"type": "null"},
],
"title": "Query string",
"description": "Query string for the items to search in the database that have a good match",
# See https://github.com/pydantic/pydantic/blob/80353c29a824c55dea4667b328ba8f329879ac9f/tests/test_fastapi.sh#L25-L34.
**(
{"deprecated": True}
if PYDANTIC_VERSION_MINOR_TUPLE >= (2, 10)
else {}
),
},
"schema": Is(parameters_schema),
"name": "item-query",
"in": "query",
}