Skip to content

Commit 8ba504d

Browse files
committed
Use typing-inspection to inspect annotation with PEP695 support
1 parent 1182644 commit 8ba504d

File tree

2 files changed

+31
-35
lines changed

2 files changed

+31
-35
lines changed

fastapi/dependencies/utils.py

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,8 @@
7171
from starlette.requests import HTTPConnection, Request
7272
from starlette.responses import Response
7373
from starlette.websockets import WebSocket
74-
from typing_extensions import Annotated, get_args, get_origin
75-
76-
try:
77-
from typing_extensions import TypeAliasType
78-
except ImportError: # pragma: no cover
79-
TypeAliasType = None # type: ignore[misc,assignment]
80-
74+
from typing_extensions import get_args, get_origin
75+
from typing_inspection.introspection import AnnotationSource, inspect_annotation
8176

8277
multipart_not_installed_error = (
8378
'Form data requires "python-multipart" to be installed. \n'
@@ -360,22 +355,22 @@ def analyze_param(
360355
) -> ParamDetails:
361356
field_info = None
362357
depends = None
363-
type_annotation: Any = Any
364-
use_annotation: Any = Any
365-
if TypeAliasType is not None and isinstance(annotation, TypeAliasType):
366-
# unpack in case py3.12 type syntax is used
367-
annotation = annotation.__value__
358+
type: Any = Any
359+
meta: list[Any] = []
368360
if annotation is not inspect.Signature.empty:
369-
use_annotation = annotation
370-
type_annotation = annotation
361+
inspected = inspect_annotation(
362+
annotation,
363+
annotation_source=AnnotationSource.BARE,
364+
unpack_type_aliases="lenient",
365+
)
366+
meta = inspected.metadata
367+
type = inspected.type
368+
else:
369+
annotation = Any
371370
# Extract Annotated info
372-
if get_origin(use_annotation) is Annotated:
373-
annotated_args = get_args(annotation)
374-
type_annotation = annotated_args[0]
371+
if meta:
375372
fastapi_annotations = [
376-
arg
377-
for arg in annotated_args[1:]
378-
if isinstance(arg, (FieldInfo, params.Depends))
373+
arg for arg in meta if isinstance(arg, (FieldInfo, params.Depends))
379374
]
380375
fastapi_specific_annotations = [
381376
arg
@@ -392,7 +387,7 @@ def analyze_param(
392387
if isinstance(fastapi_annotation, FieldInfo):
393388
# Copy `field_info` because we mutate `field_info.default` below.
394389
field_info = copy_field_info(
395-
field_info=fastapi_annotation, annotation=use_annotation
390+
field_info=fastapi_annotation, annotation=annotation
396391
)
397392
assert (
398393
field_info.default is Undefined or field_info.default is RequiredParam
@@ -427,17 +422,17 @@ def analyze_param(
427422
)
428423
field_info = value
429424
if PYDANTIC_V2:
430-
field_info.annotation = type_annotation
425+
field_info.annotation = type
431426

432427
# Get Depends from type annotation
433428
if depends is not None and depends.dependency is None:
434429
# Copy `depends` before mutating it
435430
depends = copy(depends)
436-
depends.dependency = type_annotation
431+
depends.dependency = type
437432

438433
# Handle non-param type annotations like Request
439434
if lenient_issubclass(
440-
type_annotation,
435+
type,
441436
(
442437
Request,
443438
WebSocket,
@@ -447,9 +442,9 @@ def analyze_param(
447442
SecurityScopes,
448443
),
449444
):
450-
assert depends is None, f"Cannot specify `Depends` for type {type_annotation!r}"
445+
assert depends is None, f"Cannot specify `Depends` for type {type!r}"
451446
assert field_info is None, (
452-
f"Cannot specify FastAPI annotation for type {type_annotation!r}"
447+
f"Cannot specify FastAPI annotation for type {type!r}"
453448
)
454449
# Handle default assignations, neither field_info nor depends was not found in Annotated nor default value
455450
elif field_info is None and depends is None:
@@ -458,15 +453,15 @@ def analyze_param(
458453
# We might check here that `default_value is RequiredParam`, but the fact is that the same
459454
# parameter might sometimes be a path parameter and sometimes not. See
460455
# `tests/test_infer_param_optionality.py` for an example.
461-
field_info = params.Path(annotation=use_annotation)
456+
field_info = params.Path(annotation=annotation)
462457
elif is_uploadfile_or_nonable_uploadfile_annotation(
463-
type_annotation
464-
) or is_uploadfile_sequence_annotation(type_annotation):
465-
field_info = params.File(annotation=use_annotation, default=default_value)
466-
elif not field_annotation_is_scalar(annotation=type_annotation):
467-
field_info = params.Body(annotation=use_annotation, default=default_value)
458+
type
459+
) or is_uploadfile_sequence_annotation(type):
460+
field_info = params.File(annotation=annotation, default=default_value)
461+
elif not field_annotation_is_scalar(annotation=type):
462+
field_info = params.Body(annotation=annotation, default=default_value)
468463
else:
469-
field_info = params.Query(annotation=use_annotation, default=default_value)
464+
field_info = params.Query(annotation=annotation, default=default_value)
470465

471466
field = None
472467
# It's a field_info, not a dependency
@@ -483,7 +478,7 @@ def analyze_param(
483478
):
484479
field_info.in_ = params.ParamTypes.query
485480
use_annotation_from_field_info = get_annotation_from_field_info(
486-
use_annotation,
481+
annotation,
487482
field_info,
488483
param_name,
489484
)
@@ -517,7 +512,7 @@ def analyze_param(
517512
)
518513
)
519514

520-
return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)
515+
return ParamDetails(type_annotation=type, depends=depends, field=field)
521516

522517

523518
def add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ dependencies = [
4646
"starlette>=0.40.0,<0.48.0",
4747
"pydantic>=1.7.4,!=1.8,!=1.8.1,!=2.0.0,!=2.0.1,!=2.1.0,<3.0.0",
4848
"typing-extensions>=4.8.0",
49+
"typing-inspection>=0.4.1",
4950
]
5051

5152
[project.urls]

0 commit comments

Comments
 (0)