Skip to content

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

@shaunc

Description

@shaunc

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"

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions