Components and providers isolation

Problem definition

As you know, container can be created from multiple providers, which are dynamically bound together. It allows you to reuse them or partially override in tests. It works well while you have different types across all provided objects. But what if there are some intersections. Let’s talk about three situations:

  1. Only several types are used with different meaning within a monolithic app.

  2. Several parts of an application are developed more or less independently, while they are used within same processing context.

  3. You have a modular application with multiple bounded contexts.

First situation can appear when you have for-example multiple thread pools for different tasks or multiple database connections for different databases. While they have special meaning you distinguish them by creating new types

from typing import NewType

MainDbConnection = NewType("MainDbConnection", Connection)

Once you have different types dishka can now understand which one is used in each place.

In the third situation you actually have mini-applications inside a bigger one with their own scopes and event lifecycle. So just create multiple containers.

The different thing is when you have a bunch of different types and you do not want or even cannot replace them with new types as in p.1. For this case we have a different concept - components.

Component

Component - is an isolated group of providers within the same container identified by a string. There is always a default component (DEFAULT_COMPONENT="").

Component is set for the whole provider, but single provider can be used in multiple components using .to_component(name).

from dishka import make_container, Provider

# default component is used when not specified
provider0 = Provider()

class MyProvider(Provider):
    # component can be set in class
    component = "component_name"

provider1 = MyProvider()

# component can be set on instance creation
provider2 = MyProvider(component="other")

# same provider instance is casted to use with different component
provider3 = provider2.to_component("additional")

container = make_container(provider0, provider1, provider2, provider3)

Components are isolated: provider cannot implicitly request an object from another component:

from dishka import Provider, Scope, make_container, provide

class DBConnection(Protocol): ...
class UserDBConnection(DBConnection): ...
class CommentDBConnection(DBConnection): ...
# we might use different databases for users and comments,
# although the interface for communication will remain common

class UserDAO:
    def __init__(self, db: DBConnection): ...
    # we need to distinguish this DBConnection ...

class CommentDAO:
    def __init__(self, db: DBConnection): ...
    # ... from this DBConnection

class UserProvider(Provider):
    component = "user"
    scope = Scope.APP  # should be REQUEST, but set to APP for the sake of simplicity
    db_connection = provide(UserDBConnection, provides=DBConnection)
    dao = provide(UserDAO)

class CommentProvider(Provider):
    component = "comment"
    scope = Scope.APP  # should be REQUEST, but set to APP for the sake of simplicity
    db_connection = provide(CommentDBConnection, provides=DBConnection)
    dao = provide(CommentDAO)

container = make_container(UserProvider(), CommentProvider())
container.get(DBConnection, component="user")  # UserDBConnection
container.get(DBConnection, component="comment")  # CommentDBConnection

In the following code MainProvider.foo requests integer value which is only provided in separate component. In the code below there is an error in dependency graph, so we will disable validation to show runtime behavior:

from dishka import make_container, Provider, provide, Scope

class MainProvider(Provider):
    # default component is used here

    @provide(scope=Scope.APP)
    def foo(self, a: int) -> float:
        return a / 10


class AdditionalProvider(Provider):
    component = "X"

    @provide(scope=Scope.APP)
    def foo(self) -> int:
        return 1

# we will get error immediately during container creation, skip validation for demo needs
container = make_container(MainProvider(), AdditionalProvider(), skip_validation=True)
# retrieve from component "X"
container.get(int, component="X")  # value 1 would be returned
# retrieve from default component
container.get(float)  # raises NoFactoryError because int is in another component

If the same type is provided in multiple components, it is searched only within the same component as its dependant, unless it is declared explicitly.

Components can link to each other: each provider can add a component name when declaring a dependency by FromComponent type annotation.

from typing import Annotated
from dishka import FromComponent, make_container, Provider, provide, Scope

class MainProvider(Provider):

    @provide(scope=Scope.APP)
    def foo(self, a: Annotated[int, FromComponent("X")]) -> float:
        return a / 10


class AdditionalProvider(Provider):
    component = "X"

    @provide(scope=Scope.APP)
    def foo(self) -> int:
        return 1

container = make_container(MainProvider(), AdditionalProvider())
container.get(float)  # returns 0.1

You can use Annotated[T, FromComponent(...)] in factory return type to provide this factory in specified component, instead of marking component = "component_name" in provider itself:

from typing import Annotated
from dishka import FromComponent, make_container, Provider, provide, Scope

class MainProvider(Provider):

    @provide(scope=Scope.APP)
    def foobar(self, a: Annotated[int, FromComponent("X")]) -> float:
        return a/10

    @provide(scope=Scope.APP)
    def foo(self) -> Annotated[int, FromComponent("X")]:
        return 1

container = make_container(MainProvider())
container.get(float)  # returns 0.1

Warning

Although dishka allows such declarations, it is considered to be a bad practice, because it allows you to mix factories related to different components in one provider. Use it only if your components consist of max of 1-2 factories and split them in every other case.

alias now can be used across components without changing the type:

a = alias(int, component="X")

Note

In frameworks integrations FromDishka[T] is used to get an object from default component. To use other component you can use the same syntax with annotated Annotated[T, FromComponent("X")].