Skip to content
New issue

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

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

Already on GitHub? # to your account

provider.Callable not working when being passed to another provider in a Container #704

Closed
TomaszBorczyk opened this issue May 5, 2023 · 2 comments

Comments

@TomaszBorczyk
Copy link

TomaszBorczyk commented May 5, 2023

Hi,

I've encountered a problem in our project with Callable providers and nailed the problem down to this scenario.
Namely, when we have a Factory and we want to use it in another Factory, we can do it easily as such:

some_service_factory = providers.Factory(SomeService, a=1)
other_service_factory = providers.Factory(OtherService, service=some_service_factory(b=2))

Doing something similar with Callable provider fails in runtime with TypeError: some_callable() missing 1 required positional argument: 'input_value'. Full code that causes errors is pasted below.

Code is run as python ./main.py and then entering localhost:8888 in the browser.

import functools

import uvicorn
from dependency_injector.wiring import inject, Provide
from fastapi import FastAPI, APIRouter, Depends

from dependency_injector import containers, providers


def some_callable(configurable_parameter, input_value):
    return configurable_parameter + input_value


class Service:
    def __init__(self, callback):
        self.some_callable = callback

    def greet(self):
        return self.some_callable(input_value=" world")


class AppContainer(containers.DeclarativeContainer):
    callable_instance = providers.Callable(some_callable, configurable_parameter="hello")

    service = providers.Singleton(
        Service,
        callback=callable_instance
        # below works
        # callback=functools.partial(some_callable, configurable_parameter="hello")
    )


router = APIRouter()


@router.get("/")
@inject
async def read_root(
        service=Depends(Provide[AppContainer.service])
):
    return {"message": service.greet()}


def create_app():
    container = AppContainer()
    container.wire(modules=[__name__])
    app = FastAPI()
    app.container = container

    app.include_router(router)

    return app


if __name__ == "__main__":
    uvicorn.run(
        f"{__name__}:{create_app.__name__}",
        host="0.0.0.0",
        port=8888,
        log_level="info",
        reload=True,
        factory=True,
    )

Pip freeze output:

anyio==3.6.2
click==8.1.3
dependency-injector==4.41.0
fastapi==0.95.1
h11==0.14.0
idna==3.4
pydantic==1.10.7
six==1.16.0
sniffio==1.3.0
starlette==0.26.1
typing_extensions==4.5.0
uvicorn==0.22.0

As you can see, I want to use below code:

 callable = providers.Callable(some_callable, configurable_parameter="hello")
 service = providers.Singleton(
       Service,
       callback=callabl
        # below work
        # callback=functools.partial(some_callable, configurable_parameter="hello"
    )

It fails with that dependency-injector trace:

File "/Users/tb/home/depenency_injector_fastapi_bug/.venv/lib/python3.10/site-packages/dependency_injector/wiring.py", line 994, in _patched
    return await _async_inject(
  File "src/dependency_injector/_cwiring.pyx", line 53, in _async_inject
  File "src/dependency_injector/providers.pyx", line 225, in dependency_injector.providers.Provider.__call__
  File "src/dependency_injector/providers.pyx", line 3049, in dependency_injector.providers.Singleton._provide
  File "src/dependency_injector/providers.pxd", line 650, in dependency_injector.providers.__factory_call
  File "src/dependency_injector/providers.pxd", line 577, in dependency_injector.providers.__call
  File "src/dependency_injector/providers.pxd", line 445, in dependency_injector.providers.__provide_keyword_args
  File "src/dependency_injector/providers.pxd", line 365, in dependency_injector.providers.__get_value
  File "src/dependency_injector/providers.pyx", line 225, in dependency_injector.providers.Provider.__call__
  File "src/dependency_injector/providers.pyx", line 1339, in dependency_injector.providers.Callable._provide
  File "src/dependency_injector/providers.pxd", line 635, in dependency_injector.providers.__callable_call
  File "src/dependency_injector/providers.pxd", line 608, in dependency_injector.providers.__call
TypeError: some_callable() missing 1 required positional argument: 'input_value'

When I replace providers.Callable with functools.partial it works, but it is definitely not what I want, as this is just a simple example and in real project it can get more complicated, with Callable being provided with different dependencies etc. I use Singleton here, but it might as well be a Factory or other provider.

In the scenario when I need to reuse Callable in the container and provide it with some dependencies, it does not work (as per above error), I cannot use funtools.partial (as it cannot be injected with dependencies), and need to convert the function that I want to be provided to a class that just implements a single method, only for it to possible to use providers.Factory instead of providers.Callable, which I find being a huge antipattern.

I am sure though I am missing something and would love some direction

How can I make it work?

Thanks

@TomaszBorczyk TomaszBorczyk changed the title provider.Callable not working when passing to another provider in a Container provider.Callable not working when being passed to another provider in a Container May 6, 2023
@kkjot88
Copy link

kkjot88 commented May 15, 2023

If u use providers.Callable and the whole wiring machinery, said callable will be called upon injecting ur service, consider:

from dependency_injector import containers, providers
from dependency_injector.wiring import Provide


def some_callable(configurable_parameter, input_value):
    return configurable_parameter + input_value


class Service:
    def __init__(self, callback):
        self.some_callable = callback

    def greet(self):
        return self.some_callable(input_value=" world")


class AppContainer(containers.DeclarativeContainer):
    callable_instance = providers.Callable(
        some_callable, configurable_parameter="hello", input_value="_world"
    )

    service = providers.Singleton(Service, callback=callable_instance)


service: Service = Provide["service"]

container = AppContainer()
container.wire(modules=[__name__])

print(service.some_callable)

trace:

.../dependency_injector/main.py
hello_world

Process finished with exit code 0

AFAIK if u want to pass the callable isntance further down the providers chain u have to pass callable.provider property like this:

def some_callable(configurable_parameter, input_value):
    return configurable_parameter + input_value


class Service:
    def __init__(self, callback):
        self.some_callable = callback

    def greet(self):
        return self.some_callable(input_value=" world")


class AppContainer(containers.DeclarativeContainer):
    callable_instance = providers.Callable(
        some_callable, configurable_parameter="hello"
    )
    service = providers.Singleton(Service, callback=callable_instance.provider)


service: Service = Provide["service"]

container = AppContainer()
container.wire(modules=[__name__])

print(service.some_callable)
print(service.greet())
.../dependency_injector/main.py
<dependency_injector.providers.Callable(<function some_callable at 0x7f84b3b63d90>) at 0x7f84b1fc7b50>
hello world

Process finished with exit code 0

@TomaszBorczyk
Copy link
Author

Thanks! It works
My initial approach (which didn't work, as described in the ticket) was based on the documentation: https://python-dependency-injector.ets-labs.org/providers/callable.html
There, in container there is initialization of provider:
password_verifier = providers.Callable(passlib.hash.sha256_crypt.verify)

and then it is used as

hashed_password = container.password_hasher("super secret")
assert container.password_verifier("super secret", hashed_password)

which suggests it should work as I initially tried. It completely flew over my head that indeed Provide was not used in the documentation example. I will be honest, I spent embarrassing amount of time to make it work, with no results. I think the case I presented here (have a callable that in container we inject some common configuration, and then clients are provided with it so they can deliver the rest of parameters) is quite common use case (an equivalent of functools.partial)

I will try to update the documentation to have this example.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants