Contents

Injectable: Dependency Injection for Humans™

Usage Examples 🚩 | Developer Reference 👩‍💻 | Authors 👫

license GitHub license
docs Documentation
tests Build Status Requirements Status Coverage Status Reliability Rating Security Rating Code Style Standards
package PyPI Package latest release PyPI Wheel Supported versions Supported implementations Supported Platforms Downloads per Month

Injectable is an elegant and simple Dependency Injection framework built with Heart and designed for Humans.

from injectable import Autowired, autowired
from typing import List
from models import Database
from messaging import Broker

class Service:
    @autowired
    def __init__(
        self,
        database: Autowired(Database),
        message_brokers: Autowired(List[Broker]),
    ):
        pending = database.get_pending_messages()
        for broker in message_brokers:
            broker.send_pending(pending)
from abc import ABC

class Broker(ABC):
    def send_pending(messages):
        ...
from injectable import injectable

@injectable
class Database:
    ...
from messaging import Broker
from injectable import injectable

@injectable
class KafkaProducer(Broker):
    ...
from messaging import Broker
from injectable import injectable

@injectable
class SQSProducer(Broker):
    ...

Features you’ll love ❤️

  • Autowiring: injection is transparent to the function. Just decorate the function with @autowired and annotate parameters with Autowired, that’s it.
  • Automatic dependency discovery: just call load_injection_container() at the root of your project or pass the root path as an argument. All classes decorated with @injectable will be automatically discovered and ready for injection.
  • Qualifier overloading: declare as many injectables as you like for a single qualifier or extending the same base class. You can inject all of them just by specifying a typing.List to Autowired: deps: Autowired(List[“qualifier”]).
  • Transparent lazy initialization: passing the argument lazy=True for Autowired will make your dependency to be initialized only when actually used, all in a transparent fashion.
  • Singletons: decorate your class with @injectable(singleton=True) and only a single instance will be initialized and shared for injection.
  • Namespaces: specify different namespaces for injectables as in @injectable(namespace=”foo”) and then just use them when annotating your parameters as in dep: Autowired(, namespace=”foo”).
  • Linters friendly: Autowired is carefully designed to comply with static linter analysis such as PyCharm’s to preserve the parameter original type hint.

These are just a few cool and carefully built features for you. Check out our docs!

Installation

At the command line:

pip install injectable

Usage Examples

TL;DR

This is an straightforward example of the injectable framework in a single Python file.

See also

For better understanding of this framework you can look at the other examples in the Usage Examples section. The Basic Usage Example is a good start!

tldr_example.py
from examples import Example
from injectable import injectable, autowired, Autowired, load_injection_container


@injectable
class Dep:
    def __init__(self):
        self.foo = "foo"


class IllustrativeExample(Example):
    @autowired
    def __init__(self, dep: Autowired(Dep)):
        self.dep = dep

    def run(self):
        print(self.dep.foo)
        # foo


def run_example():
    load_injection_container()
    example = IllustrativeExample()
    example.run()


if __name__ == "__main__":
    run_example()

Basic Usage Example

In this example you’ll grasp the basic ideas behind this framework.

We will be injecting dependencies into our BasicUsage class’s __init__ method using the @autowired decorator and the Autowired type annotation.

We declare the classes BasicService and StatefulRepository as injectables with the @injectable decorator to then inject the StatefulRepository into the BasicService which in turn will be injected to the BasicUsage example class.

In BasicUsage::run we illustrate how each injected dependency is a completely independent instance of other injections. We inject two BasicService instances and each one will be injected with a StatefulService instance. Finally, we set the state of each service’s repository to demonstrate how they are completely independent.

See also

The Singletons Example shows how to make a dependency to be shared for all injections instead of having the default behavior of independent instances.

basic_usage_example.py
from examples import Example
from examples.basic_usage.basic_service import BasicService
from injectable import autowired, Autowired, load_injection_container


class BasicUsage(Example):
    @autowired
    def __init__(
        self,
        basic_service: Autowired(BasicService),
        another_basic_service: Autowired(BasicService),
    ):
        self.basic_service = basic_service
        self.another_basic_service = another_basic_service

    def run(self):
        print(self.basic_service.get_repository_state())
        # None

        print(self.another_basic_service.get_repository_state())
        # None

        self.basic_service.set_repository_state("foo")
        self.another_basic_service.set_repository_state("bar")

        print(self.basic_service.get_repository_state())
        # foo

        print(self.another_basic_service.get_repository_state())
        # bar


def run_example():
    load_injection_container()
    example = BasicUsage()
    example.run()


if __name__ == "__main__":
    run_example()
basic_service.py
from examples.basic_usage.stateful_repository import StatefulRepository
from injectable import injectable, autowired, Autowired


@injectable
class BasicService:
    @autowired
    def __init__(self, repository: Autowired(StatefulRepository)):
        self.repository = repository

    def set_repository_state(self, state):
        self.repository.state = state

    def get_repository_state(self):
        return self.repository.state
stateful_repository.py
from injectable import injectable


@injectable
class StatefulRepository:
    def __init__(self):
        self._state = None

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        self._state = value

Dependencies Precedence Example

In this example you’ll see the use of declaring an injectable as primary and the use of explicitly declared qualifiers.

We declare an abstract base class AbstractService which exposes an abstract combine method. The classes SumService and MultiplyService both implement AbstractService and are declared as injectable though we specify different qualifiers for each and also declare SumService as primary.

Now, when we inject an AbstractService into our DependenciesPrecedence example class we will get an instance of the SumService class since we declared it as primary. We inject a MultipleService instance as well by using the explicit qualifier attributed to it.

Note

If SumService wasn’t declared as primary then injecting AbstractService would have failed and raised an error indicating there was ambiguity in resolving the specified dependency.

