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.