#!/usr/bin/env python # # Copyright 2010- Hui Zhang # E-mail: hui.zh012@gmail.com # # Distributed under the terms of the GPL (GNU Public License) # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA __all__ = ["Attribute", "attribute", ] from weakref import ref from functools import partial class BaseAttribute(object): """ usage: >>>class A(object): val = Attriubte('val', factory=int, validator=lambda v: v in range(3,7), onchanged=somfunc, readonly=True, otw=False, ... # for more please check the code ) >>> a = A() >>> print a.val 0 >>> a.val=1; print a.val 1 >>> del a.val; print a.val 0 notes: 'del a.val' does not delete the attr from instance. it just clean the assigned value for this attr. parameters: * name : the attr's name, default is '' * readonly : define if the attr could be assigned. notes: readonly does not impact behavior of 'del a.val' * delable : define if the value could be deleted * default : the default value. * factory, boundfactory define the initializer for the attr. -factory takes no parametor -boundfactory would get the instance as the only parameter. * validator, boundvalidator, skip_valid_err validate the value assigned to the attr. if validate failed, - the value would not be assigned - a exception would be raised if skip_valid_err is False. The validation would not be applied for the value initialized by: default, factory, boundfactory * onchanged define a hook while attr value changed. the following 4 args would be given to the hook: (instance, Attribute instance, old value, new value) * otw one time write. e.g: class A(object): v = Attribute(otw=True) class B(A): def __init__(self, value): self.v = value b = B(99) print b.v -> 99 b.v = 100 -> exception raised [initialization:] initializers are prioritized as "default, factory, boundfactory" if no any defined and value assinged, a exception would be raised while you try to read this attr. """ undefined = object() def __init__(self, name='', **kwargs): self.name = name self.dict = kwargs.copy() self.skip_valid_err = self.getarg('skip_valid_err', True) self.build_initer() self.build_assigner() self.build_deleter() onchanged = self.getarg('onchanged', None) if onchanged: self.onchanged(onchanged) ## read, write and erase the value from/to the storage ## to be implemented by sub classes def _read_(self, instance): """ read the value from the storage raise exception if the value is not initialized """ def _write_(self, instance, value): """ store the value to the storage """ def _earase_(self, instance): """ erase the value from the storage """ def __get__(self, instance, owner): if instance is None: return self else: try: return self._read_(instance) except: if self.initer: self._write_(instance, self.initer(instance)) return self._read_(instance) raise AttributeError("Attribute is used before initialized.") def __set__(self, instance, value): self.assigner(instance, value) def __delete__(self, instance): self.deleter(instance) ## build the initer, assigner and deleter def getarg(self, name, default): if name in self.dict: return self.dict.pop(name) else: return default def build_initer(self): self.initer = None _dummy = object default = self.getarg('default', _dummy) if default is not _dummy: self.initer = lambda instance: default return factory = self.getarg('factory', _dummy) if factory is not _dummy: self.initer = lambda instance: factory() return boundfactory = self.getarg('boundfactory', _dummy) if boundfactory is not _dummy: self.initer = lambda instance: boundfactory(instance) return def build_assigner(self): readonly = self.getarg('readonly', False) if readonly: def assigner(instance, value): raise AttributeError("Cannot set value to read-only attribute") self.assigner = assigner return self.assigner = self._write_ otw = self.getarg('otw', False) if otw: self.set_otw() validator = self.getarg('validator', None) if validator: self.validator(validator) boundvalidator = self.getarg('boundvalidator', None) if boundvalidator: self.boundvalidator(boundvalidator) def build_deleter(self): delable = self.getarg('delable', True) if not delable: def deleter(instance): raise AttributeError("Cannot delete attribute") self.deleter = deleter else: self.deleter = self._earase_ ## handler wrappers def set_otw(self): oldassinger = self.assigner def assigner(instance, value): try: self._read_(instance) inited = True except: inited = False if inited: raise AttributeError('Attribute could only be written once.') oldassinger(instance, value) self.assigner = assigner # be able to use as decorator def onchanged(self, hook): def wrap(func): def newfunc(*args): instance = args[0] try: oldval = self.__get__(instance, None) except: oldval = BaseAttribute.undefined func(*args) try: newval = self.__get__(instance, None) except: newval = BaseAttribute.undefined if oldval != newval: hook(instance, self, oldval, newval) return newfunc self.assigner = wrap(self.assigner) self.deleter = wrap(self.deleter) return self def validator(self, hook): oldassinger = self.assigner def assigner(instance, value): if hook(value): oldassinger(instance, value) elif not self.skip_valid_err: raise AttributeError("The value for attribute is not valid") self.assigner = assigner return self def boundvalidator(self, hook): oldassinger = self.assigner def assigner(instance, value): if hook(instance, value): oldassinger(instance, value) elif not self.skip_valid_err: raise AttributeError("The value for attribute is not valid") self.assigner = assigner return self class Attribute(BaseAttribute): def __init__(self, name='', **kwargs): super(Attribute, self).__init__(name, **kwargs) self.__values = weakkeymap() # Recipe 577580: weak reference map def _read_(self, instance): return self.__values.get(instance) def _write_(self, instance, value): self.__values.set(instance, value) def _earase_(self, instance): self.__values.remove(instance) def attribute(*args, **kwargs): assert 'boundfactory' not in kwargs \ and ( not args or (len(args)==1 and not kwargs) ) if args: return Attribute(args[0].func_name, boundfactory=args[0]) else: def deco(func): return Attribute(func.func_name, boundfactory=func, **kwargs) return deco ## demo code #class A(object): # @attribute(skip_valid_err=False) # def value(self): # return 10000 # # @value.boundvalidator # def value(self, v): # return v < 20 # # value.validator(lambda v: v>5) # # @value.onchanged # def value(self, *args): # print args # #a = A() #print a.value #a.value = 9 #print a.value #a.value = 100