Skip to main content

Python dependency injection library based on type hints

Project description

Inseminator

codecov Code style: black

(definition from dictionary.com)

a technician who introduces prepared semen dependencies into the genital tract of breeding animals python classes, especially cows and mares pure classes with proper IoC, for artificial insemination well 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.

Installation

Install using the pip tool.

pip install inseminator

Usage

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.

  1. We ask the container to resolve a dependency Controller -> container.resolve(Controller).
  2. Resolver inside the container checks the Controller's constructor signature, i.e. type hints of __init__ method and sees domain_models: DomainModel.
  3. If an instance of DomainModel class is already known by the container it 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.
  4. Because DomainModel doesn't have any dependencies it can construct it directly.
  5. Now the resolver has all the dependencies for Controller constructor 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. Using

container.register(DomainModel, value=ConcreateDomainModel())

we're guiding the resolver to use instance of ConcreateDomainModel in case someone asks for DomainModel.

Enforced parameters

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 container.resolve 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.

Injecting functions

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: Dependency = Depends(Dependency)):
    return input_value + dependency.x

Used like that, my_handler takes a single argument and thanks to closure it has dependency prepared with the right instance of Dependency.

>>> my_handler(1)
2

Default parameter values

When default parameters are specified the resolver uses them unless we override that value by enforced parameter.

def MyDependency:
    def __init__(self, parameter: int = 1) -> None:
        self.parameter = parameter

my_dependency = container.resolve(MyDependency)
assert my_dependency.parameter == 1

Docs

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

inseminator-0.4.7.tar.gz (7.1 kB view details)

Uploaded Source

Built Distribution

inseminator-0.4.7-py3-none-any.whl (8.8 kB view details)

Uploaded Python 3

File details

Details for the file inseminator-0.4.7.tar.gz.

File metadata

  • Download URL: inseminator-0.4.7.tar.gz
  • Upload date:
  • Size: 7.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.11 CPython/3.10.0 Linux/5.11.0-1020-azure

File hashes

Hashes for inseminator-0.4.7.tar.gz
Algorithm Hash digest
SHA256 b82b7adf07aff48971b90824d7a5e68e6f89308c2195821fd55300f6ab6031b1
MD5 c69c3a4b487b4e1caea67b2d42fddf21
BLAKE2b-256 f537faeeaf2730d0e73bbbf5b74b485a206fb8e3234d31f94db3b03548bee6f3

See more details on using hashes here.

File details

Details for the file inseminator-0.4.7-py3-none-any.whl.

File metadata

  • Download URL: inseminator-0.4.7-py3-none-any.whl
  • Upload date:
  • Size: 8.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.1.11 CPython/3.10.0 Linux/5.11.0-1020-azure

File hashes

Hashes for inseminator-0.4.7-py3-none-any.whl
Algorithm Hash digest
SHA256 221682d4db3f5b92174f0ee1b81dc11e11c71618383a8a03e022fe6dcd991629
MD5 c44fde8cbe6b17233a4cb8420585224d
BLAKE2b-256 83d4496b5ff885d205a15e4c95b24d25a049abf2b43e83dec447469fa2a9b8e4

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page