← eval_submission_analyzer

fastapi_9555

resolved RESOLVED PASS · 16 tool calls · 103 s · fastapi/fastapi

Task input

✨ Handle wrapped dependencies

Use the unwrapped call in `solve_dependencies` to determine if a dependency is a coroutine or (async) generator.

Since non-async dependencies are run in a threadpool, it's generally preferably to use async dependencies. For dependencies that will not change during the lifetime of the server (like settings), it's preferable to use `functools.cache` to avoid duplicate work. There are two issues with this.

First is that `@cache` will cache the coroutine itself not the result, so the first call will succeed; however, subsequent calls will fail with `RuntimeError: cannot reuse already awaited coroutine`. This is fixable with @serhiy-storchaka's `reawaitable` decorator outlined in  https://github.com/python/cpython/issues/90780 .

The second issue is that when solving dependencies, FastAPI does not consider the `__wrapped__` attribute, so because the `lru_cache_wrapper` object is not a coroutine, FastAPI will not attempt to await it. In the example below this results in an error `AttributeError: 'coroutine' object has no attribute 'foo'`. This is the issue this PR solves by calling `inspect.unwrap` on the dependency call and using the unwrapped call to determine what type it is.

```py3
from functools import cache
from functools import wraps
from typing import Awaitable
from typing import Callable
from typing import Generic
from typing import Iterator
from typing import ParamSpec
from typing import TypeVar

from fastapi import Depends
from fastapi import FastAPI
from pydantic import BaseSettings


P = ParamSpec('P')
T = TypeVar('T')


# Based off https://github.com/python/cpython/issues/90780#issuecomment-1093943964
class Reawaitable(Generic[T]):
    __sentinel = object()

    def __init__(self, awaitable: Awaitable[T]) -> None:
        self.awaitable = awaitable
        self.result = self.__sentinel

    def __await__(self) -> Iterator[T]:
        if self.result is self.__sentinel:
            self.result = yield from self.awaitable.__await__()
        return self.result


def reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]:
    @wraps(func)
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]:
        return Reawaitable(func(*args, **kwargs))
    return wrapper


class Settings(BaseSettings):
    foo: str = 'bar'


app = FastAPI()


@cache
@reawaitable
async def get_settings() -> Settings:
    return Settings()


@app.get('/foo')
async def foo(settings: Settings = Depends(get_settings)) -> str:
    return settings.foo
```

If anyone comes across this in the mean time, a reasonable workaround is to wrap the cached function in an undecorated function. 

```py3
async def get_settings() -> Settings:
    return await _get_settings()


@cache
@reawaitable
async def _get_settings() -> Settings:
    return Settings()
```

