Cached model property decorator (like @property)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from django.core.cache import cache
from django.db import models

#Inspiration snippet:
#
#(I was thinking of using this at the end, but the issues of nesting
#these are not worth the headache)
#
#class cached_property(object):
#    '''
#    A read-only @property that is only evaluated once per python object. The value 
#    is cached on the object itself rather than the function or class; this should prevent
#    memory leakage.
#    
#    Model instances are re-instantiated for each request, so this should be the last line
#    of defense in a django context.
#    '''
#    def __init__(self, fget, doc=None):
#        self.fget = fget
#        self.__doc__ = doc or fget.__doc__
#        self.__name__ = fget.__name__
#        self.__module__ = fget.__module__
#
#    def __get__(self, obj, cls):
#        if obj is None:
#            return self
#        obj.__dict__[self.__name__] = result = self.fget(obj)
#        return result
    
class cached_model_property(object):
    """
    To use inside the class declaration for SomeClass
    1. First make a subclass of of this:
    
    class cached_someclass_property(cached_model_property):
        model_name = "SomeClass"
        
    2. Use as if this were a @property decorator inside SomeClass
    
    This should of course only be used if the property is essentially
    static. If the property should return a given model instance, where
    the instance id is static but the contents could change, see gives_a
    decorator below.
    """
    model_name = "<generic>"
    
    def __init__(self, fget, doc=None):
        self.fget = fget
        self.__doc__ = doc or fget.__doc__
        self.__name__ = fget.__name__
        self.__module__ = fget.__module__

    @staticmethod
    def pickle(cuke):
        """Pickling/unpickling is used before saving to the django (outermost) cache."""
        return cuke
    
    @staticmethod
    def unpickle(dill):
        return dill
    
    def __get__(self, obj, cls):
        """Use the descriptor protocol to act as a property.
        
        3 levels of caching implemented:
        outermost: django cache
        middle: obj._cache (for multiple properties on a single object)
        innermost: replace the attribute on this object, so we can entirely avoid
            running this function a second time.
        """
        
        #First, handle the "unbound" case
        if obj is None:
            return self
        
        key = ':'.join((self.model_name, str(obj.id)))
        
        try:
            #middle cache pull
            obj_cache = obj._cache
        except AttributeError:
            #outermost cache pull
            obj_cache = cache.get(key)
            if obj_cache is None:
                obj_cache = {}
                #cache.set(key, {}) #technically, this might improve multithreaded performance, but not worth it.
            #middle cache push
            #since this is a mutable reference, further changes below will apply
            obj._cache = obj_cache
        
        #unpack property from dict of all cached properties for this object
        try:
            result = self.unpickle(obj_cache[self.__name__])
        except:
            #missing - calculate value and add to cache
            result = self.fget(obj)
            obj_cache[self.__name__] = self.pickle(result)
            #outermost cache push
            cache.set(key, obj_cache)
        
        #innermost cache push (no pull because this function itself is replaced)
        obj.__dict__[self.__name__] = result = self.fget(obj)
        
        #return
        return result

MODELS = {}
class cached_typed_model_property(cached_model_property):
    @staticmethod
    def pickle(cuke):
        if isinstance(cuke, models.Model):
            cls = cuke.__class__
            name = cls.__name__
            MODELS[name] = cls
            return (name, cuke.id)
        try: 
            if issubclass(cuke, models.Model):
                name = cuke.__name__
                MODELS[name] = cuke
                return (name,)
        except TypeError: #issubclass is annoying that way
            pass
        return cuke #for None
    
    @staticmethod
    def unpickle(dill):
        if isinstance(dill, tuple):
            if len(dill) == 2:
                return MODELS[dill[0]].objects.get(id=dill[1]) 
                #exceptions here just mean we run the function again, which will end up adding the model to MODELS
            if len(dill) == 1:
                return MODELS[dill[0]]
        return dill
    
class cached_uprop(cached_typed_model_property):
    model_name = "Users"

More like this

  1. Cache Decorator by ericmoritz 5 years, 4 months ago
  2. Extended cacheable callables and properties by gsakkis 3 years, 10 months ago
  3. Cachable Class Method Decorator by amitu 5 years, 6 months ago
  4. Property Attributes in Memcache by ori 2 years, 5 months ago
  5. Application-independent permalink to admin url for an object by dchandek 6 years, 1 month ago

Comments

(Forgotten your password?)