See also

The Qualifier Overloading Example shows how to get all instances which resolves a dependency instead of just the primary one.

dependencies_precedence_example.py
from examples import Example
from examples.dependencies_precedence.abstract_service import AbstractService
from injectable import (
    autowired,
    Autowired,
    load_injection_container,
)


class DependenciesPrecedence(Example):
    @autowired
    def __init__(
        self,
        abstract_service_1: Autowired(AbstractService),
        abstract_service_2: Autowired("multiply"),
    ):
        self.abstract_service_1 = abstract_service_1
        self.abstract_service_2 = abstract_service_2

    def run(self):
        self.abstract_service_1.combine(4, 2)
        # 4 + 2 = 6

        self.abstract_service_2.combine(4, 2)
        # 4 * 2 = 8


def run_example():
    load_injection_container()
    example = DependenciesPrecedence()
    example.run()


if __name__ == "__main__":
    run_example()
abstract_service.py
from abc import ABC, abstractmethod


class AbstractService(ABC):
    @abstractmethod
    def combine(self, a, b):
        ...
sum_service.py
from examples.dependencies_precedence.abstract_service import AbstractService
from injectable import injectable


@injectable(qualifier="sum", primary=True)
class SumService(AbstractService):
    def combine(self, a, b):
        print(f"{a} + {b} = {a + b}")
multiply_service.py
from examples.dependencies_precedence.abstract_service import AbstractService
from injectable import injectable


@injectable(qualifier="multiply")
class MultiplyService(AbstractService):
    def combine(self, a, b):
        print(f"{a} * {b} = {a * b}")

Qualifier Overloading Example

In this example you’ll learn about overloading qualifiers/classes for injection and how to take advantage of that to inject multiple dependencies as a list of instances.

Overloading happens when two or more injectables are declared for a same qualifier or class.

In this example we create a abstract base class SenderService and implement it in other three classes, EmailSenderService, SmsSenderService, and FaxSenderService. All the three concrete services are declared as injectables and as injection declared class propagates to base classes we end up with three injectables declared for the SenderService class.

In our QualifierOverloading example class we inject a list with all injectables declared for the SenderService by using the typing.List type. We also use the exclude_groups parameter to filter out injectables that were declared with the "old" group label.

See also

The Dependencies Precedence Example shows how dependency resolution works in regards to precedence when a qualifier or class are resolved by multiple injectables and you’re injecting a single instance and not all matching injectables.

qualifier_overloading_example.py
from typing import List

from examples import Example
from examples.qualifier_overloading.sender_service import SenderService
from injectable import autowired, Autowired, load_injection_container


class QualifierOverloading(Example):
    @autowired
    def __init__(
        self,
        sender_services: Autowired(List[SenderService], exclude_groups=["old"]),
    ):
        self.sender_services = sender_services

    def send_message(self, message: str, recipient: str):
        for sender_service in self.sender_services:
            sender_service.send(message, recipient)

    def run(self):
        self.send_message(message="Hello!", recipient="World")
        # Sending Email to World: Hello!
        # Sending SMS to World: Hello!


def run_example():
    load_injection_container()
    example = QualifierOverloading()
    example.run()


if __name__ == "__main__":
    run_example()
sender_service.py
from abc import ABC, abstractmethod


class SenderService(ABC):
    @abstractmethod
    def send(self, message, recipient):
        ...
email_sender_service.py
from examples.qualifier_overloading.sender_service import SenderService
from injectable import injectable


@injectable
class EmailSenderService(SenderService):
    def send(self, message, recipient):
        print(f"Sending Email to {recipient}: {message}")
sms_sender_service.py
from examples.qualifier_overloading.sender_service import SenderService
from injectable import injectable


@injectable
class SmsSenderService(SenderService):
    def send(self, message, recipient):
        print(f"Sending SMS to {recipient}: {message}")
fax_sender_service.py
from examples.qualifier_overloading.sender_service import SenderService
from injectable import injectable


@injectable(group="old")
class FaxSenderService(SenderService):
    def send(self, message, recipient):
        print(f"Sending Fax to {recipient}: {message}")

Lazy Injection Example

In this example you’ll see how to declare an injection as lazy and how does it work.

We declare classes ServiceA and ServiceB which both print when they are being instantiated and also when their method something is invoked.

In our LazyInjection example class we inject the ServiceA lazily by specifying the parameter lazy=True to Autowired and we also inject the ServiceB the default way (not lazy).

You can see that at the LazyInjection::__init__ method the ServiceB::__init__ method is called right at injection time while we do not see the same for ServiceA.

Now, in the LazyInjection::run method we can see that ServiceA::__init__ is only called when actually needed, i.e., when we invoke ServiceA::something.

See also

The Cyclic Dependency Example details how to leverage lazy injection to deal with circular references.

lazy_injection_example.py
from examples import Example
from examples.lazy_injection.service_a import ServiceA
from examples.lazy_injection.service_b import ServiceB
from injectable import autowired, Autowired, load_injection_container


class LazyInjection(Example):
    @autowired
    def __init__(
        self, service_a: Autowired(ServiceA, lazy=True), service_b: Autowired(ServiceB)
    ):
        # ServiceB::__init__ called
        print("example init started")
        # example init started
        self.service_a = service_a
        self.service_b = service_b
        print("example init finished")
        # example init finished

    def run(self):
        print("running")
        # running

        self.service_a.something()
        # ServiceA::__init__ called
        # ServiceA::something called

        self.service_b.something()
        # ServiceB::something called


def run_example():
    load_injection_container()
    example = LazyInjection()
    example.run()


if __name__ == "__main__":
    run_example()
service_a.py
from injectable import injectable


