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