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

Feature request: allow specification of types of DependencyContainer() providers #357

Closed
shaunc opened this issue Jan 14, 2021 · 11 comments
Closed
Assignees
Labels

Comments

@shaunc
Copy link

shaunc commented Jan 14, 2021

Containers have two types of parameters - dependencies, and dependency containers, which are collections of dependencies, satisfied with a container. They both can be instantiated in two ways: either on container creation (or afterwards with override_providers), or when a derived Container class overrides a dependency or dependency container.

In the latter case, currently at least, a DependenciesContainer has to be provided with explicit dependencies so that other providers binding to them have placeholders for their eventual targets:

class C(DeclarativeContainer):

    f = providers.DependenciesContainer(bar = providers.Dependency(instance_of=str))
    g = f.bar.provided 

For complex DependenciesContainers, this can be onerous, especially if the dependencies themselves container dependencies containers.

I suggest allowing a DependenciesContainer to take as an argument a Container, which specifies the base type of Container that will instantiate the DependenciesContainer:

Below is a snippet that constructs placeholders appropriate for a given incoming container (or even another dependencies container):

"""
Utilities for containers.
"""
from typing import (
    Any,
    Dict,
    Mapping,
    MutableMapping,
    Optional,
    Protocol,
    Type,
    Union,
)

from dependency_injector import containers, providers

ContainerSpec = Dict[str, providers.Provider]
ForwardContainerType = Union[
    Type[containers.DeclarativeContainer], ContainerSpec
]

#: type for DependenciesContainer possibly overridden by Container
FormalContainer = Union["DependenciesContainer", providers.Container]


class DependenciesContainer(providers.DependenciesContainer):
    """
    Dependencies container allowing spec of container to bind.
    """

    def __init__(
        self,
        subclass_of: Optional[ForwardContainerType] = None,
        **kw: Any
    ):
        if subclass_of is not None:
            if issubclass(subclass_of, containers.DeclarativeContainer):  # type: ignore
                spec: ContainerSpec = container_spec(subclass_of)  # type: ignore
            else:
                spec = subclass_of  # type: ignore
        else:
            spec = {}
        spec.update(kw)
        super().__init__(**spec)


class ContainerProtocol(Protocol):
    providers: Mapping[
        str,
        Union[providers.DependenciesContainer, providers.Dependency],
    ]


def container_spec(
    container: ContainerProtocol,
) -> ContainerSpec:
    """
    Create spec for container.

    Recurse over container and
    """

    spec: ContainerSpec = {}
    for name, provider in container.providers.items():
        if isinstance(
            provider,
            (providers.DependenciesContainer, providers.Dependency),
        ):
            spec[name] = provider
        else:
            spec[name] = providers.Dependency(instance_of=object)
    return spec


def resolve_part(c: Any, part: str) -> Any:
    """
    Resolve a part of a container.

    Helper to work around: https://github.com/ets-labs/python-dependency-injector/issues/354

    As `DependenciesContainer` can't be passed, have converted
    into a `Dependency`. However, that makes things "too lazy",
    as we have to use <container>.provided.<part>.provided, and
    consumer must do <container-instance>()().

    Instead, we take first provided "lazy" and resolve last greedily
    """
    return providers.Callable(
        lambda c: getattr(c, part)(), c.provided
    ).provided

It doesn't check the type of a passed in container on instantiation -- that is nice to have, but not as essential for me.

It can be used so:

from .container_util import DependenciesContainer, resolve_part

class C1(containers.DeclarativeContainer):
    foo1 = Dependency(instance_of=str)
    foo2 = Dependency(instance_of=str)

class C2(containers.DeclarativeContainer):
    c1 = DependenciesContainer(C1)

class C3(containers.DeclarativeContainer):
    c2 = DependenciesContainer(C2)
    foo1 = resolve_part(resolve_part(c2, "c1"), "foo1")
    foo2 = resolve_part(resolve_part(c2, "c1"), "foo2")

c3 = C3(c2 = C2(c1 = C1(foo1 = "hello", foo2="world")))

print(c3.foo1(), c3.foo2())  # prints "hello world"

Also, this should work (or something similar):

...
@containers.copy(C3)
class C4(C3):
    selected_c1 = providers.Container(C1, foo1 = "hello", foo2="world")
    c2 = DependenciesContainer(C2)
    c2.override(providers.Callable(C2, c1=selected_c1))

c4 = C4()
print(c4.foo1(), c4.foo2())  # prints "hello world"
@rmk135 rmk135 self-assigned this Jan 14, 2021
@rmk135
Copy link
Member

rmk135 commented Jan 14, 2021

Hey @shaunc, next works with current codebase:

from dependency_injector import containers, providers

from container_utils import resolve_part


class C1(containers.DeclarativeContainer):
    foo1 = providers.Dependency(instance_of=str)
    foo2 = providers.Dependency(instance_of=str)


class C2(containers.DeclarativeContainer):
    c1 = providers.DependenciesContainer(**C1.providers)


class C3(containers.DeclarativeContainer):
    c2 = providers.DependenciesContainer(**C2.providers)
    foo1 = resolve_part(resolve_part(c2, "c1"), "foo1")
    foo2 = resolve_part(resolve_part(c2, "c1"), "foo2")


if __name__ == '__main__':
    c3 = C3(c2=C2(c1=C1(foo1="hello", foo2="world")))

    print(c3.foo1(), c3.foo2())  # prints "hello world"