@injectable
class ServiceA:
    def __init__(self):
        print("ServiceA::__init__ called")

    def something(self):
        print("ServiceA::something called")
service_b.py
from injectable import injectable


@injectable
class ServiceB:
    def __init__(self):
        print("ServiceB::__init__ called")

    def something(self):
        print("ServiceB::something called")

Optional Injection Example

In this example you’ll see how to declare an injection as optional using typing.Optional.

When a dependency is not found for injection you’ll receive an injectable.InjectionError. This may not be what you want if it is expected and OK that in some situations the dependency simply won’t be present.

In our OptionalInjection example class we optionally autowire the some_service argument with the "foo" qualifier and we optionally autowire the bunch_of_services argument with a list of all injectables that satisfy the "bar" qualifier.

In this example, both qualifiers, "foo" and "bar", aren’t declared by any injectable though as we declared both injections as optional, the __init__ method won’t fail and instead will inject the value None to some_service and an empty list [] to bunch_of_services.

Note

The typing.Optional type shall be the outermost declared type, so Autowired(Optional[List[...]]) will work while Autowired(List[Optional[...]]) won’t.

See also

The Qualifier Overloading Example shows how to use typing.List to get all instances which resolves a dependency instead of just the primary one.

optional_injection_example.py
from typing import Optional, List

from examples import Example
from injectable import autowired, Autowired, load_injection_container


class OptionalInjection(Example):
    @autowired
    def __init__(
        self,
        some_service: Autowired(Optional["foo"]),
        bunch_of_services: Autowired(Optional[List["bar"]]),
    ):
        self.some_service = some_service
        self.bunch_of_services = bunch_of_services

    def run(self):
        print(self.some_service)
        # None

        print(self.bunch_of_services)
        # []


def run_example():
    load_injection_container()
    example = OptionalInjection()
    example.run()


if __name__ == "__main__":
    run_example()

Cyclic Dependency Example

In this example you’ll learn how the injectable framework can make it easier to deal with circular references.

We use qualifiers to register ours cyclic-dependent services which will enable us to refer to these dependencies by their qualifier string when injecting the services into our CyclicDependency example class instead of having to import them and possibly falling into a cyclic import loop.

For each of the services we inject the other one with a lazy modifier which will prevent us from falling into an instantiation loop as lazy dependencies are only instantiated when its attributes are accessed or its methods are invoked.

See also

The Lazy Injection Example details how lazy injection works.

cyclic_dependency_example.py
from examples import Example
from injectable import Autowired, autowired, load_injection_container


class CyclicDependency(Example):
    @autowired
    def __init__(self, service_a: Autowired("A"), service_b: Autowired("B")):
        self.service_a = service_a
        self.service_b = service_b

    def run(self):
        print(self.service_a.get_some_property_from_b)
        # some property from B

        print(self.service_b.get_some_property_from_a)
        # some property from A


def run_example():
    load_injection_container()
    example = CyclicDependency()
    example.run()


if __name__ == "__main__":
    run_example()
service_a.py
from injectable import injectable, Autowired, autowired


@injectable(qualifier="A")
class ServiceA:
    @autowired
    def __init__(self, service_b: Autowired("B", lazy=True)):
        self.service_b = service_b
        self.some_property = "some property from A"

    @property
    def get_some_property_from_b(self):
        return self.service_b.some_property
service_b.py
from injectable import injectable, autowired, Autowired


@injectable(qualifier="B")
class ServiceB:
    @autowired
    def __init__(self, service_a: Autowired("A", lazy=True)):
        self.service_a = service_a
        self.some_property = "some property from B"

    @property
    def get_some_property_from_a(self):
        return self.service_a.some_property

Namespaces Example

In this example you’ll see how namespaces work in the framework.

Each namespace has its own independent injectables registry and one namespace cannot see the others injectables.

To illustrate how this works we declare two classes, InternationalMeasuringService and UnitedStatesMeasuringService, and register them both as injectables for the "MEASURING_SERVICE" qualifier but each is assigned to a different namespace, "INTL" and "US" respectively.

In our Namespace example class we inject three parameters with "MEASURING_SERVICE" injectables: first without specifying any particular namespace and also declaring the injection as optional; then specifying the "INTL" namespace; and at last specifying the "US" namespace.

By running the example we can see that the default_measuring_service is not injected and that the other ones are successfully injected without conflicts in resolving the qualifier.

What we see for the default_measuring_service argument is that without specifying a namespace the default namespace is used and no injectable that resolves the "MEASURING_SERVICE" qualifier were registered in the default namespace. Then, for the intl_measuring_service and us_measuring_service arguments we only have a single injectable resolving the "MEASURING_SERVICE" qualifier declared in each namespace, therefore no conflicts.

See also

The Optional Injection Example details how declaring an injection as optional works.

namespaces_example.py
from typing import Optional

from examples import Example
from injectable import Autowired, autowired, load_injection_container


class Namespaces(Example):
    @autowired
    def __init__(
        self,
        default_measuring_service: Autowired(Optional["MEASURING_SERVICE"]),
        intl_measuring_service: Autowired("MEASURING_SERVICE", namespace="INTL"),
        us_measuring_service: Autowired("MEASURING_SERVICE", namespace="US"),
    ):
        self.default_measuring_service = default_measuring_service
        self.intl_measuring_service = intl_measuring_service
        self.us_measuring_service = us_measuring_service

    def run(self):
        print(self.default_measuring_service)
        # None

        print(self.intl_measuring_service.earth_to_sun_distance())
        # 151.38 million km

        print(self.us_measuring_service.earth_to_sun_distance())
        # 94.06 million miles


def run_example():
    load_injection_container()
    example = Namespaces()
    example.run()


if __name__ == "__main__":
    run_example()
intl_measuring_service.py
from injectable import injectable


