resolved RESOLVED PASS · 11 tool calls · 258 s · fastapi/fastapi
✨ 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 | Arguments | Result |
|---|---|---|---|
| 1 | run_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_depefull{"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} |
| 2 | read_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 |
| 3 | read_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_mofull{"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 |
| 4 | run_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} |
| 5 | read_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 [ |
| 6 | edit_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/fastafull{"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} |
| 7 | write_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\nfrom typing import Any\n\nfrom fastapi import Depends\nfrom fastapi import FastAPI\nfrom pydantic import BaseModel\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(BaseModel):\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')\nprint(f\"Status: {response.status_code}, Body: {response.json()}\")\nassert response.status_code == 200\nassert response.json() == 'bar'\n",
"filepath": "/tmp/repro.py"
} | {"status": "ok", "filepath": "/tmp/repro.py", "size": 1460}full{"status": "ok", "filepath": "/tmp/repro.py", "size": 1460} |
| 8 | run_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 ffull{"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/fastapi_9555/a/tmp/repro.py': [Errno 2] No such file or directory\n", "exit_code": 2}} |
| 9 | run_command | {
"command": "python3 /workspace/tmp/repro.py || python3 /tmp/repro.py"
} | {"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 2", "details": {"stdout": "", "stderr": "python3: can't open ffull{"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/fastapi_9555/a/workspace/tmp/repro.py': [Errno 2] No such file or directory\npython3: can't open file '/tmp/swe_work/eval_submission/fastapi_9555/a/tmp/repro.py': [Errno 2] No such file or directory\n", "exit_code": 2}} |
| 10 | run_command | {
"command": "mkdir -p /tmp && write_file /tmp/repro.py \"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\\nfrom typing import Any\\n\\nfrom fastapi import Depends\\nfrom fastapi import FastAPI\\nfrom pydantic import BaseModel\\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(BaseModel):\\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')\\nprint(f\\\"Status: {response.status_code}, Body: {response.json()}\\\")\\nassert response.status_code == 200\\nassert response.json() == 'bar'\\n\" && python3 /tmp/repro.py"
} | {"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 127", "details": {"stdout": "", "stderr": "/bin/bash: line 1: full{"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 127", "details": {"stdout": "", "stderr": "/bin/bash: line 1: write_file: command not found\n", "exit_code": 127}} |
| 11 | run_command | {
"command": "python3 -c \"from functools import cache, wraps; from typing import Awaitable, Callable, Generic, Iterator, ParamSpec, TypeVar, Any; from fastapi import Depends, FastAPI; from pydantic import BaseModel; from fastapi.testclient import TestClient; 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(BaseModel): 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; client = TestClient(app); response = client.get('/foo'); print(f'Status: {response.status_code}, Body: {response.json()}'); assert response.status_code == 200; assert response.json() == 'bar'\""
} | {"status": "error", "error_type": "CommandError", "error_message": "Command failed with exit code 1", "details": {"stdout": "", "stderr": " File \"<string>\", lfull{"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, wraps; from typing import Awaitable, Callable, Generic, Iterator, ParamSpec, TypeVar, Any; from fastapi import Depends, FastAPI; from pydantic import BaseModel; from fastapi.testclient import TestClient; 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(BaseModel): 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; client = TestClient(app); response = client.get('/foo'); print(f'Status: {response.status_code}, Body: {response.json()}'); assert response.status_code == 200; assert response.json() == 'bar'\n ^^^^^\nSyntaxError: invalid syntax\n", "exit_code": 1}} |
| 12 | submit_patch | [] | {"status": "ok", "patch_size": 3409, "files_changed": 2}full{"status": "ok", "patch_size": 3409, "files_changed": 2} |
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
diff --git a/tmp/repro.py b/tmp/repro.py
new file mode 100644
index 00000000..668040d3
--- /dev/null
+++ b/tmp/repro.py
@@ -0,0 +1,55 @@
+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 typing import Any
+
+from fastapi import Depends
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+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(BaseModel):
+ 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(f"Status: {response.status_code}, Body: {response.json()}")
+assert response.status_code == 200
+assert response.json() == 'bar'
.... [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 1.87s