diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py index 081b63a8b..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,7 +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 types import GenericAlias +except ImportError: # pragma: no cover + GenericAlias = None # type: ignore[misc,assignment] multipart_not_installed_error = ( 'Form data requires "python-multipart" to be installed. \n' @@ -356,6 +364,9 @@ def analyze_param( depends = None type_annotation: Any = Any use_annotation: Any = Any + if _is_typealiastype(annotation): + # 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 @@ -999,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/tests/test_dependency_pep695.py b/tests/test_dependency_pep695.py new file mode 100644 index 000000000..a60c1e5e6 --- /dev/null +++ b/tests/test_dependency_pep695.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient +from typing_extensions import Annotated, TypeAliasType + + +async def some_value() -> int: + return 123 + + +DependedValue = TypeAliasType( + "DependedValue", Annotated[int, Depends(some_value)], type_params=() +) + + +def test_pep695_type_dependencies(): + app = FastAPI() + + @app.get("/") + async def get_with_dep(value: DependedValue) -> str: # noqa + return f"value: {value}" + + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.text == '"value: 123"'