"""
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()