diff --git a/imagekit/files.py b/imagekit/files.py index c025af3..150cfa0 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -103,7 +103,7 @@ class GeneratedImageCacheFile(BaseIKFile, ImageFile): super(GeneratedImageCacheFile, self).__init__(storage=storage) def _require_file(self): - before_access.send(sender=self, generator=self.generator, file=self) + before_access.send(sender=self, file=self) return super(GeneratedImageCacheFile, self)._require_file() def clear(self): diff --git a/imagekit/imagecache/strategies.py b/imagekit/imagecache/strategies.py index 56efbd7..4a3d618 100644 --- a/imagekit/imagecache/strategies.py +++ b/imagekit/imagecache/strategies.py @@ -46,11 +46,6 @@ class StrategyWrapper(object): strategy = strategy() self._wrapped = strategy - def invoke_callback(self, name, *args, **kwargs): - func = getattr(self._wrapped, name, None) - if func: - func(*args, **kwargs) - def __unicode__(self): return unicode(self._wrapped) diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 449faac..2aea3ca 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -45,8 +45,8 @@ class ImageSpecField(SpecHostField): self.set_spec_id(cls, name) # Add the model and field as a source for this spec id - register.cacheables(self.spec_id, - ImageFieldSourceGroup(cls, self.source)) + register.source_group(self.spec_id, + ImageFieldSourceGroup(cls, self.source)) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/registry.py b/imagekit/registry.py index 6f48f9c..d74f6b1 100644 --- a/imagekit/registry.py +++ b/imagekit/registry.py @@ -1,6 +1,6 @@ from .exceptions import AlreadyRegistered, NotRegistered -from .signals import (before_access, source_created, source_changed, - source_deleted) +from .signals import before_access, source_created, source_changed, source_deleted +from .utils import call_strategy_method class GeneratorRegistry(object): @@ -12,6 +12,7 @@ class GeneratorRegistry(object): """ def __init__(self): self._generators = {} + before_access.connect(self.before_access_receiver) def register(self, id, generator): if id in self._generators: @@ -41,6 +42,67 @@ class GeneratorRegistry(object): def get_ids(self): return self._generators.keys() + def before_access_receiver(self, sender, file, **kwargs): + generator = file.generator + if generator in self._generators.values(): + # Only invoke the strategy method for registered generators. + call_strategy_method(generator, 'before_access', file=file) + + +class SourceGroupRegistry(object): + """ + The source group registry is responsible for listening to source_* signals + on source groups, and relaying them to the image cache strategies of the + appropriate generators. + + In addition, registering a new source group also registers its cacheables + generator with the cacheable registry. + + """ + _signals = { + source_created: 'on_source_created', + source_changed: 'on_source_changed', + source_deleted: 'on_source_deleted', + } + + def __init__(self): + self._source_groups = {} + for signal in self._signals.keys(): + signal.connect(self.source_group_receiver) + + def register(self, generator_id, source_group): + from .specs.sourcegroups import SourceGroupCacheablesGenerator + generator_ids = self._source_groups.setdefault(source_group, set()) + generator_ids.add(generator_id) + cacheable_registry.register(generator_id, + SourceGroupCacheablesGenerator(source_group, generator_id)) + + def unregister(self, generator_id, source_group): + from .specs.sourcegroups import SourceGroupCacheablesGenerator + generator_ids = self._source_groups.setdefault(source_group, set()) + if generator_id in generator_ids: + generator_ids.remove(generator_id) + cacheable_registry.unregister(generator_id, + SourceGroupCacheablesGenerator(source_group, generator_id)) + + def source_group_receiver(self, sender, source, signal, **kwargs): + """ + Relay source group signals to the appropriate spec strategy. + + """ + source_group = sender + + # Ignore signals from unregistered groups. + if source_group not in self._source_groups: + return + + specs = [generator_registry.get(id, source=source) for id in + self._source_groups[source_group]] + callback_name = self._signals[signal] + + for spec in specs: + call_strategy_method(spec, callback_name, file=source) + class CacheableRegistry(object): """ @@ -53,17 +115,8 @@ class CacheableRegistry(object): """ - _signals = [ - source_created, - source_changed, - source_deleted, - ] - def __init__(self): self._cacheables = {} - for signal in self._signals: - signal.connect(self.cacheable_receiver) - before_access.connect(self.before_access_receiver) def register(self, generator_id, cacheables): """ @@ -90,28 +143,6 @@ class CacheableRegistry(object): for cacheable in k(): yield cacheable - def before_access_receiver(self, sender, generator, cacheable, **kwargs): - generator.image_cache_strategy.invoke_callback('before_access', cacheable) - - def cacheable_receiver(self, sender, cacheable, signal, info, **kwargs): - """ - Redirects signals dispatched on cacheables - to the appropriate generators. - - """ - cacheable = sender - if cacheable not in self._cacheables: - return - - for generator in (generator_registry.get(id, cacheable=cacheable, **info) - for id in self._cacheables[cacheable]): - event_name = { - source_created: 'source_created', - source_changed: 'source_changed', - source_deleted: 'source_deleted', - } - generator._handle_cacheable_event(event_name, cacheable) - class Register(object): """ @@ -132,6 +163,9 @@ class Register(object): def cacheables(self, generator_id, cacheables): cacheable_registry.register(generator_id, cacheables) + def source_group(self, generator_id, source_group): + source_group_registry.register(generator_id, source_group) + class Unregister(object): """ @@ -144,8 +178,12 @@ class Unregister(object): def cacheables(self, generator_id, cacheables): cacheable_registry.unregister(generator_id, cacheables) + def source_group(self, generator_id, source_group): + source_group_registry.unregister(generator_id, source_group) + generator_registry = GeneratorRegistry() cacheable_registry = CacheableRegistry() +source_group_registry = SourceGroupRegistry() register = Register() unregister = Unregister() diff --git a/imagekit/signals.py b/imagekit/signals.py index 4c7aefa..c01e30e 100644 --- a/imagekit/signals.py +++ b/imagekit/signals.py @@ -1,6 +1,10 @@ from django.dispatch import Signal + +# "Cacheables" (cache file) signals before_access = Signal() -source_created = Signal(providing_args=[]) + +# Source group signals +source_created = Signal() source_changed = Signal() source_deleted = Signal() diff --git a/imagekit/specs/__init__.py b/imagekit/specs/__init__.py index 01c7131..7b92845 100644 --- a/imagekit/specs/__init__.py +++ b/imagekit/specs/__init__.py @@ -3,7 +3,6 @@ from django.db.models.fields.files import ImageFieldFile from hashlib import md5 import os import pickle -from ..files import GeneratedImageCacheFile from ..imagecache.backends import get_default_image_cache_backend from ..imagecache.strategies import StrategyWrapper from ..processors import ProcessorPipeline @@ -43,11 +42,6 @@ class BaseImageSpec(object): def generate(self): raise NotImplementedError - # TODO: I don't like this interface. Is there a standard Python one? pubsub? - def _handle_source_event(self, event_name, source): - file = GeneratedImageCacheFile(self) - self.image_cache_strategy.invoke_callback('on_%s' % event_name, file) - class ImageSpec(BaseImageSpec): """ diff --git a/imagekit/specs/sourcegroups.py b/imagekit/specs/sourcegroups.py index 825276e..3c23d64 100644 --- a/imagekit/specs/sourcegroups.py +++ b/imagekit/specs/sourcegroups.py @@ -1,12 +1,26 @@ +""" +Source groups are the means by which image spec sources are identified. They +have two responsibilities: + +1. To dispatch ``source_created``, ``source_changed``, and ``source_deleted`` + signals. (These will be relayed to the corresponding specs' image cache + strategies.) +2. To provide the source files that they represent, via a generator method named + ``files()``. (This is used by the warmimagecache management command for + "pre-caching" image files.) + +""" + from django.db.models.signals import post_init, post_save, post_delete from django.utils.functional import wraps +from ..files import LazyGeneratedImageCacheFile from ..signals import source_created, source_changed, source_deleted def ik_model_receiver(fn): """ A method decorator that filters out signals coming from models that don't - have fields that function as ImageFieldSourceGroup + have fields that function as ImageFieldSourceGroup sources. """ @wraps(fn) @@ -18,8 +32,15 @@ def ik_model_receiver(fn): class ModelSignalRouter(object): """ - Handles signals dispatched by models and relays them to the spec source - groups that represent those models. + Normally, ``ImageFieldSourceGroup`` would be directly responsible for + watching for changes on the model field it represents. However, Django does + not dispatch events for abstract base classes. Therefore, we must listen for + the signals on all models and filter out those that aren't represented by + ``ImageFieldSourceGroup``s. This class encapsulates that functionality. + + Related: + https://github.com/jdriscoll/django-imagekit/issues/126 + https://code.djangoproject.com/ticket/9318 """ @@ -58,25 +79,23 @@ class ModelSignalRouter(object): src in self._source_groups if src.model_class is instance.__class__) @ik_model_receiver - def post_save_receiver(self, sender, instance=None, created=False, - raw=False, **kwargs): + def post_save_receiver(self, sender, instance=None, created=False, raw=False, **kwargs): if not raw: self.init_instance(instance) old_hashes = instance._ik.get('source_hashes', {}).copy() new_hashes = self.update_source_hashes(instance) for attname, file in self.get_field_dict(instance).items(): if created: - self.dispatch_signal(source_created, file, sender, - instance, attname) + self.dispatch_signal(source_created, file, sender, instance, + attname) elif old_hashes[attname] != new_hashes[attname]: - self.dispatch_signal(source_changed, file, sender, - instance, attname) + self.dispatch_signal(source_changed, file, sender, instance, + attname) @ik_model_receiver def post_delete_receiver(self, sender, instance=None, **kwargs): for attname, file in self.get_field_dict(instance).items(): - self.dispatch_signal(source_deleted, file, sender, instance, - attname) + self.dispatch_signal(source_deleted, file, sender, instance, attname) @ik_model_receiver def post_init_receiver(self, sender, instance=None, **kwargs): @@ -91,34 +110,55 @@ class ModelSignalRouter(object): """ for source_group in self._source_groups: if source_group.model_class is model_class and source_group.image_field == attname: - info = dict( - source_group=source_group, - instance=instance, - field_name=attname, - ) - signal.send(sender=source_group, source=file, info=info) + signal.send(sender=source_group, source=file) class ImageFieldSourceGroup(object): + """ + A source group that repesents a particular field across all instances of a + model. + + """ def __init__(self, model_class, image_field): - """ - Good design would dictate that this instance would be responsible for - watching for changes for the provided field. However, due to a bug in - Django, we can't do that without leaving abstract base models (which - don't trigger signals) in the lurch. So instead, we do all signal - handling through the signal router. - - Related: - https://github.com/jdriscoll/django-imagekit/issues/126 - https://code.djangoproject.com/ticket/9318 - - """ self.model_class = model_class self.image_field = image_field signal_router.add(self) - def __call__(self): + def files(self): + """ + A generator that returns the source files that this source group + represents; in this case, a particular field of every instance of a + particular model. + + """ for instance in self.model_class.objects.all(): - yield {'source': getattr(instance, self.image_field)} + yield getattr(instance, self.image_field) + + +class SourceGroupCacheablesGenerator(object): + """ + A cacheables generator for source groups. The purpose of this class is to + generate cacheables (cache files) from a source group. + + """ + def __init__(self, source_group, generator_id): + self.source_group = source_group + self.generator_id = generator_id + + def __eq__(self, other): + return (isinstance(other, self.__class__) + and self.__dict__ == other.__dict__) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.source_group, self.generator_id)) + + def __call__(self): + for source_file in self.source_group.files(): + yield LazyGeneratedImageCacheFile(self.generator_id, + source=source_file) + signal_router = ModelSignalRouter() diff --git a/imagekit/utils.py b/imagekit/utils.py index 82a2c02..adeb377 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -422,3 +422,10 @@ def generate(generator): content = f return File(content) + + +def call_strategy_method(generator, method_name, *args, **kwargs): + strategy = getattr(generator, 'image_cache_strategy', None) + fn = getattr(strategy, method_name, None) + if fn is not None: + fn(*args, **kwargs)