Feature request: allow specification of types of DependencyContainer() providers
See original GitHub issueContainers 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"
Issue Analytics
- State:
- Created 3 years ago
- Comments:11 (8 by maintainers)
Top GitHub Comments
This example now works with
4.9.0
:Good. Thanks for the update.