From 07aff37a5942b20256c89c7285572ec3ab14a3b9 Mon Sep 17 00:00:00 2001 From: lokidev Date: Tue, 13 Feb 2024 20:24:05 +0100 Subject: [PATCH 1/9] 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/9] 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/9] 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/9] '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/9] 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/9] 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/9] 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") From 3a1fbda8f49ae65d35c8630cf9551c685850c4b3 Mon Sep 17 00:00:00 2001 From: Motov Yurii <109919500+YuriiMotov@users.noreply.github.com> Date: Wed, 29 Oct 2025 07:58:16 +0100 Subject: [PATCH 8/9] Apply suggestions from code review (add comments) --- fastapi/dependencies/utils.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 3de9bccc0..bc61f7370 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -372,7 +372,7 @@ def analyze_param( type_annotation: Any = Any use_annotation: Any = Any if _is_typealiastype(annotation): - # unpack in case py3.12 type syntax is used + # unpack in case PEP 695 type syntax is used annotation = annotation.__value__ if annotation is not inspect.Signature.empty: use_annotation = annotation @@ -1023,23 +1023,42 @@ def get_body_field( def _is_typealiastype(tp: Any, /) -> TypeGuard[TypeAliasType]: + """ + This implementation is in line with the implementation of `is_typealiastype` in `typing-inspection`: + See: + - https://github.com/pydantic/typing-inspection/blob/8db011350942f33ac4b5d7db60d4d9ea83ab480f/src/typing_inspection/typing_objects.py#L488-L499 + - https://github.com/pydantic/typing-inspection/blob/8db011350942f33ac4b5d7db60d4d9ea83ab480f/src/typing_inspection/typing_objects.py#L105-L134 + """ + + # Check if TypeAliasType exists in typing and/or typing_extensions. in_typing = hasattr(typing, "TypeAliasType") in_typing_extensions = hasattr(typing_extensions, "TypeAliasType") - is_typealiastype = False + + is_typealiastype = False # Default: assume not a TypeAliasType if in_typing and in_typing_extensions: if getattr(typing, "TypeAliasType", None) is getattr( typing_extensions, "TypeAliasType", None ): # pragma: no cover + # Case 1: Both implementations are the same object. + # Checking against one of them. is_typealiastype = isinstance(tp, typing.TypeAliasType) # type: ignore [attr-defined] else: + # Case 2: Implementations are different objects. + # Need to check against both versions. is_typealiastype = isinstance( tp, (typing.TypeAliasType, typing_extensions.TypeAliasType), # type: ignore [attr-defined] ) elif in_typing and not in_typing_extensions: # pragma: no cover + # Case 3: Only typing.TypeAliasType exists. is_typealiastype = isinstance(tp, typing.TypeAliasType) # type: ignore [attr-defined] elif not in_typing and in_typing_extensions: + # Case 4: Only typing_extensions.TypeAliasType exists. is_typealiastype = isinstance(tp, typing_extensions.TypeAliasType) + + # Special case for Python 3.10: + # On Python 3.10, parameterized PEP 695 type aliases are represented as `GenericAlias` + # instead of proper TypeAliasType. We must exclude those. if sys.version_info[:2] == (3, 10): return type(tp) is not GenericAlias and is_typealiastype return is_typealiastype From eb544d5095386221c4a10e33fbc149dad06e8b78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 22:24:43 +0000 Subject: [PATCH 9/9] =?UTF-8?q?=F0=9F=8E=A8=20[pre-commit.ci]=20Auto=20for?= =?UTF-8?q?mat=20from=20pre-commit.com=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/dependencies/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 761c4f30c..7166227f1 100644 --- a/fastapi/dependencies/utils.py +++ b/fastapi/dependencies/utils.py @@ -79,7 +79,14 @@ 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, Literal, TypeAliasType, TypeGuard, get_args, get_origin +from typing_extensions import ( + Annotated, + Literal, + TypeAliasType, + TypeGuard, + get_args, + get_origin, +) try: from types import GenericAlias