Also there is a version without resolve_part():

from dependency_injector import containers, providers


class C1(containers.DeclarativeContainer):
    foo1 = providers.Dependency(instance_of=str)
    foo2 = providers.Dependency(instance_of=str)


class C2(containers.DeclarativeContainer):
    c1 = providers.DependenciesContainer(**C1.providers)


class C3(containers.DeclarativeContainer):
    c2 = providers.DependenciesContainer(**C2.providers)
    foo1 = c2.c1.foo1
    foo2 = c2.c1.foo2


if __name__ == '__main__':
    c3 = C3(c2=C2(c1=C1(foo1="hello", foo2="world")))

    print(c3.foo1(), c3.foo2())  # prints "hello world"

@rmk135
Copy link
Member

rmk135 commented Jan 14, 2021

Example with @copy works with minor modifications in c2.override():

from dependency_injector import containers, providers


class C1(containers.DeclarativeContainer):
    foo1 = providers.Dependency(instance_of=str)
    foo2 = providers.Dependency(instance_of=str)


class C2(containers.DeclarativeContainer):
    c1 = providers.DependenciesContainer(**C1.providers)


class C3(containers.DeclarativeContainer):
    c2 = providers.DependenciesContainer(**C2.providers)
    foo1 = c2.c1.foo1
    foo2 = c2.c1.foo2


@containers.copy(C3)
class C4(C3):
    selected_c1 = providers.DependenciesContainer(foo1="hello", foo2="world")
    c2 = providers.DependenciesContainer(**C2.providers)
    c2.override(C2(c1=selected_c1))


if __name__ == '__main__':
    c4 = C4()
    print(c4.foo1(), c4.foo2())  # prints "hello world"

@rmk135
Copy link
Member

rmk135 commented Jan 14, 2021

I guess the only missing part here is that C1.providers returns dictionary of all C1 providers, not only Dependency and DependencyContainer providers. That's easy to extend. I can add a property to all container types:

    @property
    def dependencies(cls):
        """Return container dependency providers dictionary.

        Dependency providers could be both of :py:class:`dependency_injector.providers.Dependency` and
        :py:class:`dependency_injector.providers.DependenciesContainer`.

        :rtype:
            dict[str, :py:class:`dependency_injector.providers.Provider`]
        """
        return {
            name: provider
            for name, provider in cls.providers.items()
            if isinstance(provider, (Dependency, DependenciesContainer))
        }

In that case the code will look like:

from dependency_injector import containers, providers


class C1(containers.DeclarativeContainer):
    foo1 = providers.Dependency(instance_of=str)
    foo2 = providers.Dependency(instance_of=str)


class C2(containers.DeclarativeContainer):
    c1 = providers.DependenciesContainer(**C1.dependencies)


class C3(containers.DeclarativeContainer):
    c2 = providers.DependenciesContainer(**C2.dependencies)
    foo1 = c2.c1.foo1
    foo2 = c2.c1.foo2


if __name__ == '__main__':
    c3 = C3(c2=C2(c1=C1(foo1="hello", foo2="world")))

    print(c3.foo1(), c3.foo2())  # prints "hello world"

What do you think?

@shaunc
Copy link
Author

shaunc commented Jan 15, 2021

Looks great -- but where is dependencies used? Did you mean:

@dependencies
class C1( ... ):
    ...

@dependencies
class C2( ... ):
   ...

?
UPDATE Ah -- never mind -- you added to DeclarativeContainer -- (sorry -- low on sleep :)).
So ... without reservations -- looks great! 👍

@rmk135
Copy link
Member

rmk135 commented Jan 15, 2021

Ok, cool. Working on it.

@rmk135
Copy link
Member

rmk135 commented Jan 15, 2021

I've released version 4.9.0 with .dependencies attribute for the DeclarativeContainer and DynamicContainer.

@rmk135 rmk135 added feature and removed question labels Jan 15, 2021
@rmk135
Copy link
Member

rmk135 commented Jan 15, 2021

This example now works with 4.9.0:

from dependency_injector import containers, providers


class C1(containers.DeclarativeContainer):
    foo1 = providers.Dependency(instance_of=str)
    foo2 = providers.Dependency(instance_of=str)


class C2(containers.DeclarativeContainer):
    c1 = providers.DependenciesContainer(**C1.dependencies)


class C3(containers.DeclarativeContainer):
    c2 = providers.DependenciesContainer(**C2.dependencies)
    foo1 = c2.c1.foo1
    foo2 = c2.c1.foo2


if __name__ == '__main__':
    c3 = C3(c2=C2(c1=C1(foo1="hello", foo2="world")))

    print(c3.foo1(), c3.foo2())  # prints "hello world"

@shaunc
Copy link
Author

shaunc commented Jan 15, 2021

Great! Thanks so much. Yes -- I think it will help with the other issues as well. (Will have to unwind my workarounds -- will try later today.)

@rmk135
Copy link
Member

rmk135 commented Jan 15, 2021

Ok, sounds good. Waiting for your feedback from the field.

@shaunc
Copy link
Author

shaunc commented Jan 18, 2021

Everything good on this one -- Thanks!

@shaunc shaunc closed this as completed Jan 18, 2021
@rmk135
Copy link
Member

rmk135 commented Jan 18, 2021

Good. Thanks for the update.

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

No branches or pull requests

2 participants