mainline¶
Simple yet powerful python dependency injection.
- Docs: http://mainline.readthedocs.org/en/latest
- API Docs: http://mainline.readthedocs.org/en/latest/mainline.html
- PyPi: https://pypi.python.org/pypi/mainline
Why¶
Pure Python, so it basically works everywhere. Tested against cPython 3.5, 3.6, 3.7 in addition to 2.7. PyPy/PyPy3 are also fully supported.
Supports using function annotations in Python 3.x. This is in addition to a standard syntax that works with both 3.x and 2.7.
Your method signature is fully preserved, maintaining introspection ability. (Minus any injected arguments of course.)
Scope is fully configurable (per injectable), giving you tight control over where an object should be shared and where it should not.
Supports auto injection”, where your argument names are used to determine what gets injected. It’s also fully optional, as it’s slightly less performant do to it’s dynamic nature.
Provider keys tend to be strings, but really any hashable object is supported, so if you prefer to use classes, go for it.
Just keep in mind that you can’t use a class as an argument name (rightfully so) in python. This means you can’t auto inject it, for instance. You can simply make an alias to get both worlds, however. The world is your oyster.
Installation¶
pip install mainline
Usage¶
First things first, create your instance of Di
:
>>> from mainline import Di
>>> di = Di()
Factory registration and resolution of an instance¶
When registering a factory, you can specify a scope. The factory provided will be called to construct an instance once in the scope provided.
After that, the already constructed product of the factory will be injected for all calls to inject()
with the registered key in the specified scope.
For example:
- A factory registered with a
NoneScope
will construct an instance every timeinject()
is called with the registered key. - A factory registered with a
GlobalScope
will construct one instance ever. - A factory registered with a
ProcessScope
will generate an instance once per process. - A factory registered with a
ThreadScope
will generate an instance once per thread.
Scopes can be passed to register_factory()
as scope objects (eg factory callable), or as strings (e.g. NoneScope
is aliased to ‘none’, GlobalScope
is aliased to ‘global’).
The default scope is NoneScope
, which means a new instance is created every time. The only exception to this rule is set_instance()
, which defaults to a GlobalScope
if no provider exists under this key.
Scopes available by default for factory registration are: GlobalScope
(SingletonScope
), ThreadScope
, ProcessScope
and NoneScope
.
However, you may provide your own custom scopes as well by providing any object class/instance that supports a collections.MutableMapping
interface.
>>> @di.register_factory('apple', scope='global')
... def apple():
... return 'apple'
>>> di.resolve('apple') == 'apple'
True
Injection¶
Great care has been taken to maintain introspection on injection.
Using inject()
preserves your method signature minus any injected arguments.
It also has a shortened alias for those like me who don’t much love typing all of that via i()
.
Positional arguments are injected in the order given:
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> @di.inject('apple')
... def injected(apple):
... return apple
>>> injected() == apple()
True
Injecting keyword arguments is straight forward, you simply hand them as keyword arguments:
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> @di.register_factory('banana')
... @di.inject('apple')
... def banana(apple):
... return 'banana', apple
>>> @di.inject('apple', a_banana='banana')
... def injected(apple, arg1, a_banana=None):
... return apple, arg1, a_banana
>>> injected('arg1') == (apple(), 'arg1', banana())
True
You can inject a class-level property using inject_classproperty()
:
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> @di.inject_classproperty('apple')
... class Injectee(object):
... pass
>>> Injectee.apple == apple()
True
Arguments that are not injected work as expected:
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> @di.inject('apple')
... def injected(apple, arg1):
... return apple, arg1
>>> injected('arg1') == (apple(), 'arg1')
True
Injection on a class injects upon it’s __init__ method:
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> @di.inject('apple')
... class Injectee(object):
... def __init__(self, apple):
... self.apple = apple
>>> Injectee().apple == apple()
True
Auto injection based on name in argspec¶
Injecting providers based upon the argpsec can be done with auto_inject()
, or it’s shortened
alias ai()
.
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> @di.auto_inject()
... def injected(apple):
... return apple
>>> injected() == apple()
True
>>> @di.ai('apple') # alias for :func:`~mainline.di.Di.auto_inject`
... def injected(apple, arg1):
... return apple, arg1
>>> injected('arg1') == (apple(), 'arg1')
True
>>> @di.f('banana') # alias for :func:`~mainline.di.Di.register_factory`
... @di.auto_inject()
... def banana(apple):
... return 'banana', apple
>>> @di.ai() # alias for :func:`~mainline.di.Di.auto_inject`
... def injected(apple, arg1, banana=None):
... return apple, arg1, banana
>>> injected('arg1') == (apple(), 'arg1', banana())
True
>>> @di.auto_inject(renamed_banana='banana')
... def injected(apple, arg1, renamed_banana):
... return apple, arg1, renamed_banana
>>> injected('arg1') == (apple(), 'arg1', banana())
True
Instance registration¶
If you want to inject an already instantiated object, you can do so with set_instance()
.
If a factory has not been registered under the given key, one is created using the default_scope argument as it’s scope,
which defaults to GlobalScope
(ie singleton).
The instance is then injected into the factory as if it had been created by it.
>>> apple = object()
>>> di.set_instance('apple', apple)
>>> di.resolve('apple') == apple
True
>>> banana = object()
>>> di.set_instance('banana', banana, default_scope='thread')
>>> di.resolve('banana') == banana
True
Provider keys¶
Provider keys don’t have to be strings. It’s just a mapping internally, so they can be any hashable object.
>>> class Test(object):
... pass
>>> # Thread scopes are stored in a thread local
... @di.register_factory(Test, scope='thread')
... def test_factory():
... return Test()
>>> @di.inject(test=Test)
... def injected(test):
... return test
>>> isinstance(injected(), Test)
True
Catalogs¶
The Catalog
class provides a declarative way to group together factories.
>>> class CommonCatalog(di.Catalog):
... # di.provider() is a Provider factory.
... @di.provider
... def apple():
... return 'apple'
...
... # You can also give it a Provider object directly,
... # albeit being a bit silly.
... orange = di.Provider(lambda: 'orange')
>>> class TestingCatalog(CommonCatalog):
... @di.provider(scope='thread')
... def banana():
... return 'banana'
>>> di.update(TestingCatalog)
>>> @di.inject('apple', 'banana', 'orange')
... def injected(apple, banana, orange):
... return apple, banana, orange
>>> injected() == ('apple', 'banana', 'orange')
True
>>> class ProductionCatalog(di.Catalog):
... @di.provider(scope='thread')
... def banana():
... return 'prod_banana'
>>> di.update(ProductionCatalog, allow_overwrite=True)
>>> @di.inject('apple', 'banana', 'orange')
... def injected(apple, banana, orange):
... return apple, banana, orange
>>> injected() == ('apple', 'prod_banana', 'orange')
True
You can update a Di instance from another as well:
>>> @di.register_factory('apple')
... def apple():
... return 'apple'
>>> other_di = Di()
>>> @other_di.register_factory('banana')
... def banana():
... return 'banana'
>>> di.update(other_di)
>>> @di.inject('apple', 'banana')
... def injected(apple, banana):
... return apple, banana
>>> injected() == ('apple', 'banana')
True