Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle wrapped dependencies #9555

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

phy1729
Copy link

@phy1729 phy1729 commented May 20, 2023

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 python/cpython#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.

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.

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


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

(Related to #5077)

@github-actions
Copy link
Contributor

📝 Docs preview for commit 0db9928 at: https://646923cb4560f35d9856df5a--fastapi.netlify.app

@tiangolo tiangolo added p2 feature New feature or request labels Jan 14, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature or request p2
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants