+from functools import partial
from UserDict import DictMixin
from django.db import models
from django.contrib.contenttypes.models import ContentType
+from philo.utils.lazycompat import SimpleLazyObject
+
### AttributeMappers
def __getitem__(self, key):
"""Returns the ultimate python value of the :class:`~philo.models.base.Attribute` with the given ``key`` from the cache, populating the cache if necessary."""
- if not self._cache_populated:
- self._populate_cache()
+ if not self._cache_filled:
+ self._fill_cache()
return self._cache[key]
def __setitem__(self, key, value):
def get_attribute(self, key, default=None):
"""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."""
- if not self._cache_populated:
- self._populate_cache()
+ if not self._cache_filled:
+ self._fill_cache()
return self._attributes_cache.get(key, default)
def keys(self):
"""Returns the keys from the cache, first populating the cache if necessary."""
- if not self._cache_populated:
- self._populate_cache()
+ if not self._cache_filled:
+ self._fill_cache()
return self._cache.keys()
def items(self):
"""Returns the items from the cache, first populating the cache if necessary."""
- if not self._cache_populated:
- self._populate_cache()
+ if not self._cache_filled:
+ self._fill_cache()
return self._cache.items()
def values(self):
"""Returns the values from the cache, first populating the cache if necessary."""
- if not self._cache_populated:
- self._populate_cache()
+ if not self._cache_filled:
+ self._fill_cache()
return self._cache.values()
- def _populate_cache(self):
- if self._cache_populated:
+ def _fill_cache(self):
+ if self._cache_filled:
return
attributes = self.get_attributes()
value_lookups = {}
for a in attributes:
- value_lookups.setdefault(a.value_content_type, []).append(a.value_object_id)
+ value_lookups.setdefault(a.value_content_type_id, []).append(a.value_object_id)
self._attributes_cache[a.key] = a
- values_bulk = {}
+ 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()))
+
+ cache = {}
- for ct, pks in value_lookups.items():
- values_bulk[ct] = ct.model_class().objects.in_bulk(pks)
+ for a in attributes:
+ cache[a.key] = SimpleLazyObject(partial(self._lazy_value_from_bulk, values_bulk, a))
+ a._value_cache = cache[a.key]
- self._cache.update(dict([(a.key, getattr(values_bulk[a.value_content_type].get(a.value_object_id), 'value', None)) for a in attributes]))
- self._cache_populated = True
+ self._cache.update(cache)
+ self._cache_filled = True
+
+ def _lazy_value_from_bulk(self, bulk, attribute):
+ v = bulk[attribute.value_content_type_id].get(attribute.value_object_id)
+ return getattr(v, 'value', None)
def clear_cache(self):
"""Clears the cache."""
self._cache = {}
self._attributes_cache = {}
- self._cache_populated = False
+ self._cache_filled = False
class LazyAttributeMapperMixin(object):
"""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."""
def __getitem__(self, key):
- if key not in self._cache and not self._cache_populated:
+ if key not in self._cache and not self._cache_filled:
self._add_to_cache(key)
return self._cache[key]
def get_attribute(self, key, default=None):
- if key not in self._attributes_cache and not self._cache_populated:
+ if key not in self._attributes_cache and not self._cache_filled:
self._add_to_cache(key)
return self._attributes_cache.get(key, default)
return self.get_attributes().get(key=key)
def _add_to_cache(self, key):
+ from philo.models.base import Attribute
try:
attr = self._raw_get_attribute(key)
except Attribute.DoesNotExist:
class PassthroughAttributeMapper(AttributeMapper):
- """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."""
+ """
+ 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.
+
+ :param entities: An iterable of :class:`.Entity` subclass instances.
+
+ """
def __init__(self, entities):
self._attributes = [e.attributes for e in entities]
super(PassthroughAttributeMapper, self).__init__(self._attributes[0].entity)
- def _populate_cache(self):
- if self._cache_populated:
+ def _fill_cache(self):
+ if self._cache_filled:
return
for a in reversed(self._attributes):
- a._populate_cache()
+ a._fill_cache()
self._attributes_cache.update(a._attributes_cache)
self._cache.update(a._cache)
- self._cache_populated = True
+ self._cache_filled = True
def get_attributes(self):
raise NotImplementedError