A Dependency Injection Container that works with normal classes. It fills given constructors or factory methods based on their named arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | """
Non-invasive Dependency Injection Container.
It fills given constructors or factory methods
based on their named arguments.
See the demo usage at the end of file.
"""
import logging
NO_DEFAULT = "NO_DEFAULT"
class Context:
"""A depencency injection container.
It detects the needed dependencies based on arguments of factories.
"""
def __init__(self):
"""Creates empty context.
"""
self.instances = {}
self.factories = {}
def register(self, property, factory, *factory_args, **factory_kw):
"""Registers factory for the given property name.
The factory could be a callable or a raw value.
Arguments of the factory will be searched
inside the context by their name.
The factory_args and factory_kw allow
to specify extra arguments for the factory.
"""
if (factory_args or factory_kw) and not callable(factory):
raise ValueError(
"Only callable factory supports extra args: %s, %s(%s, %s)"
% (property, factory, factory_args, factory_kw))
self.factories[property] = factory, factory_args, factory_kw
def get(self, property):
"""Lookups the given property name in context.
Raises KeyError when no such property is found.
"""
if property not in self.factories:
raise KeyError("No factory for: %s", property)
if property in self.instances:
return self.instances[property]
factory_spec = self.factories[property]
instance = self._instantiate(property, *factory_spec)
self.instances[property] = instance
return instance
def get_all(self):
"""Returns instances of all properties.
"""
return [self.get(name) for name in self.factories.iterkeys()]
def build(self, factory, *factory_args, **factory_kw):
"""Invokes the given factory to build a configured instance.
"""
return self._instantiate("", factory, factory_args, factory_kw)
def _instantiate(self, name, factory, factory_args, factory_kw):
if not callable(factory):
logging.debug("Property %r: %s", name, factory)
return factory
kwargs = self._prepare_kwargs(factory, factory_args, factory_kw)
logging.debug("Property %r: %s(%s, %s)", name, factory.__name__,
factory_args, kwargs)
return factory(*factory_args, **kwargs)
def _prepare_kwargs(self, factory, factory_args, factory_kw):
"""Returns keyword arguments usable for the given factory.
The factory_kw could specify explicit keyword values.
"""
defaults = get_argdefaults(factory, len(factory_args))
for arg, default in defaults.iteritems():
if arg in factory_kw:
continue
elif arg in self.factories:
defaults[arg] = self.get(arg)
elif default is NO_DEFAULT:
raise KeyError("No factory for arg: %s" % arg)
defaults.update(factory_kw)
return defaults
def get_argdefaults(factory, num_skipped=0):
"""Returns dict of (arg_name, default_value) pairs.
The default_value could be NO_DEFAULT
when no default was specified.
"""
args, defaults = _getargspec(factory)
if defaults is not None:
num_without_defaults = len(args) - len(defaults)
default_values = (NO_DEFAULT,) * num_without_defaults + defaults
else:
default_values = (NO_DEFAULT,) * len(args)
return dict(zip(args, default_values)[num_skipped:])
def _getargspec(factory):
"""Describes needed arguments for the given factory.
Returns tuple (args, defaults) with argument names
and default values for args tail.
"""
import inspect
if inspect.isclass(factory):
factory = factory.__init__
#logging.debug("Inspecting %r", factory)
args, vargs, vkw, defaults = inspect.getargspec(factory)
if inspect.ismethod(factory):
args = args[1:]
return args, defaults
if __name__ == "__main__":
class Demo:
def __init__(self, title, user, console):
self.title = title
self.user = user
self.console = console
def say_hello(self):
self.console.println("*** IoC Demo ***")
self.console.println(self.title)
self.console.println("Hello %s" % self.user)
class Console:
def __init__(self, prefix=""):
self.prefix = prefix
def println(self, message):
print self.prefix, message
ctx = Context()
ctx.register("user", "some user")
ctx.register("console", Console, "-->")
demo = ctx.build(Demo, title="Inversion of Control")
demo.say_hello()
|
This dependency injection does not force you to change your classes. It will resolve constructor arguments based on their names. So a constructor like this is fine:
def __init__(self, title, user, console):
It is still possible to supply a different implementation to a named argument:
ctx.register("different_console", Console, prefix="different:")
ctx.build(Demo, title="A title", console=ctx.get("different_console"))
It was inspired by the declarative Dependency Injection The Python Way: Recipe 413268.
The 413268 one says:
However the "Non-Invasive Dependency Injection" is constructor injection, but without any typing. Matching is done purely on constructor parameter name, which will cause aliasing of names where unrelated components mean different things by the same-name constructor parameter.
I think you will have more luck building on the Python 3 function annotations where constructors can specify the types they expect.