@injectable(qualifier="MEASURING_SERVICE", namespace="INTL")
class InternationalMeasuringService:
    def earth_to_sun_distance(self):
        return "151.38 million km"
us_measuring_service.py
from injectable import injectable


@injectable(qualifier="MEASURING_SERVICE", namespace="US")
class UnitedStatesMeasuringService:
    def earth_to_sun_distance(self):
        return "94.06 million miles"

Singletons Example

In this example you’ll see how we define dependencies as singletons and how they behave.

A singleton injectable is instantiated only once and then this same instance is used whenever an injection is made.

In our Singleton example class we inject two parameters, client1 and client2 both with the SingletonClient class, which in turn, was declared as singleton.

When we run our example we can see that, as there is only one instance being shared between injections a change to its state is then reflected to every other place injected with it.

singleton_example.py
from examples import Example
from examples.singletons.singleton_client import SingletonClient
from injectable import Autowired, autowired, load_injection_container


class Singletons(Example):
    @autowired
    def __init__(
        self,
        client1: Autowired(SingletonClient),
        client2: Autowired(SingletonClient),
    ):
        self.client1 = client1
        self.client2 = client2

    def run(self):
        print(self.client1.connected)
        # False

        print(self.client2.connected)
        # False

        self.client1.connect()
        print(self.client1.connected)
        # True

        print(self.client2.connected)
        # True


def run_example():
    load_injection_container()
    example = Singletons()
    example.run()


if __name__ == "__main__":
    run_example()
singleton_client.py
from injectable import injectable


@injectable(singleton=True)
class SingletonClient:
    def __init__(self):
        self.connected = False

    def connect(self):
        self.connected = True

Factory Example

In this example you’ll see how we can declare a function as a factory of injectable instances.

A common use case for using factories is to wrap some class external to your code or which you cannot declare as injectable for some reason, we declare a ExternalClient class to represent such a case.

The client_factory function is declared as a factory for the ExternalClient class through the @injectable_factory decorator and will deal with all the necessary logic to instantiate an ExternalClient.

Now our Factory example class can be injected with an ExternalClient without having the responsibility of knowing how to actually instantiate it.

See also

The injectable_factory decorator can also be used in lambdas for simpler cases. The Injecting Existing Instance Example shows how to use it like so.

factory_example.py
from examples import Example
from examples.factory.external_client import ExternalClient
from injectable import autowired, Autowired, load_injection_container


class Factory(Example):
    @autowired
    def __init__(
        self,
        client: Autowired(ExternalClient),
    ):
        self.client = client

    def run(self):
        print(self.client.connect())
        # ExternalClient connected to https://dummy/endpoint


def run_example():
    load_injection_container()
    example = Factory()
    example.run()


if __name__ == "__main__":
    run_example()
client_factory.py
import os

from examples.factory.external_client import ExternalClient
from injectable.injection.injectable_factory_decorator import injectable_factory


@injectable_factory(ExternalClient)
def client_factory():
    client_endpoint = os.getenv(
        "CLIENT_ENDPOINT_EXAMPLE_ENV_VAR", "https://dummy/endpoint"
    )
    return ExternalClient(client_endpoint)
external_client.py
class ExternalClient:
    def __init__(self, endpoint: str):
        self.endpoint = endpoint

    def connect(self):
        return f"ExternalClient connected to {self.endpoint}"

Injecting Existing Instance Example

In this example you’ll see how to supply an already-initialized instance as injectable.

For whatever reason we have already initialized an instance of Application and assigned it to the app variable so we use the injectable_factory decorator in a lambda which in turn just returns the existing app.

Now our InjectingExistingInstance example class can be injected with our existing Application instance.

See also

The injectable_factory decorator can also be used in regular functions and not just in lambdas. The Factory Example shows how to use it.

injecting_existing_instance_example.py
from examples import Example
from examples.injecting_existing_instance.app import Application
from injectable import autowired, Autowired, load_injection_container


class InjectingExistingInstance(Example):
    @autowired
    def __init__(
        self,
        app: Autowired(Application),
    ):
        self.app = app

    def run(self):
        print(self.app.number)
        # 42


def run_example():
    load_injection_container()
    example = InjectingExistingInstance()
    example.run()


if __name__ == "__main__":
    run_example()
app.py
from injectable import injectable_factory


class Application:
    def __init__(self, number):
        self.number = number


app = Application(42)
...

injectable_factory(Application)(lambda: app)

Injectable Mocking For Tests Example

This is an example of how one can use the testing utility functions clear_injectables and register_injectables for mocking a dependency for tests.

In this example, since we call load_injection_container and RealDep gets registered we need to make use of the clear_injectables utility before calling register_injectables though if load_injection_container was never called we wouldn’t need to use clear_injectables.

injectable_mocking_example.py
from unittest.mock import Mock

from examples import Example
from injectable import (
    injectable,
    autowired,
    Autowired,
    Injectable,
    load_injection_container,
)
from injectable.testing import clear_injectables, register_injectables


@injectable
class RealDep:
    @staticmethod
    def print():
        print("RealDep")


class InjectableMocking(Example):
    def __init__(self):
        clear_injectables(RealDep)
        mocked_dep = Mock(wraps=RealDep)
        mocked_dep.print = Mock(side_effect=lambda: print("MockedDep"))
        mocked_injectable = Injectable(lambda: mocked_dep)
        register_injectables({mocked_injectable}, RealDep)

    @autowired
    def run(self, dep: Autowired(RealDep)):
        dep.print()
        # MockedDep
        dep.print.assert_called()


def run_example():
    load_injection_container()
    example = InjectableMocking()
    example.run()


if __name__ == "__main__":
    run_example()

Injection Container Resetting For Tests Example

