Contents¶
Injectable: Dependency Injection for Humans™¶
Usage Examples 🚩 | Developer Reference 👩💻 | Authors 👫
license | |
---|---|
docs | |
tests | |
package |
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 withAutowired
, 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
toAutowired
:deps: Autowired(List[“qualifier”])
. - Transparent lazy initialization: passing the argument
lazy=True
forAutowired
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 indep: 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!
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!
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.
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()
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
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.
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()
from abc import ABC, abstractmethod
class AbstractService(ABC):
@abstractmethod
def combine(self, a, b):
...
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}")
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.
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()
from abc import ABC, abstractmethod
class SenderService(ABC):
@abstractmethod
def send(self, message, recipient):
...
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}")
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}")
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.
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()
from injectable import injectable
@injectable
class ServiceA:
def __init__(self):
print("ServiceA::__init__ called")
def something(self):
print("ServiceA::something called")
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.
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.
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()
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
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.
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()
from injectable import injectable
@injectable(qualifier="MEASURING_SERVICE", namespace="INTL")
class InternationalMeasuringService:
def earth_to_sun_distance(self):
return "151.38 million km"
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.
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()
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.
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()
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)
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.
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()
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
.
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.
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.
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()
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
from examples.service_locator.sample_service import SampleService
from injectable import injectable
@injectable
class SpecializedService(SampleService):
pass
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 invokingload_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.
-
classmethod
-
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
anAutowiringError
will be raised.An
AutowiringError
will also be raised if a parameter annotated withAutowired
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")): ... ...
- 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.
-
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
orqualifier
parameters need to be defined. AnInjectableLoadError
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 theAutowired
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 parameteroptional
isTrue
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 raiseInjectionError
. 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 theAutowired
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 parameteroptional
isTrue
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.
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: - dependency – class or qualifier of the dependency.
- namespace – (optional) namespace in which the injectable will be registered.
Defaults to
injectable.constants.DEFAULT_NAMESPACE
.
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
orqualifier
parameters need to be defined. Otherwise aValueError
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 thepropagate
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 parameterklass
will raise aValueError
. 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
orisinstance
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¶
- Rodrigo Martins de Oliveira - https://github.com/allrod5
- Craig Minihan - https://github.com/craigminihan
- Teodor Kulej - https://github.com/mt3o
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 ofload_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 toinjectable.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 theAutowired
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
andinject_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