From 07aff37a5942b20256c89c7285572ec3ab14a3b9 Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 20:24:05 +0100 Subject: [PATCH 1/7] Add support for PEP695 TypeAliasType --- fastapi/dependencies/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 081b63a8b..a7341b990 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -73,6 +73,12 @@ from starlette.responses import Response from starlette.websockets import WebSocket from typing_extensions import Annotated, get_args, get_origin +try: + from typing import TypeAliasType +except ImportError: + TypeAliasType = None + + multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' 'You can install "python-multipart" with: \n\n' @@ -356,6 +362,9 @@ def analyze_param( depends = None type_annotation: Any = Any use_annotation: Any = Any + if TypeAliasType is not None and isinstance(annotation, TypeAliasType): + # unpack in case py3.12 type syntax is used + annotation = annotation.__value__ if annotation is not inspect.Signature.empty: use_annotation = annotation type_annotation = annotation From 5d7dbf8b1a3db2e0fb02c66b724017748449b3dc Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 21:12:20 +0100 Subject: [PATCH 2/7] Add tests for pepe695 compatibility --- tests/test_dependency_pep695.py | 28 ++++++++++++++++++++++++++++ tests/utils.py | 3 +++ 2 files changed, 31 insertions(+) create mode 100644 tests/test_dependency_pep695.py diff --git a/tests/test_dependency_pep695.py b/tests/test_dependency_pep695.py new file mode 100644 index 000000000..675c6a7d1 --- /dev/null +++ b/tests/test_dependency_pep695.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from typing import Annotated + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from .utils import needs_py312 + + +async def some_value() -> int: + return 123 + + +type DependedValue = Annotated[int, Depends(some_value)] + + +@needs_py312 +def test_pep695_type_dependencies(): + app = FastAPI() + + @app.get("/") + async def get_with_dep(value: DependedValue) -> str: + return f"value: {value}" + + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.text == '"value: 123"' diff --git a/tests/utils.py b/tests/utils.py index ae9543e3b..51d8fabfd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,6 +8,9 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) +needs_py312 = pytest.mark.skipif( + sys.version_info < (3, 12), reason="requires python3.12+" +) needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1") From 1dbb6048cab0070ffe89ed89caa5144ce50efd6e Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 21:38:44 +0100 Subject: [PATCH 3/7] Make pytest ignore py312 files for older py --- tests/conftest.py | 4 ++++ ...t_dependency_pep695.py => test_dependency_pep695_py312.py} | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/conftest.py rename tests/{test_dependency_pep695.py => test_dependency_pep695_py312.py} (89%) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..521e9f0f7 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,4 @@ +import sys + +if sys.version_info < (3, 12): + collect_ignore_glob = ["*_py312.py"] diff --git a/tests/test_dependency_pep695.py b/tests/test_dependency_pep695_py312.py similarity index 89% rename from tests/test_dependency_pep695.py rename to tests/test_dependency_pep695_py312.py index 675c6a7d1..378c9d3ee 100644 --- a/tests/test_dependency_pep695.py +++ b/tests/test_dependency_pep695_py312.py @@ -4,6 +4,7 @@ from typing import Annotated from fastapi import Depends, FastAPI from fastapi.testclient import TestClient + from .utils import needs_py312 @@ -19,7 +20,7 @@ def test_pep695_type_dependencies(): app = FastAPI() @app.get("/") - async def get_with_dep(value: DependedValue) -> str: + async def get_with_dep(value: DependedValue) -> str: # noqa return f"value: {value}" client = TestClient(app) From 198c4acce9252056542ca3beae148a4c47702161 Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 21:45:26 +0100 Subject: [PATCH 4/7] 'fix' mypy issue --- fastapi/dependencies/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index a7341b990..80a0f5826 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -74,9 +74,9 @@ from starlette.websockets import WebSocket from typing_extensions import Annotated, get_args, get_origin try: - from typing import TypeAliasType + from typing_extensions import TypeAliasType except ImportError: - TypeAliasType = None + TypeAliasType = None # type: ignore[misc,assignment] multipart_not_installed_error = ( From 6edc7e2b2c2a32e2e2da3d458574ebfd252c3dde Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 21:52:59 +0100 Subject: [PATCH 5/7] ignore py312 file for coverage: SyntaxError --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 7709451ff..28e7f6805 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,6 +199,7 @@ dynamic_context = "test_function" omit = [ "docs_src/response_model/tutorial003_04.py", "docs_src/response_model/tutorial003_04_py310.py", + "tests/test_dependency_pep695_py312.py" # syntax error for version < py312 ] [tool.coverage.report] From 118264409dd3eeeb2d2f9ec694616983d67aeb56 Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 22:01:49 +0100 Subject: [PATCH 6/7] Exclude faulty branch from coverage --- fastapi/dependencies/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 80a0f5826..3b978297c 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -75,7 +75,7 @@ from typing_extensions import Annotated, get_args, get_origin try: from typing_extensions import TypeAliasType -except ImportError: +except ImportError: # pragma: no cover TypeAliasType = None # type: ignore[misc,assignment] From 50b9bd5d8bc0071211d2917da0fb8bc3c940a7f2 Mon Sep 17 00:00:00 2001 From: Albin Skott Date: Mon, 28 Jul 2025 18:19:17 +0200 Subject: [PATCH 7/7] Vendor a copy of is_typealiastype from typing-inspection --- fastapi/dependencies/utils.py | 35 ++++++++++++++++--- pyproject.toml | 1 - tests/conftest.py | 4 --- ...695_py312.py => test_dependency_pep695.py} | 10 +++--- tests/utils.py | 3 -- 5 files changed, 34 insertions(+), 19 deletions(-) delete mode 100644 tests/conftest.py rename tests/{test_dependency_pep695_py312.py => test_dependency_pep695.py} (75%) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3b978297c..83147f15f 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -1,4 +1,6 @@ import inspect +import sys +import typing from contextlib import AsyncExitStack, contextmanager from copy import copy, deepcopy from dataclasses import dataclass @@ -19,6 +21,7 @@ from typing import ( ) import anyio +import typing_extensions from fastapi import params from fastapi._compat import ( PYDANTIC_V2, @@ -71,13 +74,12 @@ from starlette.datastructures import ( from starlette.requests import HTTPConnection, Request from starlette.responses import Response from starlette.websockets import WebSocket -from typing_extensions import Annotated, get_args, get_origin +from typing_extensions import Annotated, TypeAliasType, TypeGuard, get_args, get_origin try: - from typing_extensions import TypeAliasType + from types import GenericAlias except ImportError: # pragma: no cover - TypeAliasType = None # type: ignore[misc,assignment] - + GenericAlias = None # type: ignore[misc,assignment] multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -362,7 +364,7 @@ def analyze_param( depends = None type_annotation: Any = Any use_annotation: Any = Any - if TypeAliasType is not None and isinstance(annotation, TypeAliasType): + if _is_typealiastype(annotation): # unpack in case py3.12 type syntax is used annotation = annotation.__value__ if annotation is not inspect.Signature.empty: @@ -1008,3 +1010,26 @@ def get_body_field( field_info=BodyFieldInfo(**BodyFieldInfo_kwargs), ) return final_field + + +def _is_typealiastype(tp: Any, /) -> TypeGuard[TypeAliasType]: + in_typing = hasattr(typing, "TypeAliasType") + in_typing_extensions = hasattr(typing_extensions, "TypeAliasType") + is_typealiastype = False + if in_typing and in_typing_extensions: + if getattr(typing, "TypeAliasType", None) is getattr( + typing_extensions, "TypeAliasType", None + ): # pragma: no cover + is_typealiastype = isinstance(tp, typing.TypeAliasType) # type: ignore [attr-defined] + else: + is_typealiastype = isinstance( + tp, + (typing.TypeAliasType, typing_extensions.TypeAliasType), # type: ignore [attr-defined] + ) + elif in_typing and not in_typing_extensions: # pragma: no cover + is_typealiastype = isinstance(tp, typing.TypeAliasType) # type: ignore [attr-defined] + elif not in_typing and in_typing_extensions: + is_typealiastype = isinstance(tp, typing_extensions.TypeAliasType) + if sys.version_info[:2] == (3, 10): + return type(tp) is not GenericAlias and is_typealiastype + return is_typealiastype diff --git a/pyproject.toml b/pyproject.toml index 28e7f6805..7709451ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,7 +199,6 @@ dynamic_context = "test_function" omit = [ "docs_src/response_model/tutorial003_04.py", "docs_src/response_model/tutorial003_04_py310.py", - "tests/test_dependency_pep695_py312.py" # syntax error for version < py312 ] [tool.coverage.report] diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 521e9f0f7..000000000 --- a/tests/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -import sys - -if sys.version_info < (3, 12): - collect_ignore_glob = ["*_py312.py"] diff --git a/tests/test_dependency_pep695_py312.py b/tests/test_dependency_pep695.py similarity index 75% rename from tests/test_dependency_pep695_py312.py rename to tests/test_dependency_pep695.py index 378c9d3ee..a60c1e5e6 100644 --- a/tests/test_dependency_pep695_py312.py +++ b/tests/test_dependency_pep695.py @@ -1,21 +1,19 @@ from __future__ import annotations -from typing import Annotated - from fastapi import Depends, FastAPI from fastapi.testclient import TestClient - -from .utils import needs_py312 +from typing_extensions import Annotated, TypeAliasType async def some_value() -> int: return 123 -type DependedValue = Annotated[int, Depends(some_value)] +DependedValue = TypeAliasType( + "DependedValue", Annotated[int, Depends(some_value)], type_params=() +) -@needs_py312 def test_pep695_type_dependencies(): app = FastAPI() diff --git a/tests/utils.py b/tests/utils.py index 51d8fabfd..ae9543e3b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -8,9 +8,6 @@ needs_py39 = pytest.mark.skipif(sys.version_info < (3, 9), reason="requires pyth needs_py310 = pytest.mark.skipif( sys.version_info < (3, 10), reason="requires python3.10+" ) -needs_py312 = pytest.mark.skipif( - sys.version_info < (3, 12), reason="requires python3.12+" -) needs_pydanticv2 = pytest.mark.skipif(not PYDANTIC_V2, reason="requires Pydantic v2") needs_pydanticv1 = pytest.mark.skipif(PYDANTIC_V2, reason="requires Pydantic v1")