This is an example of how one can use the testing utility function reset_injection_container to clear all state from the injection container including all registered injectables and namespaces.

injection_container_resetting_example.py
from examples import Example
from injectable import (
    injectable,
    autowired,
    Autowired,
    load_injection_container,
)
from injectable.errors import InjectionError
from injectable.testing import reset_injection_container


@injectable
class Foo:
    def do_something(self):
        print("doing something")


class InjectionContainerResetting(Example):
    def run(self):
        self.bar()
        # doing something

        reset_injection_container()

        try:
            self.bar()
            # WARNING:root:Injection Container is empty. Make sure \
            # 'load_injection_container' is being called before any injections are made.
        except InjectionError as e:
            print(e)
            # No injectable matches class 'Foo'

    @autowired
    def bar(self, foo: Autowired(Foo)):
        foo.do_something()


def run_example():
    load_injection_container()
    example = InjectionContainerResetting()
    example.run()


if __name__ == "__main__":
    run_example()

Service Locator Example

In this example you’ll see how to use the low-level Service Locator API of this framework.

We will be injecting dependencies inside our ServiceLocator class’s __init__ method by directly using the injectable.inject() and injectable.inject_multiple() service locator methods.

We declare the classes SampleService, SpecializedService, and StatefulRepository as injectables with the @injectable decorator to then inject the StatefulRepository into the SampleService classes which in turn will be injected to the ServiceLocator example class.

In ServiceLocator::run we illustrate how the injectable.inject() and injectable.inject_multiple() methods work.

Note

The high-level API, which uses decorators and annotations, is preferred over the low-level API.

See also

The Basic Usage Example describes the high-level API of this framework which is based on annotations and decorators.

See also

The Qualifier Overloading Example details how overloading an injectable works by using class inheritance.

service_locator_example.py
from examples import Example
from examples.service_locator.sample_service import SampleService
from injectable import load_injection_container, inject, inject_multiple


class ServiceLocator(Example):
    def __init__(
        self,
    ):
        self.primary_basic_service = inject(SampleService)
        self.all_basic_service_implementations = inject_multiple(SampleService)

    def run(self):
        print(self.primary_basic_service.get_repository_state())
        # None

        for service in self.all_basic_service_implementations:
            print(service.get_repository_state())
            # None
            # None

        for service in self.all_basic_service_implementations:
            service.set_repository_state(0)
            print(service.get_repository_state())
            # 0
            # 0

        self.primary_basic_service.set_repository_state(1)

        for service in self.all_basic_service_implementations:
            print(service.get_repository_state())
            # 1
            # 0

        for service in self.all_basic_service_implementations:
            service.set_repository_state(service.get_repository_state() + 1)
            print(service.get_repository_state())
            # 2
            # 1


def run_example():
    load_injection_container()
    example = ServiceLocator()
    example.run()


if __name__ == "__main__":
    run_example()
sample_service.py
from examples.service_locator.stateful_repository import StatefulRepository
from injectable import injectable, inject


@injectable(primary=True)
class SampleService:
    def __init__(self):
        self.repository: StatefulRepository = inject(StatefulRepository)

    def set_repository_state(self, state):
        self.repository.state = state

    def get_repository_state(self):
        return self.repository.state
specialized_service.py
from examples.service_locator.sample_service import SampleService
from injectable import injectable


@injectable
class SpecializedService(SampleService):
    pass
stateful_repository.py
from injectable import injectable


@injectable
class StatefulRepository:
    def __init__(self):
        self._state = None

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        self._state = value

Reference

injectable

This is the injectable’s public API.

See also

The Usage Examples section presents a collection of examples on how to use this API.

injectable.load_injection_container(search_path: str = None, *, default_namespace: str = 'DEFAULT_NAMESPACE', encoding: str = 'utf-8')[source]

Loads injectables under the search path to a shared injection container under the designated namespaces.

Parameters:
  • search_path – (optional) path under which to search for injectables. Can be either a relative or absolute path. Defaults to the caller’s file directory.
  • default_namespace – (optional) designated namespace for registering injectables which does not explicitly request to be addressed in a specific namespace. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • encoding – (optional) defines which encoding to use when reading project files to discover and register injectables. Defaults to utf-8.

Usage:

>>> from injectable import load_injection_container
>>> load_injection_container()

Note

This method will not scan any file already scanned by previous calls to it. Multiple invocations to different search paths will add found injectables into the injection container without clearing previously loaded ones but never loading a same injectable more than once.

New in version 3.4.0.

class injectable.InjectionContainer[source]

InjectionContainer globally manages injection namespaces and the respective injectables registries.

This class shouldn’t be used directly and will be removed from the injectable’s public API in the future.

Invoking load_injection_container() is the only necessary action before injecting dependencies. Attempting to call an autowired function before invoking load_injection_container() will log a warning indicating that the injection container is empty.

This class is not meant to be instantiated and will raise an error if instantiation is attempted.

Deprecated since version 3.4.0: This class will be removed from the public API in the future.

classmethod load(search_path: str = None, *, default_namespace: str = 'DEFAULT_NAMESPACE')[source]

Loads injectables under the search path to the InjectionContainer under the designated namespaces.

Parameters:
  • search_path – (optional) path under which to search for injectables. Can be either a relative or absolute path. Defaults to the caller’s file directory.
  • default_namespace – (optional) designated namespace for registering injectables which does not explicitly request to be addressed in a specific namespace. Defaults to injectable.constants.DEFAULT_NAMESPACE.

Usage:

>>> from injectable import InjectionContainer
>>> InjectionContainer.load()

Note

This method will not scan any file more than once regardless of being called successively. Multiple invocations to different search paths will add found injectables to the InjectionContainer without clearing previously found ones.

