modern-python / that-depends

DI-framework, inspired by python-dependency-injector, but without wiring. Python 3.12 is supported
https://that-depends.readthedocs.io/
MIT License
156 stars 12 forks source link

Singleton providers broken in FastAPI with 1.13.0 #47

Closed rmasters closed 4 months ago

rmasters commented 4 months ago

Singleton providers appear to be broken when used as FastAPI dependencies in 1.13.0 (working in 1.12.0).

This appears to only affect direct dependencies: when a Singleton is used as a sub-dependency in a non-Singleton provider, I don't get an error.

Test case

from datetime import datetime
from pathlib import Path
from typing import Annotated, Iterator

from that_depends import BaseContainer
from that_depends.providers import Singleton, Factory, Resource
from fastapi import FastAPI, Depends

def path_resource() -> Iterator[Path]:
    yield Path(__file__).parent

class Container(BaseContainer):
    singleton: Singleton[Path] = Singleton(lambda: Path(__file__).parent)
    resource: Resource[Path] = Resource(path_resource)
    factory: Factory[datetime] = Factory(datetime.now)

app = FastAPI()

@app.get("/")
async def handler(
    path_singleton: Annotated[Path, Depends(Container.singleton)],
    #path_resource: Annotated[Path, Depends(Container.resource)],
    #factory: Annotated[datetime, Depends(Container.factory)],
) -> None:
    return

Expected

path_singleton is an instance of Path

Actual

TypeError: unexpected object <that_depends.providers.attr_getter.AttrGetter object at 0x105c1f690> in __signature__ attribute
sys:1: RuntimeWarning: coroutine 'AbstractProvider.__call__' was never awaited

Resource and Factory work as expected.

Full stacktrace ``` Traceback (most recent call last): File "/Users/ross/that-depends-testcase/test.py", line 23, in @app.get("/") ^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 944, in decorator self.add_api_route( File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 883, in add_api_route route = route_class( ^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/routing.py", line 513, in __init__ self.dependant = get_dependant(path=self.path_format, call=self.endpoint) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/dependencies/utils.py", line 268, in get_dependant sub_dependant = get_param_sub_dependant( ^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/dependencies/utils.py", line 111, in get_param_sub_dependant return get_sub_dependant( ^^^^^^^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/dependencies/utils.py", line 147, in get_sub_dependant sub_dependant = get_dependant( ^^^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/dependencies/utils.py", line 250, in get_dependant endpoint_signature = get_typed_signature(call) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/ross/that-depends-testcase/.venv/lib/python3.12/site-packages/fastapi/dependencies/utils.py", line 208, in get_typed_signature signature = inspect.signature(call) ^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/ross/.rye/py/cpython@3.12.3/lib/python3.12/inspect.py", line 3310, in signature return Signature.from_callable(obj, follow_wrapped=follow_wrapped, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/ross/.rye/py/cpython@3.12.3/lib/python3.12/inspect.py", line 3054, in from_callable return _signature_from_callable(obj, sigcls=cls, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/ross/.rye/py/cpython@3.12.3/lib/python3.12/inspect.py", line 2530, in _signature_from_callable raise TypeError( TypeError: unexpected object in __signature__ attribute sys:1: RuntimeWarning: coroutine 'AbstractProvider.__call__' was never awaited ```

Environment

Python 3.12.3

that-depends==1.13.0
fastapi==0.111.0
lesnik512 commented 4 months ago

@rmasters, thank you, Ross! Fixed in 1.13.1

graham-atom commented 3 months ago

@lesnik512 I am still seeing this for version 1.13.1 and fastapi 0.111.0:

no signature found for builtin <method-wrapper '__call__' of dependency_injector.providers.Singleton object at 0x112a9cac0>

def init_cognito(
    region: str,
    user_pool_id: str,
    app_client_id: str,
) ->  Iterator[Cognito]:
    """Initialize the cognito client."""
    try:
        yield Cognito(region=region, userPoolId=user_pool_id, client_id=app_client_id)
    finally:
        pass

class CognitoSettings(BaseSettings):
    """Settings for the cognito client."""

    region: str
    user_pool_id: str
    app_client_id: str

def init_settings() -> Iterator[CognitoSettings]:
    """Initialize the settings."""
    try:
        yield CognitoSettings()
    finally:
        pass

class CognitoContainer(BaseContainer):
    """Container for the cognito client."""
    settings: CognitoSettings = providers.Singleton(init_settings)

    cognito: Cognito = providers.Resource(
        init_cognito,
        region=settings.region,
        userPoolId=settings.user_pool_id,
        client_id=settings.app_client_id,
    )

async def cognito_auth(
    bearer_token: HTTPAuthorizationCredentials | None = Depends(HTTPBearer(auto_error=False)),
    cognito: Cognito = Depends(CognitoContainer.cognito),
) -> CurrentUser | None:
    """Authenticate the current user with cognito.

    Args:
        bearer_token (HTTPAuthorizationCredentials): The users bearer token.
        cognito (Cognito): The cognito client.

    Returns:
        CurrentUser | None: The current user if the bearer token exists.

    """
graham-atom commented 3 months ago

actually nevermind, looks like an install issue on my end, sorry for the confusion!