Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Dependency injection failure for unhashable types #1985

Closed
edmondop opened this issue Aug 28, 2020 · 12 comments
Closed

Dependency injection failure for unhashable types #1985

edmondop opened this issue Aug 28, 2020 · 12 comments
Labels
question Question or problem question-migrate

Comments

@edmondop
Copy link

Example

Suppose you run the following script

export MY_PREFIX_PASSWORD=hello

and then

from fastapi import FastAPI

app = FastAPI()

class MySettings(BaseSettings):
    password: SecretStr
    base_url: AnyHttpUrl = "https://www.google.com"
    localdownload_dir: str = "~/mydir-import"

    class Config:
        env_prefix = "my_prefix"

def get_settings() -> MySettings:
    return MySettings()

   
@app.get("/")
def read_root(settings:MySettings = Depends(get_settings)):
    return {"Hello": "World"}

Description

When you invoke via curl the rest endpoint, you get the following failure:

File "/venv/lib/python3.7/site-packages/uvicorn/protocols/http/httptools_impl.py", line 390, in run_asgi result = await app(self.scope, self.receive, self.send) File "/venv/lib/python3.7/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__ return await self.app(scope, receive, send) File "/venv/lib/python3.7/site-packages/fastapi/applications.py", line 179, in __call__ await super().__call__(scope, receive, send) File "/venv/lib/python3.7/site-packages/starlette/applications.py", line 111, in __call__ await self.middleware_stack(scope, receive, send) File "/venv/lib/python3.7/site-packages/starlette/middleware/errors.py", line 181, in __call__ raise exc from None File "/venv/lib/python3.7/site-packages/starlette/middleware/errors.py", line 159, in __call__ await self.app(scope, receive, _send) File "/venv/lib/python3.7/site-packages/starlette/exceptions.py", line 82, in __call__ raise exc from None File "/venv/lib/python3.7/site-packages/starlette/exceptions.py", line 71, in __call__ await self.app(scope, receive, sender) File "/venv/lib/python3.7/site-packages/starlette/routing.py", line 566, in __call__ await route.handle(scope, receive, send) File "/venv/lib/python3.7/site-packages/starlette/routing.py", line 227, in handle await self.app(scope, receive, send) File "/venv/lib/python3.7/site-packages/starlette/routing.py", line 41, in app response = await func(request) File "/venv/lib/python3.7/site-packages/fastapi/routing.py", line 176, in app dependency_overrides_provider=dependency_overrides_provider, File "/venv/lib/python3.7/site-packages/fastapi/dependencies/utils.py", line 552, in solve_dependencies solved = await run_in_threadpool(call, **sub_values) File "/venv/lib/python3.7/site-packages/starlette/concurrency.py", line 34, in run_in_threadpool return await loop.run_in_executor(None, func, *args) File "/usr/local/lib/python3.7/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) TypeError: unhashable type: 'MySettings'

Environment

  • OS: [e.g. Linux / Windows / macOS]: MACOS
  • FastAPI Version = "0.61.0"
  • Python version: 3.7.3
@edmondop edmondop added the question Question or problem label Aug 28, 2020
@old-syniex
Copy link

old-syniex commented Aug 29, 2020

Depends by default cache the dependency.

Can you check if

from fastapi import FastAPI

app = FastAPI()

class MySettings(BaseSettings):
    password: SecretStr
    base_url: AnyHttpUrl = "https://www.google.com"
    localdownload_dir: str = "~/mydir-import"

    class Config:
        env_prefix = "my_prefix"

def get_settings() -> MySettings:
    return MySettings()

   
@app.get("/")
def read_root(settings:MySettings = Depends(get_settings,use_cache=False)):
    return {"Hello": "World"}

works?

@edmondop
Copy link
Author

No, it keeps failing unfortunately

@old-syniex
Copy link

old-syniex commented Aug 29, 2020

Can you please explain what you're trying to achieve?

i just tested it myself, and i don't get any error.

what version of FastAPI and pydantic are you using?

@edmondop
Copy link
Author

Damn, I had an @lru_cache on top of a method, that was causing the caching

@tiangolo
Copy link
Owner