Deprecated since version 3.4.0: This method will be removed from the public API in the future. Use load_injection_container() instead.

class injectable.Injectable(constructor: callable, unique_id: str = <factory>, primary: bool = False, group: Optional[str] = None, singleton: bool = False)[source]

Injectable is the low-level container class in which information regarding an injectable dependency is stored for registering in a namespace.

This class is not meant for direct usage. It should be used in conjunction with the injectable.testing module utilities for testing purposes only.

Parameters:
  • constructor – callable to be used as constructor when injecting.
  • unique_id – (optional) unique identifier for the injectable which prevents duplicates of the same injectable to be registered. Defaults to a UUID generated at initialization time.
  • primary – (optional) marks the injectable as primary for resolution in ambiguous cases. Defaults to False.
  • group – (optional) group to be assigned to the injectable. Defaults to None.
  • singleton – (optional) when True the injectable will be a singleton, i.e. only one instance of it will be created and shared globally. Defaults to False.
injectable.autowired(func: T) → T[source]

Function decorator to setup dependency injection autowiring.

Only parameters annotated with Autowired will be autowired for injection.

If no parameter is annotated with Autowired an AutowiringError will be raised.

An AutowiringError will also be raised if a parameter annotated with Autowired is given a default value or if a non Autowired-annotated positional parameter is placed after an Autowired-annotated positional parameter.

Before attempting to call an autowired function make sure load_injection_container was invoked.

Note

This decorator can be applied to any function, not only an __init__ method.

Note

This decorator accepts no arguments and must be used without trailing parenthesis.

Usage:

>>> from injectable import Autowired, autowired
>>>
>>> @autowired
... def foo(dep: Autowired(...)):
...     ...
class injectable.Autowired[source]

Autowired type annotation marks a parameter to be autowired for injection.

Autowired parameters must be last in declaration if there are others which aren’t autowired. Also, autowired parameters must not be given default values.

This type annotation does not performs the function autowiring by itself. The function must be decorated with @autowired for autowiring.

Parameters:
  • dependency – class, base class or qualifier of the dependency to be used for lookup among the registered injectables. Can be wrapped in a typing sequence, e.g. List[...], to inject a list containing all matching injectables. Can be wrapped in a optional, e.g. Optional[...], to inject None if no matches are found to inject. Optional[List[...]] is valid and will inject an empty list if no matches are found to inject.
  • namespace – (optional) namespace in which to look for the dependency. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • group – (optional) group to filter out other injectables outside of this group. Defaults to None.
  • exclude_groups – (optional) list of groups to be excluded. Defaults to None.
  • lazy – (optional) when True will return an instance which will automatically initialize itself when first used but not before that. Defaults to False.

Usage:

>>> from injectable import Autowired, autowired
>>>
>>> @autowired
... def foo(arg: Autowired("qualifier")):
...     ...
injectable.injectable_factory(dependency: T = None, *, qualifier: str = None, primary: bool = False, namespace: str = None, group: str = None, singleton: bool = False) → Callable[[...], Callable[[...], T]][source]

Function decorator to mark it as a injectable factory for the dependency.

At least one of dependency or qualifier parameters need to be defined. An InjectableLoadError will be raised if none are defined.

Note

This decorator shall be the first decorator of the function since only the received function will be registered as an injectable factory

Note

All files using this decorator will be executed when load_injection_container is invoked.

Parameters:
  • dependency – (optional) the dependency class for which the factory will be registered to. Defaults to None.
  • qualifier – (optional) string qualifier for which the factory will be registered to. Defaults to None.
  • primary – (optional) marks the factory as primary for the dependency resolution in ambiguous cases. Defaults to False.
  • namespace – (optional) namespace in which the factory will be registered. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • group – (optional) group to be assigned to the factory. Defaults to None.
  • singleton – (optional) when True the factory will be used to instantiate a singleton, i.e. only one call to the factory will be made and the created instance will be shared globally. Defaults to False.

Usage:

>>> from injectable import injectable_factory
>>> from foo import Foo
>>>
>>> @injectable_factory(Foo)
... def foo_factory() -> Foo:
...     return Foo(...)
injectable.injectable(cls: T = None, *, qualifier: str = None, primary: bool = False, namespace: str = None, group: str = None, singleton: bool = False) → T[source]

Class decorator to mark it as an injectable dependency.

This decorator accepts customization parameters but can be invoked without the parenthesis when no parameter will be specified.

Note

All files using this decorator will be executed when load_injection_container is invoked.

Parameters:
  • cls – (cannot be explicitly passed) the decorated class. This will be automatically passed to the decorator by Python magic.
  • qualifier – (optional) string qualifier for the injectable to be registered with. Defaults to None.
  • primary – (optional) marks the injectable as primary for resolution in ambiguous cases. Defaults to False.
  • namespace – (optional) namespace in which the injectable will be registered. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • group – (optional) group to be assigned to the injectable. Defaults to None.
  • singleton – (optional) when True the injectable will be a singleton, i.e. only one instance of it will be created and shared globally. Defaults to False.

Usage:

>>> from injectable import injectable
>>>
>>> @injectable
... class Foo:
...     ...
injectable.inject(dependency: Union[Type[T], str], *, namespace: str = None, group: str = None, exclude_groups: Sequence[str] = None, lazy: bool = False, optional: bool = False) → T[source]

Injects the requested dependency by instantiating a new instance of it or a singleton instance if specified by the injectable. Returns an instance of the requested dependency.

One can use this method directly for injecting dependencies though this is not recommended. Use the @autowired decorator and the Autowired type annotation for dependency injection to be automatically wired to a function’s call instead.

Will log a warning indicating that the injection container is empty when invoked before load_injection_container is called.

