(definition from dictionary.com)
a technician who introduces prepared
semendependencies into the genital tract of breeding animalspython classes, especially cows and marespure classes with proper IoC, for artificial inseminationwell coupled components and clear classes signatures.
Python library for type-based dependency injection. Write code without global state and noisy boilerplate. Inseminator is meant to be used in an entry-point layer of your application and the only thing it requires is properly type-hinted classes dependencies.
Install using the pip tool.
pip install inseminator
You start by defining the container of your dependencies. Whenever you want the container to resolve a dependency, it uses the container to search for existing objects and a resolver automatically creates desired dependencies.
from inseminator import Container class DomainModel: def __init__(self): self.__logic_constant = 1 def domain_logic(self, input_value: int) -> int: return input_value + self.__logic_constant class Controller: def __init__(self, domain_model: DomainModel): self.__domain_model = domain_model def handler(self, input_value: int) -> int: return self.__domain_model.domain_logic(input_value) # entry-point of your application container = Container() # view layer handling controller = container.resolve(Controller) result = controller.handler(1) print(result)
The strategy for resolving
Controller is its constructor signature. The resolver works as follows.
- We ask the
containerto resolve a dependency
- Resolver inside the
Controller's constructor signature, i.e. type hints of
__init__method and sees
- If an instance of
DomainModelclass is already known by the
containerit uses that instance. In the opposite case, the container starts the same resolving machinery for
DomainModel- which is the exact case we are facing now.
DomainModeldoesn't have any dependencies it can construct it directly.
- Now the resolver has all the dependencies for
Controllerconstructor and can instantiate it.
If we programmed against an interface instead of implementation the example is modified like this.
from inseminator import Container from typing import Protocol class DomainModel(Protocol): def domain_logic(self, input_value: int) -> int: ... class Controller: def __init__(self, domain_model: DomainModel): self.__domain_model = domain_model def handler(self, input_value: int) -> int: return self.__domain_model.domain_logic(input_value) # domain model implementation class ConcreteDomainModel: def __init__(self): self.__logic_constant = 1 def domain_logic(self, input_value: int) -> int: return input_value + self.__logic_constant # entry point of your application container = Container() container.register(DomainModel, value=ConcreateDomainModel()) # view layer handling controller = container.resolve(Controller) result = controller.handler(1) print(result)
In this situation, protocol
DomainModel doesn't hold implementation details, only interface.
we're guiding the resolver to use instance of
ConcreateDomainModel in case someone asks
If it is not desired to provide a single concrete implementation for abstract or protocol dependency
one can enforce the resolver to use concrete types for specified parameters. Simply call
also with keywords and tell the resolve how it should resolve some particular parameters.
container = Container() controller = container.resolve(Controller, domain_model=ConcreteDomainModel)
Moreover, using this approach
ConcreteDomainModel is not evaluated and saved in the container but
rather in a sub-container which exists only during the resolving. Therefore, if we want to create
another instance that depends on
DomainModel we must either use
register or again specify
the parameter during resolving.
It might be convinient to specify funcion's dependencies in-place. The great example is Flask
handler function. It should live in the same layer the DI container lives because it provides
only infrastructure functionality and desirably the only thing it does it calling domain layer's
functions. For this purpose, there is
injector decorator on the
Container object. You just
tell which dependency to provide using
Depends type constructor.
from inseminator import Container, Depends class Dependency: def __init__(self): self.x = 1 container = Container() @container.inject def my_handler(input_value: int, dependency: Depends(Dependency)): return input_value + dependency.x
Used like that,
my_handler takes a single argument and thanks to closure it has
prepared with the right instance of
>>> my_handler(1) 2