Welcome, guest | Sign In | My Account | Store | Cart
#!/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

History