(Related to #5077)

Tool calls (17)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"def solve_dependencies\" ."
}
{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:565:async def solve_dependencies(\n", "exit_code": 0}
full
{"status": "ok", "stdout": "./fastapi/dependencies/utils.py:565:async def solve_dependencies(\n", "exit_code": 0}
2read_file
{
 "end_line": 650,
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 565
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "async def solve_dependencies(\n *,\n request: Union[Request, WebSocket],\n dependant:
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "async def solve_dependencies(\n    *,\n    request: Union[Request, WebSocket],\n    dependant: Dependant,\n    body: Optional[Union[Dict[str, Any], FormData]] = None,\n    background_tasks: Optional[StarletteBackgroundTasks] = None,\n    response: Optional[Response] = None,\n    dependency_overrides_provider: Optional[Any] = None,\n    dependency_cache: Optional[Dict[DependencyCacheKey, Any]] = None,\n    # TODO: remove this parameter later, no longer used, not removing it yet as some\n    # people might be monkey patching this function (although that's not supported)\n    async_exit_stack: AsyncExitStack,\n    embed_body_fields: bool,\n) -> SolvedDependency:\n    request_astack = request.scope.get(\"fastapi_inner_astack\")\n    assert isinstance(request_astack, AsyncExitStack), (\n        \"fastapi_inner_astack not found in request scope\"\n    )\n    function_astack = request.scope.get(\"fastapi_function_astack\")\n    assert isinstance(function_astack, AsyncExitStack), (\n        \"fastapi_function_astack not found in request scope\"\n    )\n    values: Dict[str, Any] = {}\n    errors: List[Any] = []\n    if response is None:\n        response = Response()\n        del response.headers[\"content-length\"]\n        response.status_code = None  # type: ignore\n    if dependency_cache is None:\n        dependency_cache = {}\n    for sub_dependant in dependant.dependencies:\n        sub_dependant.call = cast(Callable[..., Any], sub_dependant.call)\n        call = sub_dependant.call\n        use_sub_dependant = sub_dependant\n        if (\n            dependency_overrides_provider\n            and dependency_overrides_provider.dependency_overrides\n        ):\n            original_call = sub_dependant.call\n            call = getattr(\n                dependency_overrides_provider, \"dependency_overrides\", {}\n            ).get(original_call, original_call)\n            use_path: str = sub_depe
3read_file
{
 "filepath": "fastapi/dependencies/models.py"
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "import inspect\nimport sys\nfrom dataclasses import dataclass, field\nfrom functools
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "content": "import inspect\nimport sys\nfrom dataclasses import dataclass, field\nfrom functools import cached_property\nfrom typing import Any, Callable, List, Optional, Sequence, Union\n\nfrom fastapi._compat import ModelField\nfrom fastapi.security.base import SecurityBase\nfrom fastapi.types import DependencyCacheKey\nfrom typing_extensions import Literal\n\nif sys.version_info >= (3, 13):  # pragma: no cover\n    from inspect import iscoroutinefunction\nelse:  # pragma: no cover\n    from asyncio import iscoroutinefunction\n\n\n@dataclass\nclass SecurityRequirement:\n    security_scheme: SecurityBase\n    scopes: Optional[Sequence[str]] = None\n\n\n@dataclass\nclass Dependant:\n    path_params: List[ModelField] = field(default_factory=list)\n    query_params: List[ModelField] = field(default_factory=list)\n    header_params: List[ModelField] = field(default_factory=list)\n    cookie_params: List[ModelField] = field(default_factory=list)\n    body_params: List[ModelField] = field(default_factory=list)\n    dependencies: List[\"Dependant\"] = field(default_factory=list)\n    security_requirements: List[SecurityRequirement] = field(default_factory=list)\n    name: Optional[str] = None\n    call: Optional[Callable[..., Any]] = None\n    request_param_name: Optional[str] = None\n    websocket_param_name: Optional[str] = None\n    http_connection_param_name: Optional[str] = None\n    response_param_name: Optional[str] = None\n    background_tasks_param_name: Optional[str] = None\n    security_scopes_param_name: Optional[str] = None\n    own_oauth_scopes: Optional[List[str]] = None\n    parent_oauth_scopes: Optional[List[str]] = None\n    use_cache: bool = True\n    path: Optional[str] = None\n    scope: Union[Literal[\"function\", \"request\"], None] = None\n\n    @cached_property\n    def oauth_scopes(self) -> List[str]:\n        scopes = self.parent_oauth_scopes.copy() if self.parent_oauth_scopes else [
4read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 600,
 "end_line": 660
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " dependency_overrides_provider\n and dependency_overrides_provider.dependency_override
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "            dependency_overrides_provider\n            and dependency_overrides_provider.dependency_overrides\n        ):\n            original_call = sub_dependant.call\n            call = getattr(\n                dependency_overrides_provider, \"dependency_overrides\", {}\n            ).get(original_call, original_call)\n            use_path: str = sub_dependant.path  # type: ignore\n            use_sub_dependant = get_dependant(\n                path=use_path,\n                call=call,\n                name=sub_dependant.name,\n                parent_oauth_scopes=sub_dependant.oauth_scopes,\n                scope=sub_dependant.scope,\n            )\n\n        solved_result = await solve_dependencies(\n            request=request,\n            dependant=use_sub_dependant,\n            body=body,\n            background_tasks=background_tasks,\n            response=response,\n            dependency_overrides_provider=dependency_overrides_provider,\n            dependency_cache=dependency_cache,\n            async_exit_stack=async_exit_stack,\n            embed_body_fields=embed_body_fields,\n        )\n        background_tasks = solved_result.background_tasks\n        if solved_result.errors:\n            errors.extend(solved_result.errors)\n            continue\n        if sub_dependant.use_cache and sub_dependant.cache_key in dependency_cache:\n            solved = dependency_cache[sub_dependant.cache_key]\n        elif (\n            use_sub_dependant.is_gen_callable or use_sub_dependant.is_async_gen_callable\n        ):\n            use_astack = request_astack\n            if sub_dependant.scope == \"function\":\n                use_astack = function_astack\n            solved = await _solve_generator(\n                dependant=use_sub_dependant,\n                stack=use_astack,\n                sub_values=solved_result.values,\n            )\n        elif use_sub_dependant.is_corou
5write_file
{
 "content": "from functools import cache\nfrom functools import wraps\nfrom typing import Awaitable\nfrom typing import Callable\nfrom typing import Generic\nfrom typing import Iterator\nfrom typing import ParamSpec\nfrom typing import TypeVar\n\nfrom fastapi import Depends\nfrom fastapi import FastAPI\nfrom pydantic import BaseSettings\n\nP = ParamSpec('P')\nT = TypeVar('T')\n\nclass Reawaitable(Generic[T]):\n    __sentinel = object()\n\n    def __init__(self, awaitable: Awaitable[T]) -> None:\n        self.awaitable = awaitable\n        self.result = self.__sentinel\n\n    def __await__(self) -> Iterator[T]:\n        if self.result is self.__sentinel:\n            self.result = yield from self.awaitable.__await__()\n        return self.result\n\ndef reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]:\n    @wraps(func)\n    def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]:\n        return Reawaitable(func(*args, **kwargs))\n    return wrapper\n\nclass Settings(BaseSettings):\n    foo: str = 'bar'\n\napp = FastAPI()\n\n@cache\n@reawaitable\nasync def get_settings() -> Settings:\n    return Settings()\n\n@app.get('/foo')\nasync def foo(settings: Settings = Depends(get_settings)) -> str:\n    return settings.foo\n\nfrom fastapi.testclient import TestClient\nclient = TestClient(app)\nresponse = client.get(\"/foo\")\nassert response.status_code == 200\nassert response.json() == \"bar\"\n",
 "filepath": "/tmp/repro.py"
}
{"status": "ok", "filepath": "/tmp/repro.py", "size": 1377}
full
{"status": "ok", "filepath": "/tmp/repro.py", "size": 1377}
6run_command
{
 "command": "python3 /tmp/repro.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open f
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open file '/tmp/swe_work/eval_submission_analyzer/fastapi_9555/a/tmp/repro.py': [Errno 2] No such file or directory\n", "exit_code": 2}}
7run_command
{
 "command": "python3 /workspace/tmp/repro.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open f
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open file '/tmp/swe_work/eval_submission_analyzer/fastapi_9555/a/workspace/tmp/repro.py': [Errno 2] No such file or directory\n", "exit_code": 2}}
8run_command
{
 "command": "mkdir -p /tmp && python3 /tmp/repro.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open f
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open file '/tmp/swe_work/eval_submission_analyzer/fastapi_9555/a/tmp/repro.py': [Errno 2] No such file or directory\n", "exit_code": 2}}
9run_command
{
 "command": "python3 -c \"from functools import cache; from functools import wraps; from typing import Awaitable; from typing import Callable; from typing import Generic; from typing import Iterator; from typing import ParamSpec; from typing import TypeVar; from fastapi import Depends; from fastapi import FastAPI; from pydantic import BaseSettings; P = ParamSpec('P'); T = TypeVar('T'); class Reawaitable(Generic[T]): __sentinel = object(); def __init__(self, awaitable: Awaitable[T]) -> None: self.awaitable = awaitable; self.result = self.__sentinel; def __await__(self) -> Iterator[T]: if self.result is self.__sentinel: self.result = yield from self.awaitable.__await__(); return self.result; def reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]: @wraps(func); def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]: return Reawaitable(func(*args, **kwargs)); return wrapper; class Settings(BaseSettings): foo: str = 'bar'; app = FastAPI(); @cache; @reawaitable; async def get_settings() -> Settings: return Settings(); @app.get('/foo'); async def foo(settings: Settings = Depends(get_settings)) -> str: return settings.foo; from fastapi.testclient import TestClient; client = TestClient(app); response = client.get('/foo'); print(response.status_code); print(response.json())\""
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": " File \"<string>\", l
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "  File \"<string>\", line 1\n    from functools import cache; from functools import wraps; from typing import Awaitable; from typing import Callable; from typing import Generic; from typing import Iterator; from typing import ParamSpec; from typing import TypeVar; from fastapi import Depends; from fastapi import FastAPI; from pydantic import BaseSettings; P = ParamSpec('P'); T = TypeVar('T'); class Reawaitable(Generic[T]): __sentinel = object(); def __init__(self, awaitable: Awaitable[T]) -> None: self.awaitable = awaitable; self.result = self.__sentinel; def __await__(self) -> Iterator[T]: if self.result is self.__sentinel: self.result = yield from self.awaitable.__await__(); return self.result; def reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]: @wraps(func); def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]: return Reawaitable(func(*args, **kwargs)); return wrapper; class Settings(BaseSettings): foo: str = 'bar'; app = FastAPI(); @cache; @reawaitable; async def get_settings() -> Settings: return Settings(); @app.get('/foo'); async def foo(settings: Settings = Depends(get_settings)) -> str: return settings.foo; from fastapi.testclient import TestClient; client = TestClient(app); response = client.get('/foo'); print(response.status_code); print(response.json())\n                                                                                                                                                                                                                                                                                                                                                                               ^^^^^\nSyntaxError: invalid syntax\n", "exit_code": 1}}
10write_file
{
 "content": "from functools import cache\nfrom functools import wraps\nfrom typing import Awaitable\nfrom typing import Callable\nfrom typing import Generic\nfrom typing import Iterator\nfrom typing import ParamSpec\nfrom typing import TypeVar\n\nfrom fastapi import Depends\nfrom fastapi import FastAPI\nfrom pydantic import BaseSettings\n\nP = ParamSpec('P')\nT = TypeVar('T')\n\nclass Reawaitable(Generic[T]):\n    __sentinel = object()\n\n    def __init__(self, awaitable: Awaitable[T]) -> None:\n        self.awaitable = awaitable\n        self.result = self.__sentinel\n\n    def __await__(self) -> Iterator[T]:\n        if self.result is self.__sentinel:\n            self.result = yield from self.awaitable.__await__()\n        return self.result\n\ndef reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]:\n    @wraps(func)\n    def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]:\n        return Reawaitable(func(*args, **kwargs))\n    return wrapper\n\nclass Settings(BaseSettings):\n    foo: str = 'bar'\n\napp = FastAPI()\n\n@cache\n@reawaitable\nasync def get_settings() -> Settings:\n    return Settings()\n\n@app.get('/foo')\nasync def foo(settings: Settings = Depends(get_settings)) -> str:\n    return settings.foo\n\nif __name__ == \"__main__\":\n    from fastapi.testclient import TestClient\n    client = TestClient(app)\n    response = client.get(\"/foo\")\n    print(f\"Status: {response.status_code}\")\n    print(f\"Body: {response.json()}\")\n    assert response.status_code == 200\n    assert response.json() == \"bar\"\n",
 "filepath": "repro.py"
}
{"status": "ok", "filepath": "repro.py", "size": 1507}
full
{"status": "ok", "filepath": "repro.py", "size": 1507}
11run_command
{
 "command": "python3 repro.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recen
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"/workspace/repro.py\", line 12, in <module>\n    from pydantic import BaseSettings\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/pydantic/__init__.py\", line 437, in __getattr__\n    return _getattr_migration(attr_name)\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/pydantic/_migration.py\", line 304, in wrapper\n    raise PydanticImportError(\n    ...<3 lines>...\n    )\npydantic.errors.PydanticImportError: `BaseSettings` has been moved to the `pydantic-settings` package. See https://docs.pydantic.dev/2.13/migration/#basesettings-has-moved-to-pydantic-settings for more details.\n\nFor further information visit https://errors.pydantic.dev/2.13/u/import-error\n", "exit_code": 1}}
12edit_file
{
 "filepath": "repro.py",
 "new_string": "from pydantic_settings import BaseSettings",
 "old_string": "from pydantic import BaseSettings"
}
{"status": "ok", "filepath": "repro.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/repro.py\n+++ b/repro.py\n@@ -9,7 +9,7 @@\n \n from fastapi impor
full
{"status": "ok", "filepath": "repro.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/repro.py\n+++ b/repro.py\n@@ -9,7 +9,7 @@\n \n from fastapi import Depends\n from fastapi import FastAPI\n-from pydantic import BaseSettings\n+from pydantic_settings import BaseSettings\n \n P = ParamSpec('P')\n T = TypeVar('T')\n", "is_truncated": false}
13run_command
{
 "command": "python3 repro.py"
}
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recen
full
{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": "Traceback (most recent call last):\n  File \"/workspace/repro.py\", line 52, in <module>\n    response = client.get(\"/foo\")\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py\", line 473, in get\n    return super().get(\n           ~~~~~~~~~~~^\n        url,\n        ^^^^\n    ...<6 lines>...\n        extensions=extensions,\n        ^^^^^^^^^^^^^^^^^^^^^^\n    )\n    ^\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 1053, in get\n    return self.request(\n           ~~~~~~~~~~~~^\n        \"GET\",\n        ^^^^^^\n    ...<7 lines>...\n        extensions=extensions,\n        ^^^^^^^^^^^^^^^^^^^^^^\n    )\n    ^\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py\", line 445, in request\n    return super().request(\n           ~~~~~~~~~~~~~~~^\n        method,\n        ^^^^^^^\n    ...<11 lines>...\n        extensions=extensions,\n        ^^^^^^^^^^^^^^^^^^^^^^\n    )\n    ^\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 825, in request\n    return self.send(request, auth=auth, follow_redirects=follow_redirects)\n           ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 914, in send\n    response = self._send_handling_auth(\n        request,\n    ...<2 lines>...\n        history=[],\n    )\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 942, in _send_handling_auth\n    response = self._send_handling_redirects(\n        request,\n        follow_redirects=follow_redirects,\n        history=history,\n    )\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/httpx/_client.py\", line 979, in _send_handling_redirects\n   
14run_command
{
 "command": "python3 repro.py 2>&1 | tail -n 50"
}
{"status": "ok", "stdout": " ~~~~~~~~~~~~~~~~~^^\n File \"/root/.local/share/uv/python/cpython-3.13.14-linux-x86_64-gnu/lib/python3.13/concurrent/futures/_base.
full
{"status": "ok", "stdout": "           ~~~~~~~~~~~~~~~~~^^\n  File \"/root/.local/share/uv/python/cpython-3.13.14-linux-x86_64-gnu/lib/python3.13/concurrent/futures/_base.py\", line 402, in __get_result\n    raise self._exception\n  File \"/kaggle/tmp/envs/fastapi/lib/python3.13/site-packages/anyio/from_thread.py\", line 265, in _call_func\n    retval = await retval_or_awaitable\n             ^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/workspace/fastapi/applications.py\", line 1139, in __call__\n    await super().__call__(scope, receive, send)\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/applications.py\", line 107, in __call__\n    await self.middleware_stack(scope, receive, send)\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/middleware/errors.py\", line 186, in __call__\n    raise exc\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/middleware/errors.py\", line 164, in __call__\n    await self.app(scope, receive, _send)\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/middleware/exceptions.py\", line 63, in __call__\n    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/_exception_handler.py\", line 53, in wrapped_app\n    raise exc\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/_exception_handler.py\", line 42, in wrapped_app\n    await app(scope, receive, sender)\n  File \"/workspace/fastapi/middleware/asyncexitstack.py\", line 18, in __call__\n    await self.app(scope, receive, send)\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/routing.py\", line 716, in __call__\n    await self.middleware_stack(scope, receive, send)\n  File \"/kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/routing.py\", line 736, in app\n    await route.handle(scope, receive, send)\n  File \"/kaggle/tmp/envs/overl
15edit_file
{
 "filepath": "fastapi/dependencies/models.py",
 "new_string": "    @cached_property\n    def is_gen_callable(self) -> bool:\n        call = inspect.unwrap(self.call)\n        if inspect.isgeneratorfunction(call):\n            return True\n        dunder_call = getattr(call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        call = inspect.unwrap(self.call)\n        if inspect.isasyncgenfunction(call):\n            return True\n        dunder_call = getattr(call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        call = inspect.unwrap(self.call)\n        if inspect.isroutine(call):\n            return iscoroutinefunction(call)\n        if inspect.isclass(call):\n            return False\n        dunder_call = getattr(call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)",
 "old_string": "    @cached_property\n    def is_gen_callable(self) -> bool:\n        if inspect.isgeneratorfunction(self.call):\n            return True\n        dunder_call = getattr(self.call, \"__call__\", None)  # noqa: B004\n        return inspect.isgeneratorfunction(dunder_call)\n\n    @cached_property\n    def is_async_gen_callable(self) -> bool:\n        if inspect.isasyncgenfunction(self.call):\n            return True\n        dunder_call = getattr(self.call, \"__call__\", None)  # noqa: B004\n        return inspect.isasyncgenfunction(dunder_call)\n\n    @cached_property\n    def is_coroutine_callable(self) -> bool:\n        if inspect.isroutine(self.call):\n            return iscoroutinefunction(self.call)\n        if inspect.isclass(self.call):\n            return False\n        dunder_call = getattr(self.call, \"__call__\", None)  # noqa: B004\n        return iscoroutinefunction(dunder_call)"
}
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/models.py\n+++ b/fasta
full
{"status": "ok", "filepath": "fastapi/dependencies/models.py", "occurrences": 1, "strategy": "exact", "diff": "--- a/fastapi/dependencies/models.py\n+++ b/fastapi/dependencies/models.py\n@@ -77,25 +77,28 @@\n \n     @cached_property\n     def is_gen_callable(self) -> bool:\n-        if inspect.isgeneratorfunction(self.call):\n+        call = inspect.unwrap(self.call)\n+        if inspect.isgeneratorfunction(call):\n             return True\n-        dunder_call = getattr(self.call, \"__call__\", None)  # noqa: B004\n+        dunder_call = getattr(call, \"__call__\", None)  # noqa: B004\n         return inspect.isgeneratorfunction(dunder_call)\n \n     @cached_property\n     def is_async_gen_callable(self) -> bool:\n-        if inspect.isasyncgenfunction(self.call):\n+        call = inspect.unwrap(self.call)\n+        if inspect.isasyncgenfunction(call):\n             return True\n-        dunder_call = getattr(self.call, \"__call__\", None)  # noqa: B004\n+        dunder_call = getattr(call, \"__call__\", None)  # noqa: B004\n         return inspect.isasyncgenfunction(dunder_call)\n \n     @cached_property\n     def is_coroutine_callable(self) -> bool:\n-        if inspect.isroutine(self.call):\n-            return iscoroutinefunction(self.call)\n-        if inspect.isclass(self.call):\n+        call = inspect.unwrap(self.call)\n+        if inspect.isroutine(call):\n+            return iscoroutinefunction(call)\n+        if inspect.isclass(call):\n             return False\n-        dunder_call = getattr(self.call, \"__call__\", None)  # noqa: B004\n+        dunder_call = getattr(call, \"__call__\", None)  # noqa: B004\n         return iscoroutinefunction(dunder_call)\n \n     @cached_property\n", "is_truncated": false}
16run_command
{
 "command": "python3 repro.py"
}
{"status": "ok", "stdout": "Status: 200\nBody: bar\n", "exit_code": 0}
full
{"status": "ok", "stdout": "Status: 200\nBody: bar\n", "exit_code": 0}
17submit_patch
[]
{"status": "ok", "patch_size": 4952, "files_changed": 3}
full
{"status": "ok", "patch_size": 4952, "files_changed": 3}

Patch

diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index fbb666a7..cdaf6321 100644
--- a/fastapi/dependencies/models.py
+++ b/fastapi/dependencies/models.py
@@ -77,25 +77,28 @@ class Dependant:
 
     @cached_property
     def is_gen_callable(self) -> bool:
-        if inspect.isgeneratorfunction(self.call):
+        call = inspect.unwrap(self.call)
+        if inspect.isgeneratorfunction(call):
             return True
-        dunder_call = getattr(self.call, "__call__", None)  # noqa: B004
+        dunder_call = getattr(call, "__call__", None)  # noqa: B004
         return inspect.isgeneratorfunction(dunder_call)
 
     @cached_property
     def is_async_gen_callable(self) -> bool:
-        if inspect.isasyncgenfunction(self.call):
+        call = inspect.unwrap(self.call)
+        if inspect.isasyncgenfunction(call):
             return True
-        dunder_call = getattr(self.call, "__call__", None)  # noqa: B004
+        dunder_call = getattr(call, "__call__", None)  # noqa: B004
         return inspect.isasyncgenfunction(dunder_call)
 
     @cached_property
     def is_coroutine_callable(self) -> bool:
-        if inspect.isroutine(self.call):
-            return iscoroutinefunction(self.call)
-        if inspect.isclass(self.call):
+        call = inspect.unwrap(self.call)
+        if inspect.isroutine(call):
+            return iscoroutinefunction(call)
+        if inspect.isclass(call):
             return False
-        dunder_call = getattr(self.call, "__call__", None)  # noqa: B004
+        dunder_call = getattr(call, "__call__", None)  # noqa: B004
         return iscoroutinefunction(dunder_call)
 
     @cached_property
diff --git a/repro.py b/repro.py
new file mode 100644
index 00000000..2245ecc3
--- /dev/null
+++ b/repro.py
@@ -0,0 +1,56 @@
+from functools import cache
+from functools import wraps
+from typing import Awaitable
+from typing import Callable
+from typing import Generic
+from typing import Iterator
+from typing import ParamSpec
+from typing import TypeVar
+
+from fastapi import Depends
+from fastapi import FastAPI
+from pydantic_settings import BaseSettings
+
+P = ParamSpec('P')
+T = TypeVar('T')
+
+class Reawaitable(Generic[T]):
+    __sentinel = object()
+
+    def __init__(self, awaitable: Awaitable[T]) -> None:
+        self.awaitable = awaitable
+        self.result = self.__sentinel
+
+    def __await__(self) -> Iterator[T]:
+        if self.result is self.__sentinel:
+            self.result = yield from self.awaitable.__await__()
+        return self.result
+
+def reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]:
+    @wraps(func)
+    def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]:
+        return Reawaitable(func(*args, **kwargs))
+    return wrapper
+
+class Settings(BaseSettings):
+    foo: str = 'bar'
+
+app = FastAPI()
+
+@cache
+@reawaitable
+async def get_settings() -> Settings:
+    return Settings()
+
+@app.get('/foo')
+async def foo(settings: Settings = Depends(get_settings)) -> str:
+    return settings.foo
+
+if __name__ == "__main__":
+    from fastapi.testclient import TestClient
+    client = TestClient(app)
+    response = client.get("/foo")
+    print(f"Status: {response.status_code}")
+    print(f"Body: {response.json()}")
+    assert response.status_code == 200
+    assert response.json() == "bar"
diff --git a/tmp/repro.py b/tmp/repro.py
new file mode 100644
index 00000000..42f46e23
--- /dev/null
+++ b/tmp/repro.py
@@ -0,0 +1,53 @@
+from functools import cache
+from functools import wraps
+from typing import Awaitable
+from typing import Callable
+from typing import Generic
+from typing import Iterator
+from typing import ParamSpec
+from typing import TypeVar
+
+from fastapi import Depends
+from fastapi import FastAPI
+from pydantic import BaseSettings
+
+P = ParamSpec('P')
+T = TypeVar('T')
+
+class Reawaitable(Generic[T]):
+    __sentinel = object()
+
+    def __init__(self, awaitable: Awaitable[T]) -> None:
+        self.awaitable = awaitable
+        self.result = self.__sentinel
+
+    def __await__(self) -> Iterator[T]:
+        if self.result is self.__sentinel:
+            self.result = yield from self.awaitable.__await__()
+        return self.result
+
+def reawaitable(func: Callable[P, Awaitable[T]]) -> Callable[P, Reawaitable[T]]:
+    @wraps(func)
+    def wrapper(*args: P.args, **kwargs: P.kwargs) -> Reawaitable[T]:
+        return Reawaitable(func(*args, **kwargs))
+    return wrapper
+
+class Settings(BaseSettings):
+    foo: str = 'bar'
+
+app = FastAPI()
+
+@cache
+@reawaitable
+async def get_settings() -> Settings:
+    return Settings()
+
+@app.get('/foo')
+async def foo(settings: Settings = Depends(get_settings)) -> str:
+    return settings.foo
+
+from fastapi.testclient import TestClient
+client = TestClient(app)
+response = client.get("/foo")
+assert response.status_code == 200
+assert response.json() == "bar"

Test output

show
....                                                                     [100%]
=============================== warnings summary ===============================
../../../../../../kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
  /kaggle/tmp/envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45: DeprecationWarning: The anyio.abc.BlockingPortal alias is deprecated, use anyio.from_thread.BlockingPortal instead.
    _PortalFactoryType = Callable[[], AbstractContextManager[anyio.abc.BlockingPortal]]

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
4 passed, 1 warning in 0.72s