Welcome, guest | Sign In | My Account | Store | Cart

A Dependency Injection Container that works with normal classes. It fills given constructors or factory methods based on their named arguments.

Python, 148 lines
  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.

1 comment

Graham Poulter 12 years ago  # | flag

The 413268 one says:

The supported injection type is "Setter Injection", which basically means that
dependencies are expressed through attributes. There is another type of injection,
the "Constructor Injection", but that one builds heavily on static typing and can 
therefore not be employed in python (or at least I could not think of
 any elegant way to do it).

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.