zanna

Documentation Status Updates Python 3

Simple Dependency Injection library. Supports python 3.5+ and makes full use of the typing annotations. The design is pythonic but inspired by Guice in many aspects.

Motivation

Zanna is meant to be a modern (3.5+), well maintained injection library for Python.

Features

  • Support for typing annotations
  • Decorators are not mandatory: all the injection logic can be outside your modules
  • Supports injection by name
  • Instances can be bound directly, useful when testing (i.e. by override bindings with mocks)
  • No autodiscover for performance reasons and to avoid running into annoying bugs

Usage

Injecting by variable name

The basic form of injection is performed by variable name. The injector expects a list of modules (any callable that takes a Binder as argument). You can get the bound instance by calling get_instance

from zanna import Injector, Binder

def mymodule(binder: Binder) -> None:
    binder.bind_to("value", 3)

injector = Injector(mymodule)
assert injector.get_instance("value") == 3

Zanna will automatically inject the value into arguments with the same name:

from zanna import Injector, Binder

def mymodule(binder: Binder) -> None:
    binder.bind_to("value", 3)

class ValueConsumer:
    def __init__(self, value):
        self.value = value

injector = Injector(mymodule)
assert injector.get_instance(ValueConsumer).value == 3

Injecting by type annotation

Zanna also makes use of python typing annotations to find the right instance to inject.

from zanna import Injector, Binder

class ValueClass:
    def __init__(self, the_value: int):
        self.the_value = the_value

class ValueConsumer:
    def __init__(self, value_class_instance: ValueClass):
        self.value_class_instance = value_class_instance

def mymodule(binder: Binder) -> None:
    binder.bind_to("the_value", 3)
    binder.bind(ValueClass)

injector = Injector(mymodule)
assert injector.get_instance(ValueConsumer).value_class_instance.the_value == 3

Singleton or not singleton?

Instances provided by the injector are always singletons, meaning that the __init__ method of the class will be called only the first time, and every subsequent call of get_instance will return the same instance:

from zanna import Injector

class MyClass:
    pass
injector = Injector(lambda binder: binder.bind(MyClass))
assert injector.get_instance(MyClass) == injector.get_instance(MyClass)

Use providers for more complex use cases

Binder instances can be used to bind providers. A provider is any callable that takes any number of arguments and returns any type. The injector will try to inject all the necessary arguments. Providers can be bound explicitely or implicitely (in which case zanna will use the return annotation to bind by type).

from zanna import Injector, Binder

class AValueConsumer:
    def __init__(self, value: int):
        self.value = value

def explicit_provider(a_value: int) -> int:
    return a_value + 100

def implicit_provider(value_plus_100: int) -> AValueConsumer:
    return AValueConsumer(value_plus_100)

def mymodule(binder: Binder) -> None:
    binder.bind_to("a_value", 3)
    binder.bind_provider("value_plus_100", explicit_provider)
    binder.bind_provider(implicit_provider)

injector = Injector(mymodule)
assert injector.get_instance(AValueConsumer).value == 103

Override existing bindings

Bindings can be overridden. Overriding a non-existent binding will result in a ValueError being raised.

Override bindings is extremely useful when testing, as any part of your stack can be replaced with a mock.

from zanna import Injector, Binder
from unittest.mock import MagicMock

class ValueClass:
    def __init__(self):
        pass
    def retrieve_something(self):
        return ['some', 'thing']

class ValueConsumer:
    def __init__(self, value: ValueClass):
        self.value = value


def mymodule(binder: Binder) -> None:
    binder.bind(ValueClass)

injector = Injector(mymodule)
assert injector.get_instance(ValueConsumer).value.retrieve_something() == ['some', 'thing']

def module_overriding_value_class(binder: Binder) -> None:
    mock_value_class = MagicMock(ValueClass)
    mock_value_class.retrieve_something.return_value = ['mock']
    binder.override_binding(ValueClass, mock_value_class)

injector = Injector(mymodule, module_overriding_value_class)
assert injector.get_instance(ValueConsumer).value.retrieve_something() == ['mock']

Using the decorators

One of the advantages of using Zanna over other solutions is that it doesn’t force you to pollute your code by mixing in the injection logic.

If you are working on a small project and would like to handle part (or all) of the injection logic using decorators instead of modules, Zanna supports that as well.

Internally, Zanna creates a module that sets up the bindings as indicated by the decorators (in a random order).

All Injectors initialized with use_decorators=True will run that module first on their Binder.

Zanna supports the following decorators:

  • decorators.provider, which takes a provided annotated with an appropriate return type
  • decorators.provider_for, which can be given the name or the class of the instance provided
  • decorators.inject, to annotate class to be bound/injected

Here’s an example:

from zanna import Injector
from zanna import decorators
class Thing:
    pass

@decorators.provider_for("value")
def provide_value():
    return 3

@decorators.provider
def provide_thing() -> Thing:
    return Thing()


@decorators.inject
class OtherThing:
    def __init__(self, value, thing:Thing):
        self.value = value
        self.thing = thing

inj = Injector(use_decorators=True)
otherthing = inj.get_instance(OtherThing)
assert otherthing.value == 3
assert isinstance(otherthing.thing, Thing)
assert isinstance(otherthing, OtherThing)

Credits

This package was created with Cookiecutter and the audreyr/cookiecutter-pypackage project template.