enterprython

Type-based dependency injection


License
MIT
Install
pip install enterprython==0.6.1

Documentation

logo

CI (License MIT 1.0)

enterprython

Python library providing type-based dependency injection

Table of contents

Introduction

If you plan to develop SOLID / domain-driven (i.e., enterprisey) software, you probably want to apply inversion of control in the form of dependency injection when writing the constructors of your classes. Also you likely want to use a library doing the needed lookups for you based on static type annotations, instead of manually configuring the object graph.

enterprython provides exactly that.

from enterprython import assemble, component

@component()
class Service:
    def __init__(self) -> None:
        self._greeting: str = 'Hello'

    def greet(self, name: str) -> str:
        return f'{self._greeting}, {name}!'

class Client:
    def __init__(self, service: Service) -> None:
        self._service = service

    def run(self) -> None:
        print(self._service.greet('World'))


assemble(Client).run()

Output:

Hello, World!

Features

Abstract base classes and profiles

A client may depend on an abstract base class. Enterprython will inject the matching implementation.

from abc import ABC
from enterprython import assemble, component

class ServiceInterface(ABC):
    ...

@component()
class ServiceImpl(ServiceInterface):
    ...

class Client:
    def __init__(self, services: ServiceInterface) -> None:
        ...

assemble(Client)

One singleton instance of ServiceImpl is created and injected into Client.

This feature enables the use of different profiles. For example, you might want to use different classes implementing an interface for your production environment compared when running integration tests. By providing a profiles list, you can limit when the component is available.

@component(profiles=['prod'])
class ServiceImpl(ServiceInterface):
    ...
    
@component(profiles=['test'])
class ServiceMock(ServiceInterface):
    ...

assemble(Client, profile='test')

Factories

Annotating a function with @factory() registers a factory for its return type.

from enterprython import assemble, component

class Service:
    ...

@factory()
def service_factory() -> Service:
    return Service()

class Client:
    def __init__(self, service: Service) -> None:
        ...

assemble(Client)

service_factory is used to create the Service instance for calling the constructor of Client.

Non-singleton services

If a service is annotated with @component(singleton=False) a new instance of it is created with every injection.

@component(singleton=False)
class Service:
    ...

class Client:
    def __init__(self, service: Service) -> None:
        ...

Service lists

A client may depend on a list of implementations of a service interface.

from abc import ABC
from typing import List
from enterprython import assemble, component

class ServiceInterface(ABC):
    pass

@component()
class ServiceA(ServiceInterface):
    ...

@component()
class ServiceB(ServiceInterface):
    ...

class Client:
    def __init__(self, services: List[ServiceInterface]) -> None:
        ...

assemble(Client)

[ServiceA(), ServiceB()] is injected into Client.

Mixing managed and manual injection

One part of a client's dependencies might be injected manually, the rest automatically.

from enterprython import assemble, component

@component()
class ServiceA:
    ...

class ServiceB:
    ...

class Client:
    def __init__(self, service_a: ServiceA, service_b: ServiceB) -> None:
        ...

assemble(Client, service_b=ServiceB())

service_a comes from the DI container, service_b from user code.

If ServiceB also has a @component() annotation, the manually provided object is preferred.

Free functions as clients

Since class constructors are fundamentally just normal functions, we can inject dependencies into free functions too.

from enterprython import assemble, component

@component()
class Service:
    ...

def client(service: Service) -> None:
    ...

assemble(client)

A singleton instance of Service is created and used to call client.

Requirements and Installation

You need Python 3.6.5 or higher.

python3 -m pip install enterprython

Or, if you like to use latest version from this repository:

git clone https://github.com/Dobiasd/enterprython
cd enterprython
python3 -m pip install .

License

Distributed under the MIT License. (See accompanying file LICENSE or at https://opensource.org/licenses/MIT)