Raises InjectionError when unable to resolve the requested dependency. This can be due to a variety of reasons: the requested dependency wasn’t loaded into the container; the namespace isn’t correct; the group isn’t correct; there are multiple injectables for the dependency and none or multiple are marked as primary. When parameter optional is True no error will be raised when no injectable that matches requested qualifier/class and group is found in the specified namespace though in ambiguous cases that resolving a primary injectable is impossible an error will still be raised.

Parameters:
  • dependency – class, base class or qualifier of the dependency to be used for lookup among the registered injectables.
  • namespace – (optional) namespace in which to look for the dependency. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • group – (optional) group to filter out other injectables outside of this group. Defaults to None.
  • exclude_groups – (optional) list of groups to be excluded. Defaults to None.
  • lazy – (optional) when True will return an instance which will automatically initialize itself when first used but not before that. Defaults to False.
  • optional – (optional) when True this function returns None if no injectable matches the qualifier/class and group inside the specified namespace instead of raising an InjectionError. Ambiguous cases where resolving a primary injectable is impossible will still raise InjectionError. Defaults to False.

Usage:

>>> from foo import Foo
>>> from injectable import inject
>>>
>>> class Bar:
...     def __init__(self, foo: Foo = None):
...         self.foo = foo or inject(Foo)
injectable.inject_multiple(dependency: Union[Type[T], str], *, namespace: str = None, group: str = None, exclude_groups: Sequence[str] = None, lazy: bool = False, optional: bool = False) → List[T][source]

Injects all injectables that resolves to the specified dependency. Returns a list of instances matching the requested dependency.

One can use this method directly for injecting dependencies though this is not recommended. Use the @autowired decorator and the Autowired type annotation for dependency injection to be automatically wired to a function’s call instead.

Logs a warning indicating that the injection container is empty when invoked before load_injection_container is called.

Raises InjectionError when unable to resolve the requested dependency. This can be due to a variety of reasons: there is no injectable loaded into the container that matches the dependency; the namespace isn’t correct; the group specifications aren’t correct. When parameter optional is True no error will be raised when no injectable that matches requested qualifier/class and group is found in the specified namespace.

Parameters:
  • dependency – class, base class or qualifier of the dependency to be used for lookup among the registered injectables.
  • namespace – (optional) namespace in which to look for the dependency. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • group – (optional) group to filter out other injectables outside of this group. Defaults to None.
  • exclude_groups – (optional) list of groups to be excluded. Defaults to None.
  • lazy – (optional) when True will returned instances will automatically initialize themselves when first used but not before that. Defaults to False.
  • optional – (optional) when True this function returns an empty list if no injectable matches the qualifier/class and group inside the specified namespace. Defaults to False.

Usage:

>>> from com import AbstractService
>>> from injectable import inject_multiple
>>> from typing import Sequence
>>>
>>> class Foo:
...     def __init__(self, services: Sequence[AbstractService] = None):
...         self.services = services or inject_multiple(AbstractService)

injectable.constants

injectable.constants.DEFAULT_NAMESPACE = 'DEFAULT_NAMESPACE'

Default namespace used for registering and searching injectables

injectable.errors

Custom exceptions raised by injectable.

exception injectable.errors.AutowiringError[source]

Error indicating autowiring of a function failed.

exception injectable.errors.InjectionError[source]

Error indicating dependency injection failed.

injectable.testing

Testing utilities to ease mocking injectables.

New in version 3.3.0.

Changed in version 3.4.0: Inclusion of the reset_injection_container() utility.

See also

The Injectable Mocking For Tests Example in the Usage Examples section shows how to use these utilities for mocking purposes.

See also

The Injection Container Resetting For Tests Example in the Usage Examples section shows how to use these utilities for clearing the injection container state.

injectable.testing.clear_injectables(dependency: Union[type, str], namespace: str = None) → Set[injectable.container.injectable.Injectable][source]

Utility function to clear all injectables registered for the dependency in a given namespace. Returns a set containing all cleared injectables.

Parameters:

Usage:

>>> from injectable.testing import clear_injectables
>>> clear_injectables("foo")

New in version 3.3.0.

injectable.testing.register_injectables(injectables: Collection[injectable.container.injectable.Injectable], klass: Optional[type] = None, qualifier: Optional[str] = None, namespace: str = 'DEFAULT_NAMESPACE', propagate: bool = False)[source]

Utility function to manually register injectables in a given namespace for the provided class and/or qualifier.

At least one of klass or qualifier parameters need to be defined. Otherwise a ValueError will be raised.

Parameters:
  • injectables – a collection of injectables to register.
  • klass – (optional) the class for which the injectables will be registered. This parameter is optional as long as qualifier is provided. Injectables registering won’t be propagated to base classes unless otherwise specified by the propagate parameter. Defaults to None.
  • qualifier – (optional) the qualifier for which the injectables will be registered. This parameter is optional as long as klass is provided. Defaults to None.
  • namespace – (optional) namespace in which the injectable will be registered. Defaults to injectable.constants.DEFAULT_NAMESPACE.
  • propagate – (optional) When True injectables registering will be propagated to base classes of klass recursively. Setting this parameter to True and not specifying the parameter klass will raise a ValueError. Defaults to False.

Usage:

>>> from injectable import Injectable
>>> from injectable.testing import register_injectables
>>> injectable = Injectable(constructor=lambda: 42)
>>> register_injectables({injectable}, qualifier="foo")

New in version 3.3.0.

injectable.testing.reset_injection_container()[source]

Utility function to reset the injection container, clearing all injectables registered from all namespaces and reseting the record for already scanned files.

Usage:

>>> from injectable.testing import reset_injection_container
>>> reset_injection_container()

New in version 3.4.0.

Caveats

This is a non-exhaustive list of known caveats to the usage of this library.

Automatic dependency discovery