Thanks for the help here @syniex ! 👏 🙇

Thanks for reporting back and closing the issue @edmondo1984 👍

@cbenz
Copy link

cbenz commented Jan 21, 2021

This documentation section shows that we can use a dependency with Depends which uses @lru_cache. I followed this docs and had the same problem described by this issue.

Should the docs be updated to remove the usage of @lru_cache or can it be useful in some cases?

@escaped
Copy link

escaped commented Feb 9, 2021

Just ran into the same issue. Looks like pydantic does not implement a __hash__() method for its models as of now: pydantic/pydantic#1881. So, I guess we should update the documentation, or do I miss something? (edit: see next comment)

I guess, in general, it makes sense to document the use of lru_cache, as otherwise, the dependency generator is called for every request instead of just once. Additionally, it might be useful to explicitly state that lru_cache only works for hashable objects.

Anyway, we can work around that issue for now by either let fastapi call the method multiple times, implement a __hash__() method for the Settings class or implement a custom caching mechanism, eg.

def get_settings() -> config.Settings:
    cache = getattr(get_settings, "__cached", None)
    if not cache:
        settings = config.Settings()
        get_settings.__cache = settings
    return get_settings.__cache

@escaped
Copy link

escaped commented Feb 9, 2021

Nevermind, I found the issue for my case. lru_cache only hashes the function arguments. So, the example in the docs works and is correct. I just ran into an issue, because I built a tree of dependencies, where one of the arguments is a Settings instance:

...
@lru_cache()
def get_settings() -> config.Settings:
    return config.Settings()


@lru_cache()  # this does not work, due to the unhashable argument
def get_foo(
    settings: config.Settings = Depends(get_settings),  # noqa: B008
) -> Foo:
    return Foo(settings.bar)

@app.get("/foo")
def read_foo(foo: Foo = Depends(get_foo),  # noqa: B008) :
    ...
...

So, the error is raised as soon as get_foo() is initialised, as the settings argument is not hashable. Anyway, the possible solutions I provided in my previous answer still apply :)

@efung
Copy link

efung commented Jun 25, 2021

    cache = getattr(get_settings, "__cached", None)

@escaped The argument to getattr should be "__cache" here. Just got bit by this bug, as it made the getter method always have a cache miss!

@stevanmilic
Copy link

To clarify for anyone asking the @cbenz question.

Should the docs be updated to remove the usage of @lru_cache or can it be useful in some cases?

No, @lru_cache is still needed, caching on the Depends class is only used for sub-dependencies caching i.e. it only caches the result on the same request in a single request/response cycle. Check the tests for the PR that introduced caching on Depends.

@babymastodon
Copy link

In case anybody else runs into the same problem, I transformed @escaped 's solution into a decorator, with some additional input from various google searches. So far, it seems to fix the problem.

FN = TypeVar('FN', bound=Callable)                                                                                                                                                                                    
                                                                                                                                                                                                                      
                                                                                                                                                                                                                      
def do_once(fn: FN) -> FN:                                                                                                                                                                                           
    lock = Lock()                                                                                                                                                                                                     
    sentinel = object()  # in case the wrapped method returns None                                                                                                                                                        
    cache = sentinel                                                                                                                                                                                                  
                                                                                                                                                                                                                      
    @wraps(fn)                                                                                                                                                                                                        
    def inner(*args, **kwargs):                                                                                                                                                                                       
        nonlocal cache                                                                                                                                                                                                
        with lock:                                                                                                                                                                                                    
            if cache is sentinel:                                                                                                                                                                                     
                cache = fn(*args, **kwargs)                                                                                                                                                                           
        return cache                                                                                                                                                                                                  
                                                                                                                                                                                                                      
    return cast(FN, inner)

@do_once
def get_settings() -> config.Settings:
    config.Settings()

@SdgJlbl
Copy link

SdgJlbl commented Oct 25, 2022

A simple way to fix the issue, at least in some cases:

class Settings(BaseSettings):
    class Config:
        frozen = True

When defining your Settings class, define it as frozen. This way, it will be hashable, so other deps functions requiring a Settings objects can themselves have a lru_cache.

@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #7266 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

9 participants