python-dependency-injector icon indicating copy to clipboard operation
python-dependency-injector copied to clipboard

What is the proper way to handle dependencies at module load time?

Open cpvandehey opened this issue 1 year ago • 2 comments

Hello all! Im having a fun time experimenting with this framework, but have found myself stumped on this design issue for a while. In short, if a dependency needs to be used at module load time (e.g. decorators), the init_resources/wiring breaks

Example

containers.py

class ApplicationContainer(containers.DeclarativeContainer):
    my_dependency = providers.Singleton(myClass)
    my_decorator = providers.Callable(my_dynamic_decorator_function, my_dependency)

main.py

application_container = dependency_containers.ApplicationContainer()
application_container.init_resources()
application_container.wire(modules=module_names, packages=packages)

some_dangerous_file.py

from . import containers

@containers.ApplicationContainer.my_decorator()
def my_function():
  print("...")

dependency_injector.errors.Error: Can not copy initialized resource

This is notably caused by the way/order python loads modules. All decorators are applied before the AppContainer or main can properly wire/initialize the dependencies. This basically means that if there is a dependency that is used outside a function, it will fail.

Is there any design or trick to get around this, i'd love to hear it. I don't like the idea of putting any container initialization into the dunder init file.

Here are some of my thoughts: If there was a dependency/provider type that wraps a dependency that doesn't need to be available immediately i.e. lazy initialization, the module load would work at bootup and would be properly injected when it needs to be (after main executes)

cpvandehey avatar Mar 06 '23 23:03 cpvandehey

I've encountered this before; needing a decorator that depends on the initilisation of something within the container. The issue is to do with the order that such objects are initialised, like you said. The decorator is created at the point the class is loaded, not when the class is instantiated. Therefore your decorator has already been created before the init_resources() is called. However, decorators do have access to self and so you can embed your dependency in the class instance. This requires use of an additional decorator to take the dependency from the class instance and then call the decorated function.

I wouldn't recommend this approach as it is quite complicated and hard to read. See here for further discussions of the topic: https://stackoverflow.com/questions/1231950/how-can-i-use-a-class-instance-variable-as-an-argument-for-a-method-decorator-in

If you choose to use this approach, you should be able to modify the below example by injecting whatever dependency you need in to MyClass and then taking that attribute from a modified add_sleep_s_to_kwargs() before finally calling your decorator with said dependency.

from dependency_injector import containers, providers


def add_sleep_s_to_kwargs():
    def dec(fn):
        @wraps(fn)
        def wrapper(self, *args, **kwargs):
            sleep_s = self._config["sleep_s"]
            return fn(self, *args, sleep_s=sleep_s, **kwargs)

        return wrapper

    return dec


def sleep_dec(fn):
    async def _sleep_fn(self, *args, __sleep_time, **kwargs):
        print(f"sleeping for {__sleep_time} at {datetime.datetime.utcnow()}")
        await asyncio.sleep(__sleep_time)
        return await fn(self, *args, **kwargs)

    @wraps(fn)
    def _wrapped(self, *args, **kwargs):
        seconds = kwargs["sleep_s"]
        del kwargs["sleep_s"]
        return _sleep_fn(self, *args, __sleep_time=seconds, **kwargs)

    return _wrapped


class MyClass:
    def __init__(self, config):
        self._config = config

    @add_sleep_s_to_kwargs()
    @sleep_dec
    async def foo(self):
        print(f"foo returning at {datetime.datetime.utcnow()}")


class ApplicationContainer(containers.DeclarativeContainer):
    config = providers.Configuration()
    my_class = providers.Singleton(MyClass, config)


def main():
    container = ApplicationContainer()
    container.config.from_dict({"sleep_s": 2})
    container.init_resources()
    my_class = container.my_class()
    asyncio.run(my_class.foo())


if __name__ == "__main__":
    main()

This prints:

sleeping for 2 at 2023-03-07 16:20:14.235376
foo returning at 2023-03-07 16:20:16.239335

laker-93 avatar Mar 07 '23 16:03 laker-93

Heres the approach that my team used:

class LazyDecoratorProvider:
    """
    If you need a dependency to be used in a decorator, it CANT
    be accessed at module load/import time, therefore a clever/hacky
    abstraction like this needs to wrap a dependency provider.
    """

    def __init__(self, provider: providers.Provider):
        self.provider = provider

    def __call__(self, func: Callable):
        @functools.wraps(func)
        def inner(*args, **kwargs):
            if self.provider:
                return self.provider()(func)(*args, **kwargs)
            return func(*args, **kwargs)

        return inner

In short, we define our dependency like so:

LazyDecoratorProvider(
    dependency_containers.ApplicationContainer.some_dependency_container
)

During import time, our decorated methods simply get the function of inner. This will end up calling inner i.e. the decorated function, when the methods are executed.

IMO, this is valuable enough to make into a provider type itself... Like DecoratorProvider or something along those lines.

cpvandehey avatar Mar 13 '23 19:03 cpvandehey