14 changed files with 245 additions and 4 deletions
@ -0,0 +1,23 @@ |
|||
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=5) |
|||
|
|||
|
|||
@app.get("/") |
|||
async def read_root(potato: Annotated[Potato, Depends(get_potato)]): |
|||
return {"Hello": "World"} |
|||
|
|||
|
|||
@dataclass |
|||
class Potato: |
|||
color: str |
|||
size: int |
|||
@ -0,0 +1,19 @@ |
|||
from __future__ import annotations |
|||
|
|||
from typing import TYPE_CHECKING |
|||
|
|||
from fastapi import Depends, FastAPI |
|||
|
|||
if TYPE_CHECKING: |
|||
from sqlalchemy.orm import Session # pragma: no cover |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
def get_db() -> Session: |
|||
return "fake_db" # type: ignore[return-value] |
|||
|
|||
|
|||
@app.get("/") |
|||
def read_root(db=Depends(get_db)) -> dict: |
|||
return {"db": str(db)} |
|||
@ -0,0 +1,17 @@ |
|||
from __future__ import annotations |
|||
|
|||
from typing import TYPE_CHECKING |
|||
|
|||
from fastapi import Depends, FastAPI |
|||
|
|||
if TYPE_CHECKING: |
|||
from pydantic import BaseModel # pragma: no cover |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
def get_thing(item: BaseModel) -> None: ... |
|||
|
|||
|
|||
@app.get("/") |
|||
def read_root(thing=Depends(get_thing)) -> dict: ... |
|||
@ -0,0 +1,14 @@ |
|||
from __future__ import annotations |
|||
|
|||
from typing import TYPE_CHECKING |
|||
|
|||
from fastapi import FastAPI |
|||
|
|||
if TYPE_CHECKING: |
|||
from pydantic import BaseModel # pragma: no cover |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/") |
|||
def read_root(item: BaseModel) -> dict: ... |
|||
@ -0,0 +1,14 @@ |
|||
from __future__ import annotations |
|||
|
|||
from typing import TYPE_CHECKING |
|||
|
|||
from fastapi import FastAPI |
|||
|
|||
if TYPE_CHECKING: |
|||
from pydantic import BaseModel # pragma: no cover |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/") |
|||
def read_root() -> BaseModel: ... |
|||
@ -0,0 +1,17 @@ |
|||
from __future__ import annotations |
|||
|
|||
from dataclasses import dataclass |
|||
|
|||
from fastapi import FastAPI |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/") |
|||
def read_root(potato: Potato) -> Potato: ... |
|||
|
|||
|
|||
@dataclass |
|||
class Potato: |
|||
color: str |
|||
size: int |
|||
@ -0,0 +1,15 @@ |
|||
from __future__ import annotations |
|||
|
|||
from fastapi import FastAPI |
|||
from pydantic import BaseModel |
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
class Item(BaseModel): |
|||
name: str |
|||
|
|||
|
|||
@app.post("/") |
|||
def create_item(item: Item) -> Item: |
|||
return item |
|||
@ -0,0 +1,74 @@ |
|||
"""Tests that unresolvable annotations raise clear errors at route registration time. |
|||
|
|||
Covers two common user mistakes: |
|||
1. Using a type imported only under `if TYPE_CHECKING:` in endpoint/dependency params. |
|||
2. Using a class that is defined after the route that references it. |
|||
""" |
|||
|
|||
import importlib |
|||
|
|||
import pytest |
|||
from fastapi.testclient import TestClient |
|||
|
|||
APP_PREFIX = "tests.test_unresolvable_annotations.apps" |
|||
|
|||
|
|||
def test_type_declared_after_endpoint(): |
|||
"""A class used as an endpoint param before it's defined should fail.""" |
|||
with pytest.raises(NameError, match="Could not resolve annotation 'Potato'"): |
|||
importlib.import_module(f"{APP_PREFIX}.type_declared_after_endpoint") |
|||
|
|||
|
|||
def test_annotated_depends_late_type(): |
|||
"""Annotated[Potato, Depends(get_potato)] with Potato defined after the route should fail.""" |
|||
with pytest.raises(NameError, match="Could not resolve annotation"): |
|||
importlib.import_module(f"{APP_PREFIX}.annotated_depends_late_type") |
|||
|
|||
|
|||
def test_type_checking_import_in_endpoint_param(): |
|||
"""TYPE_CHECKING-only import used as an endpoint parameter type should fail.""" |
|||
with pytest.raises(NameError, match="Could not resolve annotation 'BaseModel'"): |
|||
importlib.import_module(f"{APP_PREFIX}.type_checking_in_endpoint_param") |
|||
|
|||
|
|||
def test_type_checking_import_in_dependency_param(): |
|||
"""TYPE_CHECKING-only import used as a dependency parameter type should fail.""" |
|||
with pytest.raises(NameError, match="Could not resolve annotation 'BaseModel'"): |
|||
importlib.import_module(f"{APP_PREFIX}.type_checking_in_dep_param") |
|||
|
|||
|
|||
def test_type_checking_import_in_return_type(): |
|||
"""TYPE_CHECKING-only import used as an endpoint return type should fail.""" |
|||
with pytest.raises(NameError, match="Could not resolve annotation 'BaseModel'"): |
|||
importlib.import_module(f"{APP_PREFIX}.type_checking_in_return_type") |
|||
|
|||
|
|||
def test_type_checking_dep_return_type_is_safe(): |
|||
"""TYPE_CHECKING-only import used ONLY as a dependency return type is safe. |
|||
|
|||
FastAPI doesn't inspect dependency return types for schema generation, |
|||
so this pattern should not raise an error. |
|||
""" |
|||
mod = importlib.import_module(f"{APP_PREFIX}.type_checking_dep_return_type_safe") |
|||
client = TestClient(mod.app) |
|||
response = client.get("/") |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"db": "fake_db"} |
|||
|
|||
|
|||
def test_valid_type_before_endpoint(): |
|||
"""Sanity check: a type defined before the route works normally.""" |
|||
mod = importlib.import_module(f"{APP_PREFIX}.valid_type_before_endpoint") |
|||
client = TestClient(mod.app) |
|||
response = client.post("/", json={"name": "test"}) |
|||
assert response.status_code == 200 |
|||
assert response.json() == {"name": "test"} |
|||
|
|||
|
|||
def test_error_message_mentions_both_causes(): |
|||
"""The error message should mention both TYPE_CHECKING and late declaration.""" |
|||
from fastapi.dependencies.utils import get_typed_annotation |
|||
|
|||
with pytest.raises(NameError, match="TYPE_CHECKING") as exc_info: |
|||
get_typed_annotation("Unresolvable", {}) |
|||
assert "defined after the route" in str(exc_info.value) |
|||
Loading…
Reference in new issue