Browse Source

Preserve Annotated dependency metadata for stringified refs

pull/15300/head
itsviseph 3 months ago
parent
commit
aab5905a07
  1. 16
      fastapi/dependencies/utils.py
  2. 79
      tests/test_stringified_annotations_forwardrefs.py

16
fastapi/dependencies/utils.py

@ -81,6 +81,13 @@ multipart_not_installed_error = (
'You can install "python-multipart" with: \n\n' 'You can install "python-multipart" with: \n\n'
"pip install python-multipart\n" "pip install python-multipart\n"
) )
class _ForwardRefNamespace(dict[str, Any]):
def __missing__(self, key: str) -> Any:
value = ForwardRef(key)
self[key] = value
return value
multipart_incorrect_install_error = ( multipart_incorrect_install_error = (
'Form data requires "python-multipart" to be installed. ' 'Form data requires "python-multipart" to be installed. '
'It seems you installed "multipart" instead. \n' 'It seems you installed "multipart" instead. \n'
@ -244,8 +251,13 @@ def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any: def get_typed_annotation(annotation: Any, globalns: dict[str, Any]) -> Any:
if isinstance(annotation, str): if isinstance(annotation, str):
annotation = ForwardRef(annotation) try:
annotation = evaluate_forwardref(annotation, globalns, globalns) # ty: ignore[deprecated] annotation = eval(annotation, globalns, globalns)
except NameError:
# Preserve the outer typing structure (e.g. Annotated[..., Depends(...)])
# even when some inner symbols are not defined yet.
forwardref_globalns = _ForwardRefNamespace(globalns)
annotation = eval(annotation, forwardref_globalns, forwardref_globalns)
if annotation is type(None): if annotation is type(None):
return None return None
return annotation return annotation

79
tests/test_stringified_annotations_forwardrefs.py

@ -0,0 +1,79 @@
from __future__ import annotations
from fastapi.testclient import TestClient
from .utils import needs_py310
def _build_app(source: str):
namespace: dict[str, object] = {}
exec(source, namespace, namespace)
return namespace["app"]
@needs_py310
def test_late_defined_annotated_dependency_forwardref_is_not_treated_as_query_param():
app = _build_app("""
from __future__ import annotations
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
def get_potato() -> Potato:
return Potato(color="red", size=10)
@app.get("/")
async def read_root(potato: Annotated[Potato, Depends(get_potato)]):
return {"color": potato.color, "size": potato.size}
@dataclass
class Potato:
color: str
size: int
""")
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"color": "red", "size": 10}
operation = client.get("/openapi.json").json()["paths"]["/"]["get"]
assert "parameters" not in operation
@needs_py310
def test_stringified_annotated_dependency_with_defined_type_remains_a_dependency():
app = _build_app("""
from __future__ import annotations
from dataclasses import dataclass
from typing import Annotated
from fastapi import Depends, FastAPI
app = FastAPI()
@dataclass
class Potato:
color: str
size: int
def get_potato() -> Potato:
return Potato(color="gold", size=7)
@app.get("/")
async def read_root(potato: Annotated["Potato", Depends(get_potato)]):
return {"color": potato.color, "size": potato.size}
""")
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200, response.text
assert response.json() == {"color": "gold", "size": 7}
operation = client.get("/openapi.json").json()["paths"]["/"]["get"]
assert "parameters" not in operation
Loading…
Cancel
Save