← eval_submission_nothink

fastapi_9555

resolved RESOLVED PASS · 9 tool calls · 145 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 (10)

#ToolArgumentsResult
1run_command
{
 "command": "grep -rn \"solve_dependencies\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:565:async def solve_dependencies(\nfastapi/dependencies/utils.py:616: solved_result = await solve_depe
full
{"status": "ok", "stdout": "fastapi/dependencies/utils.py:565:async def solve_dependencies(\nfastapi/dependencies/utils.py:616:        solved_result = await solve_dependencies(\nfastapi/routing.py:46:    solve_dependencies,\nfastapi/routing.py:381:        solved_result = await solve_dependencies(\nfastapi/routing.py:451:        solved_result = await solve_dependencies(\n", "exit_code": 0}
2read_file
{
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 565,
 "end_line": 650
}
{"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
{
 "end_line": 560,
 "filepath": "fastapi/dependencies/utils.py",
 "start_line": 500
}
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": " alias = field_info.alias or param_name\n field_info.alias = alias\n field = create_mo
full
{"status": "ok", "filepath": "fastapi/dependencies/utils.py", "content": "            alias = field_info.alias or param_name\n        field_info.alias = alias\n        field = create_model_field(\n            name=param_name,\n            type_=use_annotation_from_field_info,\n            default=field_info.default,\n            alias=alias,\n            required=field_info.default\n            in (RequiredParam, may_v1.RequiredParam, Undefined),\n            field_info=field_info,\n        )\n        if is_path_param:\n            assert is_scalar_field(field=field), (\n                \"Path params must be of one of the supported types\"\n            )\n        elif isinstance(field_info, (params.Query, temp_pydantic_v1_params.Query)):\n            assert (\n                is_scalar_field(field)\n                or is_scalar_sequence_field(field)\n                or (\n                    _is_model_class(field.type_)\n                    # For Pydantic v1\n                    and getattr(field, \"shape\", 1) == 1\n                )\n            )\n\n    return ParamDetails(type_annotation=type_annotation, depends=depends, field=field)\n\n\ndef add_param_to_fields(*, field: ModelField, dependant: Dependant) -> None:\n    field_info = field.field_info\n    field_info_in = getattr(field_info, \"in_\", None)\n    if field_info_in == params.ParamTypes.path:\n        dependant.path_params.append(field)\n    elif field_info_in == params.ParamTypes.query:\n        dependant.query_params.append(field)\n    elif field_info_in == params.ParamTypes.header:\n        dependant.header_params.append(field)\n    else:\n        assert field_info_in == params.ParamTypes.cookie, (\n            f\"non-body parameters must be in path, query, header or cookie: {field.name}\"\n        )\n        dependant.cookie_params.append(field)\n\n\nasync def _solve_generator(\n    *, dependant: Dependant, stack: AsyncExitStack, sub_values: Dict[str, Any]\n) -> Any:\n    assert dependant.call\n    
4run_command
{
 "command": "grep -rn \"class Dependant\" fastapi | head -20"
}
{"status": "ok", "stdout": "fastapi/dependencies/models.py:25:class Dependant:\n", "exit_code": 0}
full
{"status": "ok", "stdout": "fastapi/dependencies/models.py:25:class Dependant:\n", "exit_code": 0}
5read_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 [
6run_command
{
 "command": "python3 -c \"\nimport inspect\nfrom functools import cache, wraps\nfrom typing import Awaitable, Callable, Generic, Iterator, ParamSpec, TypeVar\nfrom pydantic import BaseModel\n\nP = ParamSpec('P')\nT = TypeVar('T')\n\nclass Reawaitable(Generic[T]):\n    __sentinel = object()\n    def __init__(self, awaitable: Awaitable[T]) -> None:\n        self.awaitable = awaitable\n        self.result = self.__sentinel\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(BaseModel):\n    foo: str = 'bar'\n\n@cache\n@reawaitable\nasync def get_settings() -> Settings:\n    return Settings()\n\nprint(f'is_routine: {inspect.isroutine(get_settings)}')\nprint(f'is_coroutine_function: {inspect.iscoroutinefunction(get_settings)}')\nprint(f'unwrapped: {inspect.unwrap(get_settings)}')\nprint(f'unwrapped is_coroutine_function: {inspect.iscoroutinefunction(inspect.unwrap(get_settings))}')\n\""
}
{"status": "ok", "stdout": "is_routine: True\nis_coroutine_function: False\nunwrapped: <function get_settings at 0x78ea78632c00>\nunwrapped is_coroutine_functio
full
{"status": "ok", "stdout": "is_routine: True\nis_coroutine_function: False\nunwrapped: <function get_settings at 0x78ea78632c00>\nunwrapped is_coroutine_function: True\n", "exit_code": 0}
7edit_file
{
 "filepath": "fastapi/dependencies/models.py",
 "new_string": "    @cached_property\n    def is_gen_callable(self) -> bool:\n        call = inspect.unwrap(self.call) if self.call else None\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) if self.call else None\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) if self.call else None\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": "error", "error_type": "FileEditError", "error_message": "old_string not found in file (tried exact, flexible and regex matching)"}
full
{"status": "error", "error_type": "FileEditError", "error_message": "old_string not found in file (tried exact, flexible and regex matching)"}
8read_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 [
9edit_file
{
 "filepath": "fastapi/dependencies/models.py",
 "new_string": "    @cached_property\n    def is_gen_callable(self) -> bool:\n        call = inspect.unwrap(self.call) if self.call else None\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) if self.call else None\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) if self.call else None\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) if self.call else None\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) if self.call else None\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) if self.call else None\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}
10submit_patch
[]
{"status": "ok", "patch_size": 1757, "files_changed": 1}
full
{"status": "ok", "patch_size": 1757, "files_changed": 1}

Patch

diff --git a/fastapi/dependencies/models.py b/fastapi/dependencies/models.py
index fbb666a7..4aafbe8e 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 self.call else None
+        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 self.call else None
+        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 self.call else None
+        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

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.69s