From 227cb85a03ec6522f641467fbf6af9bc5b1c2e75 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:35:43 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=85=20Fix=20parameterized=20tests=20w?= =?UTF-8?q?ith=20snapshots=20(#14875)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_path/test_required_str.py | 6 +-- ...est_tutorial001_tutorial002_tutorial003.py | 4 +- ...est_tutorial002_tutorial003_tutorial004.py | 4 +- .../test_tutorial003_tutorial004.py | 4 +- .../test_tutorial010.py | 39 +++++++++---------- 5 files changed, 28 insertions(+), 29 deletions(-) diff --git a/tests/test_request_params/test_path/test_required_str.py b/tests/test_request_params/test_path/test_required_str.py index 5add058c2..aecc2eb6c 100644 --- a/tests/test_request_params/test_path/test_required_str.py +++ b/tests/test_request_params/test_path/test_required_str.py @@ -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", } ] diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py index 78e5e53a2..18bce279b 100644 --- a/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py +++ b/tests/test_tutorial/test_body_nested_models/test_tutorial001_tutorial002_tutorial003.py @@ -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", diff --git a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py index f79d38b83..68e046ac5 100644 --- a/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_custom_response/test_tutorial002_tutorial003_tutorial004.py @@ -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", diff --git a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py index 8e86cd8a5..c13a3c38c 100644 --- a/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py +++ b/tests/test_tutorial/test_path_operation_configurations/test_tutorial003_tutorial004.py @@ -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": { diff --git a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py index 395f79518..efe9f1fa6 100644 --- a/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py +++ b/tests/test_tutorial/test_query_params_str_validations/test_tutorial010.py @@ -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", } From 0c0f6332e29f62001c94bf5c2d77ff1d79b73bf9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Feb 2026 15:36:09 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 238a4ff2a..fb404fcbd 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -11,6 +11,10 @@ hide: * 🌐 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 From ed2512a5ec33d9d70f7a9ef2e5f9fe52d3b77ab2 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:31:57 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B=20Fix=20`on=5Fstartup`=20and?= =?UTF-8?q?=20`on=5Fshutdown`=20parameters=20of=20`APIRouter`=20(#14873)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- fastapi/routing.py | 21 ++++++------- tests/test_router_events.py | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/fastapi/routing.py b/fastapi/routing.py index 0b4d28873..16a89ef3e 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -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 []) diff --git a/tests/test_router_events.py b/tests/test_router_events.py index 65f2f521c..a47d11913 100644 --- a/tests/test_router_events.py +++ b/tests/test_router_events.py @@ -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 From 0a4033aeeedcf8243502794648b25059e950e499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 9 Feb 2026 18:19:22 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=94=96=20Release=20version=200.128.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 2 ++ fastapi/__init__.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index fb404fcbd..9a1751d3a 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,8 @@ hide: ## Latest Changes +## 0.128.6 + ### Translations * 🌐 Update translations for zh (update-outdated). PR [#14843](https://github.com/fastapi/fastapi/pull/14843) by [@tiangolo](https://github.com/tiangolo). diff --git a/fastapi/__init__.py b/fastapi/__init__.py index 117685689..95e57e2eb 100644 --- a/fastapi/__init__.py +++ b/fastapi/__init__.py @@ -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 From 4e879799dd134e8e634d3e54c0973c0515a32d2e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Feb 2026 17:21:32 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 9a1751d3a..0abdfe6f2 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ hide: ## Latest Changes +### 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). + ## 0.128.6 ### Translations From fbca586c1df1940f07ff8051716ec5a9ba97b149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Mon, 9 Feb 2026 18:25:04 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/en/docs/release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index 0abdfe6f2..b151ff852 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,12 +7,12 @@ hide: ## Latest Changes +## 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). -## 0.128.6 - ### Translations * 🌐 Update translations for zh (update-outdated). PR [#14843](https://github.com/fastapi/fastapi/pull/14843) by [@tiangolo](https://github.com/tiangolo). From 8fd291465b1211f88958c86779b9759b1268fddd Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:38:48 +0100 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=94=A7=20Configure=20`test`=20workflo?= =?UTF-8?q?w=20to=20run=20tests=20with=20`inline-snapshot=3Dreview`=20(#14?= =?UTF-8?q?876)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e314b6a04..3fb837c04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,7 @@ on: env: UV_NO_SYNC: true + INLINE_SNAPSHOT_DEFAULT_FLAGS: review jobs: changes: From e94028ab60d18080810910352bc9129e8046c92d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 9 Feb 2026 17:39:11 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [skip ci] --- docs/en/docs/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/en/docs/release-notes.md b/docs/en/docs/release-notes.md index b151ff852..71dc7bb74 100644 --- a/docs/en/docs/release-notes.md +++ b/docs/en/docs/release-notes.md @@ -7,6 +7,10 @@ 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