Separate source groups and cacheables.

This allows a sensible specialized interface for source groups, but also
for ImageKit to interact with specs using the generalized image
generator interface.
This commit is contained in:
Matthew Tretter 2013-01-29 01:40:00 -05:00
parent 5b44564318
commit 3931b552a0
8 changed files with 157 additions and 79 deletions

View file

@ -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):

View file

@ -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)

View file

@ -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):

View file

@ -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()

View file

@ -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()

View file

@ -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):
"""

View file

@ -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()

View file

@ -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)