.. _components: 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 .. code-block:: python 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)``. .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python 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: .. code-block:: python 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")]``.