#!/usr/local/bin/python
'''
Sample test code:

>>> f = Foo()         
>>> print f.x
1
>>> print f.y
2
>>> print f.z
2007-08-05 00:00:00
>>> print vars(f)
{'_z': datetime.datetime(2007, 8, 5, 0, 0), '_y': 2, '_x': 1}
>>> 
>>> b = Bar()
>>> print b.x
1
>>> print b.y
2
>>> print b.z
2007-08-05 00:00:00
>>> print vars(b)
{'_z': datetime.datetime(2007, 8, 5, 0, 0), '_y': 2, '_x': 1}
>>> 
>>> z = Baz()
>>> print z.x
1
>>> print z.y
2
>>> print z.z
2007-08-05 00:00:00
>>> print vars(z)
{}
'''

from datetime import datetime


#------------------------------------------------------------------
def simple_curry(f, *args, **kws):
    '''Simple helper function for pre-bundling a function with some 
    known arguments.'''
    def inner_curry(self):
        return f(self, *args, **kws)
        
    return inner_curry

#==================================================================
class CacheProperty(object):
    '''Caching mechanism that uses the supplied instance's 
    namespace for storage.'''

    #--------------------------------------------------------------
    def __init__(self, cache_var_name, initializer):
        self.initializer = initializer
        self.var = cache_var_name

    #--------------------------------------------------------------
    def __set__(self, *args):
        raise AttributeError, u'Read-only attribute'
        
    #--------------------------------------------------------------
    def __get__(self, instance, cls):
        if instance is None:
            raise AttributeError(
                u'%s must be accessed via instance' % (self.var,)
            )

        if not hasattr(instance, self.var):
            # print '... No Cache'
            if callable(self.initializer):
                setattr(instance, self.var, self.initializer(instance))
            else:
                setattr(instance, self.var, self.initializer)
            
        return getattr(instance, self.var)


#==================================================================
class CacheProperty2(object):
    '''Caching descriptor that stores the cached data itself.'''

    __NIL = object()

    #--------------------------------------------------------------
    def __init__(self, initializer):
        self.initializer = initializer
        self.cache = self.__NIL

    #--------------------------------------------------------------
    def __set__(self, *args):
        raise AttributeError, u'Read-only attribute'
        
    #--------------------------------------------------------------
    def __get__(self, instance, cls):
        if instance is None:
            raise AttributeError, u'Instance level access only'

        if self.cache == self.__NIL:
            # print '... No Cache'
            if callable(self.initializer):
                self.cache = self.initializer(instance)
            else:
                self.cache = self.initializer
            
        return self.cache

        
#==================================================================
class Foo(object):
    '''A typical representation of the attribute caching mechanism 
    in verbose fashion.'''

    #--------------------------------------------------------------
    def expensive_function(self, arg):
        # do something expensive here
        return arg
        
    #--------------------------------------------------------------
    def _get_x(self):
        if not hasattr(self, '_x'):
            self._x = self.expensive_function(1)
            
        return self._x

    #--------------------------------------------------------------
    def _get_y(self):
        if not hasattr(self, '_y'):
            self._y = self.expensive_function(2)
            
        return self._y
        
    #--------------------------------------------------------------
    def _get_z(self):
        if not hasattr(self, '_z'):
            self._z = datetime(2007,8,5)
            
        return self._z
        
    #--------------------------------------------------------------
    x = property(_get_x)
    y = property(_get_y)
    z = property(_get_z)

    
#==================================================================
class Bar(object):
    '''Exhibit "A" for caching attributes, in this case the data is
    stored directly within the instance.'''

    #--------------------------------------------------------------
    def expensive_function(self, arg):
        # do something expensive here
        return arg

    #--------------------------------------------------------------
    x = CacheProperty('_x', simple_curry(expensive_function, 1))
    y = CacheProperty('_y', simple_curry(expensive_function, 2))
    z = CacheProperty('_z', lambda self: datetime(2007,8,5))


#==================================================================
class Baz(object):
    '''Exhibit "B" for caching attributes, in this case the data is
    contained within the descriptor.'''

    #--------------------------------------------------------------
    def expensive_function(self, arg):
        # do something expensive here
        return arg

    #--------------------------------------------------------------
    x = CacheProperty2(simple_curry(expensive_function, 1))
    y = CacheProperty2(simple_curry(expensive_function, 2))
    z = CacheProperty2(lambda self: datetime(2007,8,5))


#------------------------------------------------------------------
def test():    
    # doctest does not seem to work in 2.3
    import doctest
    doctest.testmod()

    
###################################################################
if __name__ == '__main__':
    test()