3
\$\begingroup\$

I'm looking into having a simple dependency injection container in Python. I've Googled on it but so far the solutions that I've found are quite huge and use constructs such as decorators and so on which I'm not so fond of.

So, inspired (or copying) the Twittee IoC written by Fabien Potencier in PHP I wrote this script. I'd like to get a review on it

class Pymple(object):

    def __init__(self):
        self._values = {}

    def register(self, key, value, as_singleton=False):
        self._values[key] = value

        if as_singleton:
            self._values[key] = self.resolve(key)

    def resolve(self, key):
        if not key in self._values:
            raise ValueError('Value "%s" is not registered.' % key)

        return self._values[key](self) if callable(self._values[key]) else self._values[key]

This could latter be used as

class Sum:

    def __ini__():
        pass

    def sum(self, a, b):
        return a + b

class Multiply:
    def __init__(self, sum):
        self.sum = sum

    def multiply(self, a, b):
        rv = 0
        for k in range(0, a):
            rv = self.sum.sum(rv, b)
        return rv

container = Pymple()
container.register('sum', lambda c: Sum(), True)
container.register('multiply', lambda c: Multiply(c.resolve('sum')))
\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I would expect your Pymple class to be a bit simpler to use. At least for the retrieval of values something like this:

container = Pymple()
container.register('sum', lambda c: Sum(), True)
container.sum.sum(1, 2)  # returns 3

or like this:

container = Pymple()
container.sum = lambda c: Sum()
container.sum.sum(1, 2)  # returns 3

feels more natural to me. You can even provide the two versions, register being able to take optional parameters that __setattr__ can't.

So I would start with something like:

class Pymple(object):
    """Some usefull docstring here"""

    def __init__(self):
        super().__setattr__('_values', {})

    def register(self, key, value, as_singleton=False):
        self._values[key] = value

        if as_singleton:
            self._values[key] = getattr(self, key)

    def __setattr__(self, key, value):
        self.register(key, value, True)

    def __getattr__(self, key):
        if key not in self._values:
            raise AttributeError("'{}' is not registered.".format(key))

        attribute = self._values[key]
        return attribute(self) if callable(attribute) else attribute

Note the change in the type of error raised since we are dealing with attributes now. You also need to deal with the modified version of __setattr__ so you can't directly assign {} to self._values.


Now about the way you return your values… Given the above interface, I am expecting that:

def email(container):
    mail = "{c.name}.{c.lastname}@{c.domain}".format(c=container)
    def functor():
        return '[{}] {}'.format(time.time(), mail)
    return functor

container = Pymple()
container.name = lambda c: 'spam'
container.lastname = lambda c: 'eggs'
container.domain = lambda c: 'bacon'
container.frozen_email = email
container.name = lambda c: 'foo'
container.register('email', email)
print(container.frozen_email())
print(container.email())
container.domain = lambda c: 'example.org'
print(container.frozen_email())
print(container.email())

would print:

[1479207663.6114736] spam.eggs@bacon
[1479207663.7469463] foo.eggs@bacon
[1479207663.8756231] spam.eggs@bacon
[1479207663.9963526] [email protected]

However it will fail as, even though frozen_email is stored "as a singleton", the callable returned by email will be called with the container as argument and it does not expect it.

Two other things that can be observed from this example:

  • as_singleton is more an as_constant than anything else;
  • there is too much boilerplate required when storing a constant.

I think that, instead of trying to know if you have stored a constant or a function in the getter, you should wrap your constants into functions taking the container as parameter into the setter. And the getter will blindly perform a function call:

class Pymple(object):
    """Some usefull docstring here"""

    def __init__(self):
        super().__setattr__('_values', {})

    def register(self, key, value, constant=False):
        if constant:
            self._values[key] = lambda c: value
        else:
            self._values[key] = value

    def __setattr__(self, key, value):
        self.register(key, value, True)

    def __getattr__(self, key):
        if key not in self._values:
            raise AttributeError("'{}' is not registered.".format(key))

        return self._values[key](self)

And the above example is now:

container = Pymple()
container.name = 'spam'
container.lastname = 'eggs'
container.domain = 'bacon'
container.frozen_email = email(container)
container.name = 'foo'
container.register('email', email)
print(container.frozen_email())
print(container.email())
container.domain = 'example.org'
print(container.frozen_email())
print(container.email())

Now that we simplified the registration of "constant" values (be they callable or not), let's see if we can't do the same with "variable" ones. For one, register offers a way to use your Pymple class with existing functions so let's keep it. For two, you can easily use __setattr__ to store variable attributes as well as constant ones if you provide a base class that your user can inherit from to "mark" a value as variable:

class PympleVariable(object):
    def __init__(self, container):
        raise NotImplementedError


class Pymple(object):
    def __init__(self):
        super().__setattr__('_values', {})

    def register(self, key, value, constant=False):
        if constant:
            self._value[key] = lambda c: value
        else:
            self._value[key] = value

    def __setattr__(self, key, value):
        try:
            variable = issubclass(value, PympleVariable)
        except TypeError:
            variable = False

        self.register(key, value, not variable)

    def __getattr__(self, key):
        if key not in self._values:
            raise AttributeError("'{}' is not registered.".format(key))

        return self._values[key](self)

So you can write:

class email(PympleVariable):
    def __init__(self, container):
        self.mail = "{c.name}.{c.lastname}@{c.domain}".format(c=container)

    def __call__(self):
        return '[{}] {}'.format(time.time(), self.mail)

container = Pymple()
container.name = 'spam'
container.lastname = 'eggs'
container.domain = 'bacon'
container.frozen_email = email(container)
container.name = 'foo'
container.email = email
print(container.frozen_email())
print(container.email())
container.domain = 'example.org'
print(container.frozen_email())
print(container.email())

and get a pretty nice interface.

\$\endgroup\$
9
  • \$\begingroup\$ Thank you so much. I'll not take the PympleVariable thing though, as I think it goes back into being unnecessarily complicated again. But I'm definitely taking the tip on using __getattr__ and __setattr__ \$\endgroup\$ Commented Nov 20, 2016 at 16:52
  • \$\begingroup\$ My bad, I meant to mark as answer right after leaving the comment. \$\endgroup\$ Commented Nov 20, 2016 at 17:20
  • \$\begingroup\$ Actually with this code you hit a recursion error in __getattr__ when doing if key not in self._values: \$\endgroup\$ Commented Nov 20, 2016 at 17:29
  • \$\begingroup\$ @PauloPhagula Why so ? self._values is perfectly defined so won't trigger __getattr__ again. \$\endgroup\$ Commented Nov 20, 2016 at 17:32
  • \$\begingroup\$ So instead of def __init__: self._values = {} it should be def __init__ (self): self.__dict__['_values'] = {} Because __setattr__ is always called every time we do something like self.foo = bar and __getattr__ every time we want to get the value a property like self._value which is being done in if key not in self._values:. \$\endgroup\$ Commented Nov 20, 2016 at 18:08
1
\$\begingroup\$

There are not much of things to review. I don't really like a way you are going to call sum and multiply. But I think this is just an example. However few things here.

Remove __ini__ from Sum since it's not used and does nothing.

Instead of not key in use key not in

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.