diff --git a/fastapi/params.py b/fastapi/params.py index 2dc04be14..d0b2ccbd4 100644 --- a/fastapi/params.py +++ b/fastapi/params.py @@ -3,15 +3,12 @@ from dataclasses import dataclass from enum import Enum from typing import Any, Callable, Dict, List, Optional, Sequence, Union -from fastapi.openapi.models import Example from pydantic.fields import FieldInfo from typing_extensions import Annotated, deprecated -from ._compat import ( - PYDANTIC_V2, - PYDANTIC_VERSION_MINOR_TUPLE, - Undefined, -) +from fastapi.openapi.models import Example + +from ._compat import PYDANTIC_V2, PYDANTIC_VERSION_MINOR_TUPLE, Undefined _Unset: Any = Undefined @@ -767,6 +764,48 @@ class Depends: dependency: Optional[Callable[..., Any]] = None use_cache: bool = True + def __init__( + self, dependency: Optional[Callable[..., Any]] = None, *, use_cache: bool = True + ): + # Validate that dependency is used correctly + if dependency is not None: + # Check for nested Depends + if isinstance(dependency, Depends): + raise TypeError( + "Invalid usage of Depends(). " + "You have nested Depends() calls: Depends(Depends(...)). " + "Each dependency should only be wrapped once. " + "Remove the inner Depends() and pass the function directly." + ) + + # Check if dependency is callable + if not callable(dependency): + # Provide helpful error based on the type + dep_type = type(dependency).__name__ + error_msg = f"Depends() expects a callable (function or class), but received {dep_type}. " + + # Add specific hints for common mistakes + if isinstance(dependency, (dict, list, str, int, float, bool)): + error_msg += ( + "\n\nIt looks like you may have called the dependency function instead of passing it. " + "\nāœ“ Correct: Depends(my_function)" + "\nāœ— Wrong: Depends(my_function())" + ) + elif hasattr(dependency, "__class__") and hasattr( + dependency.__class__, "__call__" + ): + # Might be an instance of a callable class + error_msg += "\n\nIf you're using a callable class, make sure to pass the class itself or a callable instance." + + raise TypeError(error_msg) + + self.dependency = dependency + self.use_cache = use_cache + + def __repr__(self) -> str: + attr = getattr(self.dependency, "__name__", type(self.dependency).__name__) + cache = "" if self.use_cache else ", use_cache=False" + return f"{self.__class__.__name__}({attr}{cache})" @dataclass class Security(Depends): diff --git a/tests/test_depends_validation.py b/tests/test_depends_validation.py new file mode 100644 index 000000000..080d6fda8 --- /dev/null +++ b/tests/test_depends_validation.py @@ -0,0 +1,150 @@ +import pytest + +from fastapi import Depends, FastAPI +from fastapi.testclient import TestClient + + +def test_depends_with_called_function(): + """Test that calling a function in Depends() raises a clear error.""" + + def get_value(): + return "value" + + with pytest.raises(TypeError) as exc_info: + app = FastAPI() + + @app.get("/") + def route(val=Depends(get_value())): # Wrong: calling the function + return val + + error_message = str(exc_info.value) + assert "callable" in error_message.lower() + assert ( + "Depends(my_function())" in error_message + or "called the dependency" in error_message + ) + + +def test_depends_with_nested_depends(): + """Test that nesting Depends raises a clear error.""" + + def get_value(): + return "value" + + with pytest.raises(TypeError) as exc_info: + app = FastAPI() + + @app.get("/") + def route(val=Depends(Depends(get_value))): # Wrong: nested Depends + return val + + error_message = str(exc_info.value) + assert "nested" in error_message.lower() + assert "Depends(Depends(" in error_message + + +def test_depends_with_string(): + """Test that passing a string to Depends raises a clear error.""" + + with pytest.raises(TypeError) as exc_info: + app = FastAPI() + + @app.get("/") + def route(val=Depends("not_a_function")): # Wrong: string + return val + + error_message = str(exc_info.value) + assert "callable" in error_message.lower() + assert "str" in error_message + + +def test_depends_with_dict(): + """Test that passing a dict to Depends raises a clear error.""" + + with pytest.raises(TypeError) as exc_info: + app = FastAPI() + + @app.get("/") + def route(val=Depends({"key": "value"})): # Wrong: dict + return val + + error_message = str(exc_info.value) + assert "callable" in error_message.lower() + assert "dict" in error_message + + +def test_depends_with_correct_usage(): + """Test that correct usage of Depends works fine.""" + + def get_value(): + return "correct_value" + + app = FastAPI() + + @app.get("/") + def route(val: str = Depends(get_value)): # Correct usage + return {"value": val} + + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"value": "correct_value"} + + +def test_depends_with_callable_class(): + """Test that Depends works with callable classes.""" + + class CallableClass: + def __call__(self): + return "from_callable_class" + + app = FastAPI() + + @app.get("/") + def route(val: str = Depends(CallableClass())): # Correct: callable instance + return {"value": val} + + client = TestClient(app) + response = client.get("/") + assert response.status_code == 200 + assert response.json() == {"value": "from_callable_class"} + + +def test_depends_with_none(): + """Test that Depends with None works (for sub-dependencies).""" + + app = FastAPI() + + @app.get("/") + def route(val=Depends(None)): # Valid for sub-dependencies + return {"value": "none"} + + # This should not raise an error during app definition + assert app is not None + + +def test_error_message_quality(): + """Test that error messages are helpful and specific.""" + + def returns_dict(): + return {"data": "value"} + + with pytest.raises(TypeError) as exc_info: + app = FastAPI() + + @app.get("/") + def route(val=Depends(returns_dict())): + return val + + error_message = str(exc_info.value) + + # Check that the error message contains helpful hints + assert any( + hint in error_message + for hint in [ + "āœ“ Correct", + "āœ— Wrong", + "called the dependency", + "function instead of passing it", + ] + )