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

Diff to Previous Revision

--- revision 1 2011-02-18 09:19:48
+++ revision 2 2011-02-20 16:31:08
@@ -4,11 +4,6 @@
 #   E-mail: hui.zh012@gmail.com
 #
 #   Distributed under the terms of the GPL (GNU Public License)
-#
-#   xy is free software; you can redistribute it and/or modify
-#   it under the terms of the GNU General Public License as published by
-#   the Free Software Foundation; either version 2 of the License, or
-#   (at your option) any later version.
 #
 #   This program is distributed in the hope that it will be useful,
 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -24,7 +19,7 @@
 from weakref import ref
 from functools import partial
 
-class Attribute(object):
+class BaseAttribute(object):
     """        
         usage:
             >>>class A(object):
@@ -101,23 +96,45 @@
     
     def __init__(self, name='', **kwargs):
         self.name = name
-        self.initer = self.build_initer(kwargs)
-        self.assigner = self.build_assigner(kwargs)
-        self.deleter = self.build_deleter(kwargs)
         self.dict = kwargs.copy()
-        self.values = {}
-        self.instances = {}
-
+        
+        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:
-            if id(instance) in self.values:
-                return self.values[id(instance)]
-            elif self.initer:
-                return self.assign(instance, self.initer(instance))
-            else:
-                raise AttributeError("Attribute is used before assigned")
+            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)
@@ -125,144 +142,138 @@
     def __delete__(self, instance):
         self.deleter(instance)
 
-    def build_initer(self, argdict):
-        if 'default' in argdict:
-            return lambda instance: argdict['default']
-
-        factory = argdict.get('factory', None)
-        if factory:
-            return lambda instance: factory()
-            
-        boundfactory = argdict.get('boundfactory', None)
-        if boundfactory:
-            return lambda instance: boundfactory(instance)
-
-        return None
-
-    def build_assigner(self, argdict):
-        if argdict.get('readonly', False):
+    ## 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:
-            ### wrap change hook
-            def add_change_hook(oldassigner):
-                onchanged = argdict.get('onchanged', None)
-                if onchanged:
-                    return self.hookchange(oldassigner, onchanged)
-                else:
-                    return oldassigner
-            #-- end / wrap change hook
-
-            ### wrap one time write check
-            def add_otw_check(oldassigner):
-                otw = argdict.get('otw', False)
-                if otw:
-                    def newassigner(instance, value):
-                        if id(instance) in self.values:
-                            raise AttributeError('Attribute could only be written once.')
-                        oldassigner(instance, value)
-                    return newassigner
-                else:
-                    return oldassigner
-
-            ### wrap validation check
-            def add_validation(oldassigner):
-                validator = argdict.get('validator', None)
-                boundvalidator = argdict.get('boundvalidator', None)
-                skip_valid_err = argdict.get('skip_valid_err', False)
+            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)
                 
-                if not (validator or boundvalidator):
-                    check_valid = None
-                else:
-                    def check_valid(value, instance):
-                        valid = (validator is None or validator(value)) \
-                                    and (boundvalidator is None or boundvalidator(value, instance))
-                        if not valid and not skip_valid_err:
-                            raise AttributeError("The value for attribute is not valid")
-                        return valid                        
+                try:
+                    newval = self.__get__(instance, None)
+                except:
+                    newval = BaseAttribute.undefined
                 
-                if check_valid:
-                    def newassigner(instance, value):
-                        if check_valid(value, instance):
-                            oldassigner(instance, value)
-                    return newassigner
-                else:
-                    return oldassigner
-
-            assigner = add_validation(
-                            add_otw_check(
-                                add_change_hook(self.assign)
-                            )
-                        )
-        return assigner
-    
-    def build_deleter(self, argdict):
-        if not argdict.get('delable', True):
-            def deleter(instance):
-                raise AttributeError("Cannot set value to read-only attribute")
-        else:
-            def add_existing_checker(func):
-                def newfunc(instance):
-                    if id(instance) in self.values:
-                        func(instance)
-                return newfunc
-
-            def deleter(instance):      
-                    self.clean(id(instance))
-            
-            if 'onchanged' in argdict:
-                deleter = self.hookchange(deleter, argdict['onchanged'])
-            deleter = add_existing_checker(deleter)
-            
-        return deleter
-
-    def assign(self, instance, value):
-        self.values[id(instance)] = value
-        if id(instance) not in self.instances:
-            try:
-                self.instances[id(instance)] = ref(instance, partial(self.clean, id(instance)))
-            except:
-                # can not clean up the attributes for the instances not able to weak ref
-                pass
-        return value
-
-    def clean(self, instanceid, obj=None):
-        if instanceid in self.instances:
-            self.instances.pop(instanceid)
-        if instanceid in self.values:
-            self.values.pop(instanceid)
-            
-    def hookchange(self, action, hook):
-        def newaction(*args):
-            instance = args[0]
-            try:
-                oldval = self.__get__(instance, None)
-            except:
-                oldval = Attribute.undefined
-                
-            action(*args)
-            
-            try:
-                newval = self.__get__(instance, None)
-            except:
-                newval = Attribute.undefined
-            
-            if oldval != newval:
-                hook(instance, self, oldval, newval)
-                
-        return newaction 
+                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):
-    """ decorator. 
-            class A(object):
-                @attribute(**kwargs)
-                def value(self):
-                    return something
-        
-            this is equal to:
-            class A(object):
-                value = Attribute('value', **kwargs)
-    """
     assert 'boundfactory' not in kwargs \
             and ( not args
                   or (len(args)==1 and not kwargs)
@@ -277,3 +288,25 @@
                              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