Tweaked AttributeMapper._fill_cache to also store values on the Attribute instance...
[philo.git] / philo / utils / entities.py
1 from functools import partial
2 from UserDict import DictMixin
3
4 from django.db import models
5 from django.contrib.contenttypes.models import ContentType
6
7 from philo.utils.lazycompat import SimpleLazyObject
8
9
10 ### AttributeMappers
11
12
13 class AttributeMapper(object, DictMixin):
14         """
15         Given an :class:`~philo.models.base.Entity` subclass instance, this class allows dictionary-style access to the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s. In order to prevent unnecessary queries, the :class:`AttributeMapper` will cache all :class:`~philo.models.base.Attribute`\ s and the associated python values when it is first accessed.
16         
17         :param entity: The :class:`~philo.models.base.Entity` subclass instance whose :class:`~philo.models.base.Attribute`\ s will be made accessible.
18         
19         """
20         def __init__(self, entity):
21                 self.entity = entity
22                 self.clear_cache()
23         
24         def __getitem__(self, key):
25                 """Returns the ultimate python value of the :class:`~philo.models.base.Attribute` with the given ``key`` from the cache, populating the cache if necessary."""
26                 if not self._cache_filled:
27                         self._fill_cache()
28                 return self._cache[key]
29         
30         def __setitem__(self, key, value):
31                 """Given a python value, sets the value of the :class:`~philo.models.base.Attribute` with the given ``key`` to that value."""
32                 # Prevent circular import.
33                 from philo.models.base import JSONValue, ForeignKeyValue, ManyToManyValue, Attribute
34                 old_attr = self.get_attribute(key)
35                 if old_attr and old_attr.entity_content_type == ContentType.objects.get_for_model(self.entity) and old_attr.entity_object_id == self.entity.pk:
36                         attribute = old_attr
37                 else:
38                         attribute = Attribute(key=key)
39                         attribute.entity = self.entity
40                         attribute.full_clean()
41                 
42                 if isinstance(value, models.query.QuerySet):
43                         value_class = ManyToManyValue
44                 elif isinstance(value, models.Model):
45                         value_class = ForeignKeyValue
46                 else:
47                         value_class = JSONValue
48                 
49                 attribute.set_value(value=value, value_class=value_class)
50                 self._cache[key] = attribute.value.value
51                 self._attributes_cache[key] = attribute
52         
53         def get_attributes(self):
54                 """Returns an iterable of all of the :class:`~philo.models.base.Entity`'s :class:`~philo.models.base.Attribute`\ s."""
55                 return self.entity.attribute_set.all()
56         
57         def get_attribute(self, key, default=None):
58                 """Returns the :class:`~philo.models.base.Attribute` instance with the given ``key`` from the cache, populating the cache if necessary, or ``default`` if no such attribute is found."""
59                 if not self._cache_filled:
60                         self._fill_cache()
61                 return self._attributes_cache.get(key, default)
62         
63         def keys(self):
64                 """Returns the keys from the cache, first populating the cache if necessary."""
65                 if not self._cache_filled:
66                         self._fill_cache()
67                 return self._cache.keys()
68         
69         def items(self):
70                 """Returns the items from the cache, first populating the cache if necessary."""
71                 if not self._cache_filled:
72                         self._fill_cache()
73                 return self._cache.items()
74         
75         def values(self):
76                 """Returns the values from the cache, first populating the cache if necessary."""
77                 if not self._cache_filled:
78                         self._fill_cache()
79                 return self._cache.values()
80         
81         def _fill_cache(self):
82                 if self._cache_filled:
83                         return
84                 
85                 attributes = self.get_attributes()
86                 value_lookups = {}
87                 
88                 for a in attributes:
89                         value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
90                         self._attributes_cache[a.key] = a
91                 
92                 values_bulk = dict(((ct_pk, SimpleLazyObject(partial(ContentType.objects.get_for_id(ct_pk).model_class().objects.in_bulk, pks))) for ct_pk, pks in value_lookups.items()))
93                 
94                 cache = {}
95                 
96                 for a in attributes:
97                         cache[a.key] = SimpleLazyObject(partial(self._lazy_value_from_bulk, values_bulk, a))
98                         a._value_cache = cache[a.key]
99                 
100                 self._cache.update(cache)
101                 self._cache_filled = True
102         
103         def _lazy_value_from_bulk(self, bulk, attribute):
104                 v = bulk[attribute.value_content_type_id].get(attribute.value_object_id)
105                 return getattr(v, 'value', None)
106         
107         def clear_cache(self):
108                 """Clears the cache."""
109                 self._cache = {}
110                 self._attributes_cache = {}
111                 self._cache_filled = False
112
113
114 class LazyAttributeMapperMixin(object):
115         """In some cases, it may be that only one attribute value needs to be fetched. In this case, it is more efficient to avoid populating the cache whenever possible. This mixin overrides the :meth:`__getitem__` and :meth:`get_attribute` methods to prevent their populating the cache. If the cache has been populated (i.e. through :meth:`keys`, :meth:`values`, etc.), then the value or attribute will simply be returned from the cache."""
116         def __getitem__(self, key):
117                 if key not in self._cache and not self._cache_filled:
118                         self._add_to_cache(key)
119                 return self._cache[key]
120         
121         def get_attribute(self, key, default=None):
122                 if key not in self._attributes_cache and not self._cache_filled:
123                         self._add_to_cache(key)
124                 return self._attributes_cache.get(key, default)
125         
126         def _raw_get_attribute(self, key):
127                 return self.get_attributes().get(key=key)
128         
129         def _add_to_cache(self, key):
130                 from philo.models.base import Attribute
131                 try:
132                         attr = self._raw_get_attribute(key)
133                 except Attribute.DoesNotExist:
134                         raise KeyError
135                 else:
136                         val = getattr(attr.value, 'value', None)
137                         self._cache[key] = val
138                         self._attributes_cache[key] = attr
139
140
141 class LazyAttributeMapper(LazyAttributeMapperMixin, AttributeMapper):
142         def get_attributes(self):
143                 return super(LazyAttributeMapper, self).get_attributes().exclude(key__in=self._cache.keys())
144
145
146 class TreeAttributeMapper(AttributeMapper):
147         """The :class:`~philo.models.base.TreeEntity` class allows the inheritance of :class:`~philo.models.base.Attribute`\ s down the tree. This mapper will return the most recently declared :class:`~philo.models.base.Attribute` among the :class:`~philo.models.base.TreeEntity`'s ancestors or set an attribute on the :class:`~philo.models.base.Entity` it is attached to."""
148         def get_attributes(self):
149                 """Returns a list of :class:`~philo.models.base.Attribute`\ s sorted by increasing parent level. When used to populate the cache, this will cause :class:`~philo.models.base.Attribute`\ s on the root to be overwritten by those on its children, etc."""
150                 from philo.models import Attribute
151                 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
152                 ct = ContentType.objects.get_for_model(self.entity)
153                 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys())
154                 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
155
156
157 class LazyTreeAttributeMapper(LazyAttributeMapperMixin, TreeAttributeMapper):
158         def get_attributes(self):
159                 from philo.models import Attribute
160                 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
161                 ct = ContentType.objects.get_for_model(self.entity)
162                 attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys()).exclude(key__in=self._cache.keys())
163                 return sorted(attrs, key=lambda x: ancestors[x.entity_object_id])
164         
165         def _raw_get_attribute(self, key):
166                 from philo.models import Attribute
167                 ancestors = dict(self.entity.get_ancestors(include_self=True).values_list('pk', 'level'))
168                 ct = ContentType.objects.get_for_model(self.entity)
169                 try:
170                         attrs = Attribute.objects.filter(entity_content_type=ct, entity_object_id__in=ancestors.keys(), key=key)
171                         sorted_attrs = sorted(attrs, key=lambda x: ancestors[x.entity_object_id], reverse=True)
172                         return sorted_attrs[0]
173                 except IndexError:
174                         raise Attribute.DoesNotExist
175
176
177 class PassthroughAttributeMapper(AttributeMapper):
178         """
179         Given an iterable of :class:`Entities <philo.models.base.Entity>`, this mapper will fetch an :class:`AttributeMapper` for each one. Lookups will return the value from the first :class:`AttributeMapper` which has an entry for a given key. Assignments will be made to the first :class:`.Entity` in the iterable.
180         
181         :param entities: An iterable of :class:`.Entity` subclass instances.
182         
183         """
184         def __init__(self, entities):
185                 self._attributes = [e.attributes for e in entities]
186                 super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
187         
188         def _fill_cache(self):
189                 if self._cache_filled:
190                         return
191                 
192                 for a in reversed(self._attributes):
193                         a._fill_cache()
194                         self._attributes_cache.update(a._attributes_cache)
195                         self._cache.update(a._cache)
196                 
197                 self._cache_filled = True
198         
199         def get_attributes(self):
200                 raise NotImplementedError
201         
202         def clear_cache(self):
203                 super(PassthroughAttributeMapper, self).clear_cache()
204                 for a in self._attributes:
205                         a.clear_cache()
206
207
208 class LazyPassthroughAttributeMapper(LazyAttributeMapperMixin, PassthroughAttributeMapper):
209         """The :class:`LazyPassthroughAttributeMapper` is lazy in that it tries to avoid accessing the :class:`AttributeMapper`\ s that it uses for lookups. However, those :class:`AttributeMapper`\ s may or may not be lazy themselves."""
210         def _raw_get_attribute(self, key):
211                 from philo.models import Attribute
212                 for a in self._attributes:
213                         attr = a.get_attribute(key)
214                         if attr is not None:
215                                 return attr
216                 raise Attribute.DoesNotExist