Injectable automatic dependency discovery system is inspired from Airflow’s DAG automatic discovery. So first all files in the search path are recursively read looking for any occurrence of the following four strings: @injectable, injectable(, @injectable_factory, and injectable_factory(. Then those files are executed as python modules so the decorators can register the injectable to the container.

This implementation leads to some issues:

  • If, for any reason, the code aliases the decorators to other incompatible names or do not use the decorator functions directly automatic dependency will fail and those injectables will never be registered to the container.
  • Any file containing these strings will be executed causing potential unintended side-effects such as file-level code outside classes and functions being executed.
  • The module of each injectable class may be loaded twice: one for in this automatic discovery step and another by the regular application operation. This will render impossible to run type checks for injected objects through the use of type or isinstance builtin functions. If one must type check using the type’s __qualname__ attribute is a possible workaround.

Pytest and relative imports

As described in this issue: https://github.com/pytest-dev/pytest/issues/9007 , pytest won’t work with injectable’s automatic dependency discovery system if one declares injectables in the same file of the test itself, load the injection container during the test and use relative imports in this file. This corner-case combination will lead to an AttributeError: 'AssertionRewritingHook' object has no attribute 'get_code'.

Currently the workaround for this is either to use absolute imports in these files or to move the declaration of injectables to any other file other than the test’s file.

Contributing

Contributions of any kind are welcome in any form.

If you want to submit a code contribution you may do so by forking the project and then opening a Pull Request to this repository’s master branch.

Include a clear and brief description of what your Pull Request is about.

Write unit tests for the code you wrote and add an use case example test if applicable.

Keep in mind that a clear, well organized and readable code is better than a magical, tricky and hard-to-follow code.

Run make black to auto-format your code to this project’s style guidelines and run make checks to ensure there are no linter issues.

While documentation in the form of docstrings are encouraged, code comments are often discouraged. If your code is so cryptic that you need a comment to clarify it, then there may be a better way of writing it so the code speaks for itself in a clear way.

Nevertheless, open a Pull Request and it will receive feedback and be reviewed thoroughly.

Authors

Changelog

3.4.7 (2021-08-15)

  • Fix injectable crashing when relative imports are used in files containing injectables.

3.4.6 (2021-03-20)

  • Fix testing.register_injectables not creating the namespace when it doesn’t exist yet

3.4.5 (2021-03-11)

  • Fix opening of UTF-8 files & allow for user set encoding

3.4.4 (2020-07-29)

  • Fix inject return type hint

3.4.3 (2020-06-24)

  • Fix Injectable failing to resolve complex/entangled imports

3.4.2 (2020-05-22)

  • Fix optional injection bug when the namespace is empty

3.4.1 (2020-05-11)

  • Fix the use of named args by the caller breaking autowired functions injection

3.4.0 (2020-05-09)

  • Deprecate InjectionContainer::load in favor of load_injection_container.
  • Change default namespace name from "_GLOBAL" to "DEFAULT_NAMESPACE".
  • Fix minor quirks with Python 3.7 and 3.8.
  • Add tons of unit tests.
  • Add reset_injection_container utility to injectable.testing.

3.3.0 (2020-04-20)

  • Include the injectable.testing utilities to ease mocking injectables.

3.2.1 (2020-04-19)

  • InjectionContainer::load is more resilient against duplicated injectables registering

3.2.0 (2020-04-15)

  • Support for optional injection in declarative fashion: Autowired(Optional[...])

3.1.4 (2020-04-15)

  • Fix Autowired(List[...]) not working with qualifiers

3.1.3 (2020-04-15)

  • Fix Windows injectables not being loaded.

3.1.2 (2020-04-14)

  • Remove unused inspect imports.

3.1.1 (2020-04-13)

  • Fix bug of scanning the same module more than once when InjectionContainer.load() is called multiple times with different relative search paths.

3.1.0 (2020-04-13)

  • Added @injectable_factory decorator for declaring injectable factory methods
  • Include the console output in the examples

3.0.1 (2020-04-13)

  • Fix package content missing

3.0.0 (2020-04-12)

  • Drop support for autowiring without previous initialization of the InjectionContainer
  • Refactor @autowired decorator for working with the Autowired type annotation
  • Added @injectable decorator for registering injectables to the InjectionContainer
  • Support for qualifiers, groups and namespaces
  • Added Autowired type annotation for marking parameters for autowiring
  • Added inject and inject_multiple as service locators
  • Added InjectionContainer for registering injectables
  • Official support for Python 3.7 and 3.8
  • Official support for Ubuntu, Windows and MacOS
  • Drop Python 3.4 and 3.5 official support
  • General code refactoring
  • Official documentation
  • Added usage examples

2.0.0 (2018-02-24)

  • Drop Python 3.3 official support

1.1.2 (2018-02-24)

  • Support for dependencies of classes without signature
  • Fix bug of builtin types not being accepted for injectable dependencies

1.1.1 (2018-02-23)

  • Statically infer dependency’s constructor suitability for injection instead of using trial instantiation
  • Fix bug of raising TypeError when injectable fails on the trial dependency instantiation which can happen when the dependency does provide a default constructor with no arguments but the running environment (possibly a test suite environment) will make the instantiation fail

1.1.0 (2018-02-10)

  • Enable the use of @autowired decorator without parenthesis

1.0.1 (2018-02-10)

  • Fixes required dependency lazy_object_proxy not being installed when installing injectable through pip

1.0.0 (2018-02-06)

  • First stable release

0.2.0 (2018-02-06)

  • Support for lazy dependency initialization
  • Support for type annotations with strings

0.1.1 (2018-02-05)

  • Python 3.3 and 3.4 support

0.1.0 (2018-02-05)

  • First beta release

Indices and tables