/autocontainer

A modern typing based service container and dependency injector for Python 3.5+

Primary LanguagePythonMIT LicenseMIT

AutoContainer

Python really needed a modern reflection based dependency injection container that "just works". Alas, welcome to AutoContainer for python. The dependency injection service container that just works.

Features

  • Direct class as service
  • Separate provider functions
  • Service Behaviors:
    • Singleton
    • Factory
    • Assembler
    • Instance
  • Naming Services
  • Container Bound Functions
  • Dependency Graph
  • Automatic Injection
  • Service registration checking
  • Inject typehint by name
  • Primitive types by name

Installation

pip install autocontainer

Requirements:

  • Python >= 3.5

Usage

It's all about types and hints, but first create the container

from autocontainer import Container

container = Container()

# Party Time

Classes & Injection

We'll use singleton as an example.

class A:
    pass

class B:
    def __init__(self, obj_a: A):
        assert isinstance(obj_a, A)

# Order does not matter.
container.singleton(B)
container.singleton(A)

obj_b = container.get(B)
assert isinstance(obj_b, B)

Note: The container actually contains itself as a singleton service (it's the first registered service), hence if you typehint the Container class or any derivateives, the container can and will inject itself.

Naming Services

class A:
    pass

container.singleton(A, 'ayy')

obj_a = container.get(A)
obj_b = container.get('ayy')


assert obj_a is obj_b

Other ways to get

obj_a = container.get(A)      # <--- Best IDE Support due to type hints.
obj_b = container.get('ayy')
obj_c = container.ayy         # <--- the most concise way.
obj_d = container('ayy')

Builder Functions

You won't always put raw classes into the service container sometimes, it's necessary to write a function that custom initializes a class or object.

class A:
    pass

class B:
    def __init__(self):
        self.fruit = 'tomato'

def makeB(obj_a: A) -> B: # Return type MUST be annotated
    b = B()
    b.fruit = 'mango'

    return b

container.singleton(makeB)

obj_b = container(B)

assert obj_b.fruit == 'mango'

Factory

Factories can also take builder function as well as classes. The container returns a new instance every time.

class A:
    pass

container.factory(A)

aa = container.get(A)
ab = container.get(A)

assert aa is not ab
assert isinstance(aa, A)
assert isinstance(ab, A)

Binding

This is the coolest feature, trust me. Imagine you have a function that needs both classes out of a container and vanilla arguments like int and str, this would be a pain to do manually. Unless...

class A:
    pass

class B:
    pass

container.singleton(A)
container.singleton(B)

def crazy_function(a: A, repeating: str, b: B, times: int):
    assert isinstance(a, A)
    assert isinstance(b, B)

    return repeating * times

less_crazy_function = container.bind(crazy_function)

result = less_crazy_function("pew", 3)
assert result == 'pewpewpew'

This works for both classes (class constructors) and for functions.

Injecting

Same as binding but for simpler times.

class A:
    pass

class B:
    pass

container.singleton(A)
container.singleton(B)

def crazy_function(a: A, b: B):
    assert isinstance(a, A)
    assert isinstance(b, B)

    return 'potato'

assert container.inject(crazy_function) == 'potato'

Specificity Injector

The container maintains an internal graph of dependencies that allows it to efficiently push instances of ancestor classes.

class A:
    pass

class B:
    pass

class C(A):
    pass

class D(C, B):
    pass

container.factory(A)
container.singleton(B)
container.factory(C)
container.singleton(D)

obj = container.get(A)
assert isinstance(obj, D)
assert isinstance(obj, A)

Hinting by Name

This is completely valid with the container

class A:
    pass

container.singleton(A, 'apple')

def magic(ap: 'apple'):
    assert isinstance(ap, A)

container.inject(magic)

Available Methods

  • get(service: Union[Type, str]) Retreives a service

  • has(service: Union[Type, str])
    Returns if a service exists

  • singleton(service: Union[Type, Callable[..., Instance]], name?: str)
    Adds a service as a singleton into container. (Returns the same object on every get)

  • factory(service: Union[Type, Callable[..., Instance]], name?: str)
    Adds a service as a factory into container. (Returns a fresh object on every get)

  • instance(service: object, name?: str)
    Adds a service as an instance into container. (Returns the same object on every get, but does not try to instantiate)

  • assembler(service: Union[Type, Callable[..., Instance]], name?: str)
    Adds a service such that on every get, the container returns a bound callable that produces a fresh object everytime.

  • bind(func: Callable)
    Returns a new callable but in which the arguments recognized by the container are automatically pushed when calling. (see examples below)

  • inject(func: Callable)
    Takes a callable and calls it by injecting all the services it requires and then returns the return value.

Running Tests

python -m unittest discover -s ./

License

MIT. Go crazy.