Usage¶
Preparations¶
Before we start to inject dependencies, let's define code which needs these dependencies. Also, let's add some behavior to your robot.
>>> class Robot:
... def __init__(self, servo, controller, settings):
... self.servo = servo
... self.controller = controller
... self.settings = settings
...
... def run(self):
... while True:
... events = self.accept_events()
... if not events:
... break
... self.process(events)
...
... def accept_events(self):
... # We can inject methods.
... return self.controller()
...
... def process(self, events):
... # We can inject dictionaries.
... max_point = self.settings["max_point"]
... for event in events:
... if event.x > max_point:
... # We can inject objects.
... self.servo.reverse("x")
... if event.y > max_point:
... self.servo.reverse("y")
We use constructor-based dependency injection here: we define necessary arguments and store them explicitly, for the sake of readability. This will help us to understand the execution path of your system. Attributes sourced from nowhere in your code aren't fun. Believe me.
Now, it's time to make this work in the real world.
>>> class MechanicalMotor:
... def reverse(self, coordinate):
... # Hardware work goes here.
... pass
>>> def read_sensor():
... # Another hardware work goes here.
... return []
>>> production = {'max_point': 0.01}
We are close to scream "It's alive!" and, if we're lucky enough, run out of the building.
>>> from dependencies import Injector
>>> class Container(Injector):
... robot = Robot
... servo = MechanicalMotor
... controller = read_sensor
... settings = production
>>> robot = Container.robot # Robots' constructor called here.
>>> robot.run()
Congratulations! We've built our robot with dependency injection.
Injection rules¶
Container
above is a dependency scope, and dependencies are defined as its
attributes. When you access one of those attributes, the following happens:
- If attribute value is a
class
, it will be instantiated. To make that possible, the library will inspect its constructor's argument list and search current dependency scope for dependencies with the same name. - If attribute value is a
class
but attribute name ends with_class
- then it will be returned as is. (For example,Container.foo_class
will return the class stored in it, not its instance). - Anything else is returned as is.
- If, during dependency search, we encounter another
class
- it will be instantiated along these rules, as well. The process is recursive.
Here is a demonstration of rules above.
>>> class Foo:
... def __init__(self, one, two):
... self.one = one
... self.two = two
>>> class Bar:
... pass
>>> class Baz:
... def __init__(self, x):
... self.x = x
>>> from dependencies import Injector
>>> class Scope(Injector):
... foo = Foo
... one = Bar
... two = Baz
... x = 1
>>> Scope.foo # doctest: +ELLIPSIS
<__main__.Foo object at 0x...>
>>> Scope.foo.one # doctest: +ELLIPSIS
<__main__.Bar object at 0x...>
>>> Scope.foo.two # doctest: +ELLIPSIS
<__main__.Baz object at 0x...>
>>> Scope.foo.two.x
1
Let's roll down what is happening here:
Foo
class requires an argument namedtwo
;- In dependency scope, that argument resolves to
Baz
class; - Which is a class - oh, we need to instantiate it as well;
- But its constructor requires an argument named
x
; - Which resolves to
1
in the dependency scope, so we do not need to go any further.
Having found that out, we effectively construct, execute, and return
Foo(two=Baz(x=1))
.
Calculation rules¶
Each dependency evaluates exactly once during injection process. If during dependency injection different classes have constructor argument with the same name, the corresponding dependency will be instantiated once and these two constructors will receive the same object. But this object only lives during one injection process; another attribute access means a new object.
>>> from dependencies import Injector
>>> class Container(Injector):
...
... class Foo:
...
... def __init__(self, bar, baz):
... self.bar = bar
... self.baz = baz
...
... def check(self):
... return self.bar.x is self.baz.x
...
... class Bar:
...
... def __init__(self, x):
... self.x = x
...
... class Baz:
...
... def __init__(self, x):
... self.x = x
...
... class X:
... pass
...
... # Names.
... foo, bar, baz, x = Foo, Bar, Baz, X
>>> Container.foo.check()
True
>>> Container.bar.x is Container.bar.x
False
Scope extension¶
You can define a dependency scope partially and then extend it; only in injection moment, meaning at the time of attribute access, you are required to have the complete scope.
It is possible to extend dependency scopes in two ways:
- inheritance
- call
Inheritance¶
You can add additional dependencies or redefine existing ones in a scope subclass:
>>> class Foo:
... pass
>>> class Scope(Injector):
... foo = Foo
>>> class ChildScope(Scope):
... bar = Bar
>>> ChildScope.foo # doctest: +ELLIPSIS
<__main__.Foo object at 0x...>
Multiple inheritance is allowed as well.
>>> class Scope1(Injector):
... foo = Foo
>>> class Scope2(Injector):
... bar = Bar
>>> class ChildScope(Scope1, Scope2):
... pass
>>> ChildScope.foo # doctest: +ELLIPSIS
<__main__.Foo object at 0x...>
We also provide and
notation for in-place Injector
composition. Example
below is full equivalent to the previous one, but without intermediate class
needed.
>>> class Scope1(Injector):
... foo = Foo
>>> class Scope2(Injector):
... bar = Bar
>>> (Scope1 & Scope2).foo # doctest: +ELLIPSIS
<__main__.Foo object at 0x...>
Note
Extension scope should not be empty. You can't inherit from
Injector
just to have pass
keyword as the body of the class.
>>> class Container(Injector):
... pass
Traceback (most recent call last):
...
_dependencies.exceptions.DependencyError: Extension scope can not be empty
Call¶
You can temporary redefine a dependency for only one case. This is extremely useful for tests. Inject an assertion instead of one or more dependencies, and you will be able to test your system in all possible cases. It is, for example, possible to simulate database integrity error on concurrent access.
>>> class Scope(Injector):
... foo = Foo
... bar = Bar
>>> Scope(bar=Baz).foo # doctest: +ELLIPSIS
<__main__.Foo object at 0x...>
It is possible to build dependency scopes directly from dictionaries using call.
>>> settings = {'host': 'localhost', 'port': 1234}
>>> Scope = Injector(foo=Foo, bar=Bar, **settings)
hasattr
alternative¶
hasattr
works by attribute access, so it triggers dependency injection. If
this is unnecessary side effect, dependencies
provides alternative way.
>>> class Container(Injector):
... foo = 1
>>> 'foo' in Container
True
— ⭐ —