failed NO_PATCH NO_PATCH UNSUBMITTED empty_patch(None) · None tool calls · 0 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 |
|---|---|---|---|
| No trace captured. | |||
.F
=================================== FAILURES ===================================
________________ test_class_dependency[/wrapped-gen-dependency] ________________
route = '/wrapped-gen-dependency'
@pytest.mark.parametrize(
"route",
[
"/wrapped-dependency",
"/wrapped-gen-dependency",
"/async-wrapped-dependency",
"/async-wrapped-gen-dependency",
],
)
def test_class_dependency(route):
response = client.get(route)
assert response.status_code == 200, response.text
> assert response.json() is True
E assert [True] is True
E + where [True] = json()
E + where json = <Response [200 OK]>.json
tests/test_dependency_wrapped.py:77: AssertionError
=============================== warnings summary ===============================
../../../../../../../Users/jp/repos/kaggle-gemini-coding-agent-post-training/.envs/overlays/starlette-0.50.0-py3-none-any/starlette/testclient.py:45
/Users/jp/repos/kaggle-gemini-coding-agent-post-training/.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
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
1 failed, 1 passed, 1 warning in 0.67s