Compare commits

..

13 commits
develop ... 3.3

Author SHA1 Message Date
Bryan Veloso
bc93ec2920 Merge branch 'release/3.3'
* release/3.3:
  Add @vstoykov to the author list.
  Bump version number.
  Update django-nose version to work with Django 1.9
  Add a missing env to the tox matrix
  Work a compatibility implementation for Django 1.2
  Tells tox to only run the designated env
  Enable the new travis architecture for speed and reliability
  Allow the test to fail fast
  Allow travis to fail for the python3.5 interpreter not yet available
  Use the env conf for travis to split the test builds
  Add tox env for django 1.9
  Update the doc to reflect the new `IMAGEKIT_CACHE_BACKEND` behavior
  Cleaner implementation thanks to @vstoykov explanation
  Handle cases where DEFAULT_CACHE_ALIAS is None in old Django versions
  Do not take a decision on which cache to use in DEBUG mode
  Use a compat method to wrap the new way of retrieving the cache engine
2015-12-08 11:41:23 -08:00
Bryan Veloso
96f0b5da4d Merge branch 'release/3.2.7'
* release/3.2.7:
  Bump the version to 3.2.7.
  Fixes open cache file never getting closed
  Fixes open source file never getting closed
  Do not use progressive when we are not running in terminal
  Add test environments for Python3.4 and Django1.7 and Django1.8
  Fixes imports in README example for ProcessedImageField
2015-08-23 18:02:32 -07:00
Bryan Veloso
b398c2cee4 Merge branch 'release/3.2.6'
* release/3.2.6:
  Bump the version to 3.2.6.
  Updated importlib import to fix DeprecationWarning (for django 1.8)
  Add note about usage of optimistic strategy with async backend
  Fix typo
2015-02-26 10:28:51 -08:00
Bryan Veloso
2bcb93da22 Merge branch 'release/3.2.5'
* release/3.2.5:
  Bump the version to 3.2.5.
  Explicitly setting serializer for celery task
2015-01-05 15:58:47 -08:00
Bryan Veloso
00b4095e40 Merge branch 'release/3.2.4'
* release/3.2.4:
  Bump the version to 3.2.4.
  Deprecate `imagekit.processors` submodules
  Catch autodiscover module import error
2014-09-28 13:23:46 -07:00
Bryan Veloso
bc0b0a8a75 Merge branch 'release/3.2.3'
* release/3.2.3:
  Bump the version to 3.2.3.
  Exclude tests from dist
  Revert "Remove test dir __init__.py"
  Only include fetched fields in initial hash of sources
  Add test to illustrate GH-295
  Test that there isn't IO done when you get a URL
  Support should_verify_existence on strategies
  Test that Optimistic strategy doesn't cause reads
  Ignore my Python3 virtualenv
  Remove test dir __init__.py
  Fixed minor spelling error in README.rst
2014-09-27 22:21:17 -07:00
Bryan Veloso
b176c0cafe Merge branch 'release/3.2.2'
* release/3.2.2:
  Bump version to 3.2.2.
  Make sure image files has a name associated.
2014-07-14 12:24:13 -07:00
Bryan Veloso
9f69d7e056 Merge branch 'release/3.2.1'
* release/3.2.1:
  Bump the version to 3.2.1.
  Simplified RQ cache file backed by using the job decorator
  Add note about `open()`
  Always call variable "source_file"
  setup.py: added 'async_rq' extra, django-rq dependency
  Removed unneeded django_rq import
  Added a DeprecationWarning if Async cache file backend is used
  Updated tox.ini to test against Django 1.6.
  Fixed #266 -- Simplified (and renamed) StrategyWrapper.
  Link to Instakit.
2014-04-04 09:27:30 -07:00
Bryan Veloso
ab3593ed54 Merge branch 'release/3.2'
* release/3.2:
  Bump the version to 3.2.
  Use signal.connect for backwards compat
  Add Venelin Stoykov to AUTHORS
  Improve logic of contributing ImageSpecFields to Models
  Use force_bytes from imagekit.lib in test_cachefiles
  Remove @vstoykov's note. Seems like the right place to me (:
  Move force_bytes into lib module
  Don't use a raw string with \u escapes
  Fix sanitizing cache key under Python 3
  Add module to sys.modules
  Test for Python 3
  Insert importer at beginning of list
  Delay Django import until needed
  Add Python 3 suport and drop support for Python 2.5
2014-01-01 15:18:38 -08:00
Bryan Veloso
a14fc91c8d Merge branch 'release/3.1'
* release/3.1:
  Tagging 3.1.
  Correct reference to generateimage tag
  Fix celery backend
  Cache file backends: dropped the "Async" in class names
  Added RQ-based async cache file backend
2013-12-20 09:19:40 -08:00
Bryan Veloso
6929128180 Merge branch 'release/3.0.4'
* release/3.0.4:
  Bump the version to 3.0.4.
  Add __getstate__ method to ImageCacheFile
  Update IMAGEKIT_DEFAULT_FILE_STORAGE description
  Discover image generators during lookup
  Added global boolean to prevent autodiscover() from being called more than once.
  Update fields.py
  Run tests with Django 1.5
2013-09-26 10:56:30 -07:00
Bryan Veloso
a9a152cdfa Merge branch 'release/3.0.3'
* release/3.0.3:
  Bump to 3.0.3.
  Woops. It uses the cache if DEBUG is False.
  Remove PENDING state
2013-07-22 10:01:26 -07:00
Bryan Veloso
31eab7666a Merge branch 'release/3.0.2'
* release/3.0.2:
  Removing the changelog. Changelogs are hard.
  Bump to 3.0.2.
  Use == for comparison
  Don't mutate __dict__
  Don't require source in __setstate__; fixes #234
  Add (failing) test for #234
  Don't include cache in serialization of backend
  Fixed name of spec id attr
2013-07-17 11:46:07 -07:00
42 changed files with 310 additions and 428 deletions

5
.gitignore vendored
View file

@ -4,14 +4,11 @@
*.pyc
.DS_Store
.tox
.idea
.vscode
MANIFEST
build
dist
/tests/media/*
!/tests/media/reference.png
!/tests/media/lenna.png
/venv
/venv3
/.env
/tags

View file

@ -1,35 +1,46 @@
language: python
python: "2.7"
sudo: false
language: python
python:
- "3.8"
- "3.7"
- "3.6"
- "3.5"
env:
- DJANGO="master"
- DJANGO="30"
- DJANGO="22"
- DJANGO="21"
- DJANGO="21"
- DJANGO="20"
- DJANGO="111"
- TOX_ENV=py26-django12
- TOX_ENV=py26-django13
- TOX_ENV=py26-django14
- TOX_ENV=py26-django15
- TOX_ENV=py26-django16
- TOX_ENV=py27-django12
- TOX_ENV=py27-django13
- TOX_ENV=py27-django14
- TOX_ENV=py27-django15
- TOX_ENV=py27-django16
- TOX_ENV=py27-django17
- TOX_ENV=py27-django18
- TOX_ENV=py27-django19
- TOX_ENV=py32-django15
- TOX_ENV=py32-django16
- TOX_ENV=py32-django17
- TOX_ENV=py32-django18
- TOX_ENV=py33-django15
- TOX_ENV=py33-django16
- TOX_ENV=py33-django17
- TOX_ENV=py33-django18
- TOX_ENV=py34-django16
- TOX_ENV=py34-django17
- TOX_ENV=py34-django18
- TOX_ENV=py34-django19
- TOX_ENV=py35-django19
install:
- pip install tox
script:
- tox -e py$(python -c 'import sys;print("".join(map(str, sys.version_info[:2])))')-django${DJANGO}
jobs:
matrix:
# Python 3.5 not yet available on travis, watch this to see when it is.
fast_finish: true
allow_failures:
- env: DJANGO="master"
exclude:
- python: "3.5"
env: DJANGO="30"
- python: "3.5"
env: DJANGO="master"
- env: TOX_ENV=py35-django19
install:
- pip install tox --use-mirrors
script:
- tox -e $TOX_ENV
notifications:
irc: "irc.freenode.org#imagekit"

View file

@ -1,18 +1,6 @@
include AUTHORS
include LICENSE
include README.rst
include testrunner.py
include setup.cfg
include tests/*.py
include tests/assets/Lenna.png
include tests/assets/lenna-*.jpg
include tests/media/lenna.png
prune tests/media/CACHE
prune tests/media/b
prune tests/media/photos
include docs/Makefile
include docs/conf.py
include docs/make.bat
include docs/*.rst
recursive-include docs/_themes LICENSE README.rst flask_theme_support.py theme.conf *.css_t *.css *.html
recursive-include imagekit/templates *.html
recursive-include docs *
recursive-include imagekit/templates *
prune tests

View file

@ -1,6 +1,6 @@
|Build Status|_
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.svg?branch=develop
.. |Build Status| image:: https://travis-ci.org/matthewwithanm/django-imagekit.png?branch=develop
.. _Build Status: https://travis-ci.org/matthewwithanm/django-imagekit
ImageKit is a Django app for processing images. Need a thumbnail? A
@ -39,7 +39,6 @@ Installation
Usage Overview
==============
.. _specs:
Specs
-----
@ -71,8 +70,8 @@ your model class:
options={'quality': 60})
profile = Profile.objects.all()[0]
print(profile.avatar_thumbnail.url) # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
print(profile.avatar_thumbnail.width) # > 100
print profile.avatar_thumbnail.url # > /media/CACHE/images/982d5af84cddddfd0fbf70892b4431e4.jpg
print profile.avatar_thumbnail.width # > 100
As you can probably tell, ImageSpecFields work a lot like Django's
ImageFields. The difference is that they're automatically generated by
@ -98,8 +97,8 @@ class:
options={'quality': 60})
profile = Profile.objects.all()[0]
print(profile.avatar_thumbnail.url) # > /media/avatars/MY-avatar.jpg
print(profile.avatar_thumbnail.width) # > 100
print profile.avatar_thumbnail.url # > /media/avatars/MY-avatar.jpg
print profile.avatar_thumbnail.width # > 100
This is pretty similar to our previous example. We don't need to specify a
"source" any more since we're not processing another image field, but we do need
@ -145,7 +144,7 @@ on, or what should be done with the result; that's up to you:
.. code-block:: python
source_file = open('/path/to/myimage.jpg', 'rb')
source_file = open('/path/to/myimage.jpg')
image_generator = Thumbnail(source=source_file)
result = image_generator.generate()
@ -160,7 +159,7 @@ example, if you wanted to save it to disk:
.. code-block:: python
dest = open('/path/to/dest.jpg', 'wb')
dest = open('/path/to/dest.jpg', 'w')
dest.write(result.read())
dest.close()
@ -407,37 +406,6 @@ Django admin classes:
admin.site.register(Photo, PhotoAdmin)
To use specs defined outside of models:
.. code-block:: python
from django.contrib import admin
from imagekit.admin import AdminThumbnail
from imagekit import ImageSpec
from imagekit.processors import ResizeToFill
from imagekit.cachefiles import ImageCacheFile
from .models import Photo
class AdminThumbnailSpec(ImageSpec):
processors = [ResizeToFill(100, 30)]
format = 'JPEG'
options = {'quality': 60 }
def cached_admin_thumb(instance):
# `image` is the name of the image field on the model
cached = ImageCacheFile(AdminThumbnailSpec(instance.image))
# only generates the first time, subsequent calls use cache
cached.generate()
return cached
class PhotoAdmin(admin.ModelAdmin):
list_display = ('__str__', 'admin_thumbnail')
admin_thumbnail = AdminThumbnail(image_field=cached_admin_thumb)
admin.site.register(Photo, PhotoAdmin)
AdminThumbnail can even use a custom template. For more information, see
``imagekit.admin.AdminThumbnail``.

View file

@ -163,7 +163,7 @@ A simple example of a custom source group class is as follows:
def files(self):
os.chdir(self.dir)
for name in glob.glob('*.jpg'):
yield open(name, 'rb')
yield open(name)
Instances of this class could then be registered with one or more spec id:

View file

@ -3,7 +3,7 @@ Caching
Default Backend Workflow
========================
================
``ImageSpec``
@ -29,8 +29,6 @@ objects, but they've got a little trick up their sleeve: they represent files
that may not actually exist!
.. _cache-file-strategy:
Cache File Strategy
-------------------
@ -57,8 +55,6 @@ The default strategy only defines the first two of these, as follows:
file.generate()
.. _cache-file-backend:
Cache File Backend
------------------
@ -104,21 +100,13 @@ ImageKit. Each has its own pros and cons.
Caching Data About Generated Files
----------------------------------
Generally, once a file is generated, you will never be removing it, so by
default ImageKit will use default cache to cache the state of generated
files "forever" (or only 5 minutes when ``DEBUG = True``).
The time for which ImageKit will cache state is configured with
``IMAGEKIT_CACHE_TIMEOUT``. If set to ``None`` this means "never expire"
(default when ``DEBUG = False``). You can reduce this timeout if you want
or set it to some numeric value in seconds if your cache backend behaves
differently and for example do not cache values if timeout is ``None``.
If you clear your cache durring deployment or some other reason probably
you do not want to lose the cache for generated images especcialy if you
are using some slow remote storage (like Amazon S3). Then you can configure
seprate cache (for example redis) in your ``CACHES`` config and tell ImageKit
to use it instead of the default cache by setting ``IMAGEKIT_CACHE_BACKEND``.
The easiest, and most significant improvement you can make to improve the
performance of your site is to have ImageKit cache the state of your generated
files. The default cache file backend will already do this (if ``DEBUG`` is
``False``), using your default Django cache backend, but you can make it way
better by setting ``IMAGEKIT_CACHE_BACKEND``. Generally, once a file is
generated, you will never be removing it; therefore, if you can, you should set
``IMAGEKIT_CACHE_BACKEND`` to a cache backend that will cache forever.
Pre-Generating Images
@ -189,11 +177,6 @@ Or, in Python:
def on_source_saved(self, file):
file.generate()
.. note::
If you use custom storage backend for some specs,
(storage passed to the field different than configured one)
it's required the storage to be pickleable
__ https://pypi.python.org/pypi/django-celery

View file

@ -54,7 +54,7 @@ execfile(os.path.join(os.path.dirname(__file__), '..', 'imagekit',
# built documents.
#
# The short X.Y version.
version = re.match(r'\d+\.\d+', pkgmeta['__version__']).group()
version = re.match('\d+\.\d+', pkgmeta['__version__']).group()
# The full version, including alpha/beta/rc tags.
release = pkgmeta['__version__']

View file

@ -55,15 +55,6 @@ Settings
.. _`Django cache section`: https://docs.djangoproject.com/en/1.8/topics/cache/#accessing-the-cache
.. attribute:: IMAGEKIT_CACHE_TIMEOUT
:default: ``None``
Use when you need to override the timeout used to cache file state.
By default it is "cache forever".
It's highly recommended that you use a very high timeout.
.. attribute:: IMAGEKIT_CACHE_PREFIX
:default: ``'imagekit:'``

View file

@ -79,9 +79,12 @@ IK3 provides analogous settings for cache file backends and strategies:
IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'path.to.MyCacheFileBackend'
IMAGEKIT_DEFAULT_CACHEFILE_STRATEGY = 'path.to.MyCacheFileStrategy'
See the documentation on :ref:`cache file backends <cache-file-backend>` and :ref:`cache file strategies <cache-file-strategy>`
See the documentation on `cache file backends`_ and `cache file strategies`_
for more details.
.. _`cache file backends`:
.. _`cache file strategies`:
Conditional model ``processors``
--------------------------------
@ -90,7 +93,9 @@ In IK2, an ``ImageSpecField`` could take a ``processors`` callable instead of
an iterable, which allowed processing decisions to made based on other
properties of the model. IK3 does away with this feature for consistency's sake
(if one kwarg could be callable, why not all?), but provides a much more robust
solution: the custom ``spec``. See the :doc:`advanced usage <advanced_usage>` documentation for more.
solution: the custom ``spec``. See the `advanced usage`_ documentation for more.
.. _`advanced usage`:
Conditonal ``cache_to`` file names
@ -104,7 +109,9 @@ There is a way to achieve custom file names by overriding your spec's
``cachefile_name``, but it is not recommended, as the spec's default
behavior is to hash the combination of ``source``, ``processors``, ``format``,
and other spec options to ensure that changes to the spec always result in
unique file names. See the documentation on :ref:`specs` for more.
unique file names. See the documentation on `specs`_ for more.
.. _`specs`:
Processors have moved to PILKit

View file

@ -3,7 +3,6 @@ from django.conf import settings
from django.core.files import File
from django.core.files.images import ImageFile
from django.utils.functional import SimpleLazyObject
from django.utils.encoding import smart_str
from ..files import BaseIKFile
from ..registry import generator_registry
from ..signals import content_required, existence_required
@ -144,33 +143,12 @@ class ImageCacheFile(BaseIKFile, ImageFile):
# file is hidden link to "file" attribute
state.pop('_file', None)
# remove storage from state as some non-FileSystemStorage can't be
# pickled
settings_storage = get_singleton(
settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
'file storage backend'
)
if state['storage'] == settings_storage:
state.pop('storage')
return state
def __setstate__(self, state):
if 'storage' not in state:
state['storage'] = get_singleton(
settings.IMAGEKIT_DEFAULT_FILE_STORAGE,
'file storage backend'
)
self.__dict__.update(state)
def __nonzero__(self):
# Python 2 compatibility
return self.__bool__()
def __repr__(self):
return smart_str("<%s: %s>" % (
self.__class__.__name__, self if self.name else "None")
)
class LazyImageCacheFile(SimpleLazyObject):
def __init__(self, generator_id, *args, **kwargs):

View file

@ -2,7 +2,6 @@ from ..utils import get_singleton, get_cache, sanitize_cache_key
import warnings
from copy import copy
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
class CacheFileState(object):
@ -53,7 +52,8 @@ class CachedFileBackend(object):
@property
def cache(self):
if not getattr(self, '_cache', None):
self._cache = get_cache()
from django.conf import settings
self._cache = get_cache(settings.IMAGEKIT_CACHE_BACKEND)
return self._cache
def get_key(self, file):
@ -75,7 +75,7 @@ class CachedFileBackend(object):
if state == CacheFileState.DOES_NOT_EXIST:
self.cache.set(key, state, self.existence_check_timeout)
else:
self.cache.set(key, state, settings.IMAGEKIT_CACHE_TIMEOUT)
self.cache.set(key, state)
def __getstate__(self):
state = copy(self.__dict__)
@ -110,7 +110,7 @@ class Simple(CachedFileBackend):
def _exists(self, file):
return bool(getattr(file, '_file', None)
or (file.name and file.storage.exists(file.name)))
or file.storage.exists(file.name))
def _generate_file(backend, file, force=False):

View file

@ -1,6 +1,5 @@
from appconf import AppConf
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
class ImageKitConf(AppConf):
@ -14,24 +13,32 @@ class ImageKitConf(AppConf):
CACHE_BACKEND = None
CACHE_PREFIX = 'imagekit:'
CACHE_TIMEOUT = None
USE_MEMCACHED_SAFE_CACHE_KEY = True
def configure_cache_backend(self, value):
if value is None:
from django.core.cache import DEFAULT_CACHE_ALIAS
return DEFAULT_CACHE_ALIAS
# DEFAULT_CACHE_ALIAS doesn't exist in Django<=1.2
try:
from django.core.cache import DEFAULT_CACHE_ALIAS as default_cache_alias
except ImportError:
default_cache_alias = 'default'
if value not in settings.CACHES:
raise ImproperlyConfigured("{0} is not present in settings.CACHES".format(value))
caches = getattr(settings, 'CACHES', None)
if caches is None:
# Support Django<=1.2 there is no default `CACHES` setting
try:
from django.core.cache.backends.dummy import DummyCache
except ImportError:
dummy_cache = 'dummy://'
else:
dummy_cache = 'django.core.cache.backends.dummy.DummyCache'
return dummy_cache
return value
if default_cache_alias in caches:
value = default_cache_alias
else:
raise ValueError("The default cache alias '%s' is not available in CACHES" % default_cache_alias)
def configure_cache_timeout(self, value):
if value is None and settings.DEBUG:
# If value is not configured and is DEBUG set it to 5 minutes
return 300
# Otherwise leave it as is. If it is None then valies will never expire
return value
def configure_default_file_storage(self, value):

View file

@ -49,25 +49,14 @@ class BaseIKFile(File):
def _get_size(self):
self._require_file()
if not getattr(self, '_committed', False):
if not self._committed:
return self.file.size
return self.storage.size(self.name)
size = property(_get_size)
def open(self, mode='rb'):
self._require_file()
try:
self.file.open(mode)
except ValueError:
# if the underlaying file can't be reopened
# then we will use the storage to try to open it again
if self.file.closed:
# clear cached file instance
del self.file
# Because file is a property we can acces it after
# we deleted it
return self.file.open(mode)
raise
self.file.open(mode)
def _get_closed(self):
file = getattr(self, '_file', None)

View file

@ -24,10 +24,6 @@ class ProcessedImageField(ImageField, SpecHost):
if data and data != initial:
spec = self.get_spec(source=data)
f = generate(spec)
# Name is required in Django 1.4. When we drop support for it
# then we can dirrectly return the result from `generate(spec)`
f.name = data.name
return f
data = generate(spec)
return data

View file

@ -14,15 +14,11 @@ match both. Subsegments are always matched, so "a" will match "a" as
well as "a:b" and "a:b:c".""")
args = '[generator_ids]'
def add_arguments(self, parser):
parser.add_argument('generator_id', nargs='*', help='<app_name>:<model>:<field> for model specs')
def handle(self, *args, **options):
generators = generator_registry.get_ids()
generator_ids = options['generator_id'] if 'generator_id' in options else args
if generator_ids:
patterns = self.compile_patterns(generator_ids)
if args:
patterns = self.compile_patterns(args)
generators = (id for id in generators if any(p.match(id) for p in patterns))
for generator_id in generators:

View file

@ -1,6 +1,5 @@
from __future__ import unicode_literals
from django.conf import settings
from django.db import models
from django.db.models.signals import class_prepared
from .files import ProcessedImageFieldFile
@ -93,7 +92,7 @@ class ProcessedImageField(models.ImageField, SpecHostField):
def __init__(self, processors=None, format=None, options=None,
verbose_name=None, name=None, width_field=None, height_field=None,
autoconvert=None, spec=None, spec_id=None, **kwargs):
autoconvert=True, spec=None, spec_id=None, **kwargs):
"""
The ProcessedImageField constructor accepts all of the arguments that
the :class:`django.db.models.ImageField` constructor accepts, as well
@ -101,10 +100,6 @@ class ProcessedImageField(models.ImageField, SpecHostField):
:class:`imagekit.models.ImageSpecField`.
"""
# if spec is not provided then autoconvert will be True by default
if spec is None and autoconvert is None:
autoconvert = True
SpecHost.__init__(self, processors=processors, format=format,
options=options, autoconvert=autoconvert, spec=spec,
spec_id=spec_id)
@ -116,11 +111,9 @@ class ProcessedImageField(models.ImageField, SpecHostField):
return super(ProcessedImageField, self).contribute_to_class(cls, name)
# If the project does not use south, then we will not try to add introspection
if 'south' in settings.INSTALLED_APPS:
try:
from south.modelsinspector import add_introspection_rules
except ImportError:
pass
else:
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])
try:
from south.modelsinspector import add_introspection_rules
except ImportError:
pass
else:
add_introspection_rules([], [r'^imagekit\.models\.fields\.ProcessedImageField$'])

View file

@ -1,5 +1,5 @@
__title__ = 'django-imagekit'
__author__ = 'Matthew Tretter, Venelin Stoykov, Eric Eldredge, Bryan Veloso, Greg Newman, Chris Drackett, Justin Driscoll'
__version__ = '4.0.2'
__version__ = '3.3'
__license__ = 'BSD'
__all__ = ['__title__', '__author__', '__version__', '__license__']

View file

@ -145,24 +145,18 @@ class ImageSpec(BaseImageSpec):
# TODO: Move into a generator base class
# TODO: Factor out a generate_image function so you can create a generator and only override the PIL.Image creating part. (The tricky part is how to deal with original_format since generator base class won't have one.)
closed = self.source.closed
if closed:
# Django file object should know how to reopen itself if it was closed
# https://code.djangoproject.com/ticket/13750
self.source.open()
try:
img = open_image(self.source)
new_image = process_image(img,
processors=self.processors,
format=self.format,
autoconvert=self.autoconvert,
options=self.options)
finally:
if closed:
# We need to close the file if it was opened by us
self.source.close()
except ValueError:
# Re-open the file -- https://code.djangoproject.com/ticket/13750
self.source.open()
img = open_image(self.source)
new_image = process_image(img, processors=self.processors,
format=self.format, autoconvert=self.autoconvert,
options=self.options)
self.source.close()
return new_image

View file

@ -87,15 +87,12 @@ class ModelSignalRouter(object):
if isinstance(instance, src.model_class))
@ik_model_receiver
def post_save_receiver(self, sender, instance=None, created=False, update_fields=None, 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 in self.get_source_fields(instance):
if update_fields and attname not in update_fields:
continue
file = getattr(instance, attname)
if file and old_hashes.get(attname) != new_hashes[attname]:
self.dispatch_signal(source_saved, file, sender, instance,

View file

@ -4,7 +4,7 @@ from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe
from ..compat import parse_bits
from .compat import parse_bits
from ..cachefiles import ImageCacheFile
from ..registry import generator_registry
from ..lib import force_text

View file

@ -72,26 +72,6 @@ def autodiscover():
if _autodiscovered:
return
try:
from django.utils.module_loading import autodiscover_modules
except ImportError:
# Django<1.7
_autodiscover_modules_fallback()
else:
autodiscover_modules('imagegenerators')
_autodiscovered = True
def _autodiscover_modules_fallback():
"""
Auto-discover INSTALLED_APPS imagegenerators.py modules and fail silently
when not present. This forces an import on them to register any admin bits
they may want.
Copied from django.contrib.admin
Used for Django versions < 1.7
"""
from django.conf import settings
try:
from importlib import import_module
@ -99,6 +79,8 @@ def _autodiscover_modules_fallback():
from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule
_autodiscovered = True
for app in settings.INSTALLED_APPS:
# As of Django 1.7, settings.INSTALLED_APPS may contain classes instead of modules, hence the try/except
# See here: https://docs.djangoproject.com/en/dev/releases/1.7/#introspecting-applications
@ -150,13 +132,16 @@ def generate(generator):
"""
content = generator.generate()
f = File(content)
# The size of the File must be known or Django will try to open a file
# without a name and raise an Exception.
f.size = len(content.read())
# After getting the size reset the file pointer for future reads.
content.seek(0)
return f
# If the file doesn't have a name, Django will raise an Exception while
# trying to save it, so we create a named temporary file.
if not getattr(content, 'name', None):
f = NamedTemporaryFile()
f.write(content.read())
f.seek(0)
content = f
return File(content)
def call_strategy_method(file, method_name):
@ -166,15 +151,14 @@ def call_strategy_method(file, method_name):
fn(file)
def get_cache():
def get_cache(backend, **kwargs):
try:
from django.core.cache import caches
except ImportError:
# Django < 1.7
from django.core.cache import get_cache
return get_cache(settings.IMAGEKIT_CACHE_BACKEND)
return get_cache(backend, **kwargs)
return caches[settings.IMAGEKIT_CACHE_BACKEND]
return caches[backend]
def sanitize_cache_key(key):

View file

@ -1,2 +0,0 @@
[bdist_wheel]
universal = 1

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python
#/usr/bin/env python
import codecs
import os
from setuptools import setup, find_packages
@ -7,27 +7,25 @@ import sys
# Workaround for multiprocessing/nose issue. See http://bugs.python.org/msg170215
try:
import multiprocessing # NOQA
import multiprocessing
except ImportError:
pass
if 'publish' in sys.argv:
os.system('python setup.py sdist bdist_wheel upload')
os.system('python setup.py sdist upload')
sys.exit()
read = lambda filepath: codecs.open(filepath, 'r', 'utf-8').read()
def exec_file(filepath, globalz=None, localz=None):
exec(read(filepath), globalz, localz)
# Load package meta from the pkgmeta module without loading imagekit.
pkgmeta = {}
exec_file(os.path.join(os.path.dirname(__file__),
'imagekit', 'pkgmeta.py'), pkgmeta)
'imagekit', 'pkgmeta.py'), pkgmeta)
setup(
@ -41,16 +39,16 @@ setup(
maintainer_email='bryan@revyver.com',
license='BSD',
url='http://github.com/matthewwithanm/django-imagekit/',
packages=find_packages(exclude=['*.tests', '*.tests.*', 'tests.*', 'tests']),
packages=find_packages(),
zip_safe=False,
include_package_data=True,
tests_require=[
'beautifulsoup4>=4.4.0',
'nose>=1.3.6',
'nose-progressive>=1.5.1',
'django-nose>=1.4',
'Pillow',
'mock>=1.0.1',
'beautifulsoup4==4.1.3',
'nose>=1.3.6,<1.4',
'nose-progressive==1.5.1',
'django-nose>=1.2,<1.5',
'Pillow<3.0',
'mock==1.0.1',
],
test_suite='testrunner.run_tests',
install_requires=[
@ -70,11 +68,12 @@ setup(
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Utilities'
],
)

View file

@ -16,7 +16,4 @@ def run_tests():
cls = get_runner(settings)
runner = cls()
failures = runner.run_tests(['tests'])
# Clean autogenerated junk before exit
from tests.utils import clear_imagekit_test_files
clear_imagekit_test_files()
sys.exit(failures)

BIN
tests/assets/Lenna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

BIN
tests/media/lenna.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 463 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

View file

@ -1,17 +1,10 @@
from django.db import models
from imagekit import ImageSpec
from imagekit.models import ProcessedImageField
from imagekit.models import ImageSpecField
from imagekit.processors import Adjust, ResizeToFill, SmartCrop
class Thumbnail(ImageSpec):
processors = [ResizeToFill(100, 60)]
format = 'JPEG'
options = {'quality': 60}
class ImageModel(models.Model):
image = models.ImageField(upload_to='b')
@ -34,10 +27,6 @@ class ProcessedImageFieldModel(models.Model):
options={'quality': 90}, upload_to='p')
class ProcessedImageFieldWithSpecModel(models.Model):
processed = ProcessedImageField(spec=Thumbnail, upload_to='p')
class CountingCacheFileStrategy(object):
def __init__(self):
self.on_existence_required_count = 0

View file

@ -47,23 +47,6 @@ NOSE_ARGS = [
if os.getenv('TERM'):
NOSE_ARGS.append('--with-progressive')
DEBUG = True
TEMPLATE_DEBUG = DEBUG
CACHE_BACKEND = 'locmem://'
# Django >= 1.8
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
},
},
]

View file

@ -1,4 +1,3 @@
from unittest import mock
from django.conf import settings
from hashlib import md5
from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile
@ -49,31 +48,6 @@ def test_no_source_error():
file.generate()
def test_repr_does_not_send_existence_required():
"""
Ensure that `__repr__` method does not send `existance_required` signal
Cachefile strategy may be configured to generate file on
`existance_required`.
To generate images, backend passes `ImageCacheFile` instance to worker.
Both celery and RQ calls `__repr__` method for each argument to enque call.
And if `__repr__` of object will send this signal, we will get endless
recursion
"""
with mock.patch('imagekit.cachefiles.existence_required') as signal:
# import here to apply mock
from imagekit.cachefiles import ImageCacheFile
spec = TestSpec(source=get_unique_image_file())
file = ImageCacheFile(
spec,
cachefile_backend=DummyAsyncCacheFileBackend()
)
file.__repr__()
eq_(signal.send.called, False)
def test_memcached_cache_key():
"""
Ensure the default cachefile backend is sanitizing its cache key for

View file

@ -1,25 +0,0 @@
from nose.tools import assert_false, assert_true
from .models import Thumbnail
from .utils import create_photo
def test_do_not_leak_open_files():
instance = create_photo('leak-test.jpg')
source_file = instance.original_image
# Ensure the FieldFile is closed before generation
source_file.close()
image_generator = Thumbnail(source=source_file)
image_generator.generate()
assert_true(source_file.closed)
def test_do_not_close_open_files_after_generate():
instance = create_photo('do-not-close-test.jpg')
source_file = instance.original_image
# Ensure the FieldFile is opened before generation
source_file.open()
image_generator = Thumbnail(source=source_file)
image_generator.generate()
assert_false(source_file.closed)
source_file.close()

View file

@ -5,9 +5,7 @@ from imagekit import forms as ikforms
from imagekit.processors import SmartCrop
from nose.tools import eq_
from . import imagegenerators # noqa
from .models import (ProcessedImageFieldModel,
ProcessedImageFieldWithSpecModel,
ImageModel)
from .models import ProcessedImageFieldModel, ImageModel
from .utils import get_image_file
@ -21,16 +19,6 @@ def test_model_processedimagefield():
eq_(instance.processed.height, 50)
def test_model_processedimagefield_with_spec():
instance = ProcessedImageFieldWithSpecModel()
file = File(get_image_file())
instance.processed.save('whatever.jpeg', file)
instance.save()
eq_(instance.processed.width, 100)
eq_(instance.processed.height, 60)
def test_form_processedimagefield():
class TestForm(forms.ModelForm):
image = ikforms.ProcessedImageField(spec_id='tests:testform_image',

View file

@ -1,12 +1,11 @@
from django.template import TemplateSyntaxError
from nose.tools import eq_, assert_false, raises, assert_not_equal
from . import imagegenerators # noqa
from .utils import render_tag, get_html_attrs, clear_imagekit_cache
from .utils import render_tag, get_html_attrs
def test_img_tag():
ttag = r"""{% generateimage 'testspec' source=img %}"""
clear_imagekit_cache()
attrs = get_html_attrs(ttag)
expected_attrs = set(['src', 'width', 'height'])
eq_(set(attrs.keys()), expected_attrs)
@ -16,7 +15,6 @@ def test_img_tag():
def test_img_tag_attrs():
ttag = r"""{% generateimage 'testspec' source=img -- alt="Hello" %}"""
clear_imagekit_cache()
attrs = get_html_attrs(ttag)
eq_(attrs.get('alt'), 'Hello')
@ -30,7 +28,7 @@ def test_dangling_html_attrs_delimiter():
@raises(TemplateSyntaxError)
def test_html_attrs_assignment():
"""
You can either use generateimage as an assignment tag or specify html attrs,
You can either use generateimage as an assigment tag or specify html attrs,
but not both.
"""
@ -44,13 +42,11 @@ def test_single_dimension_attr():
"""
ttag = r"""{% generateimage 'testspec' source=img -- width="50" %}"""
clear_imagekit_cache()
attrs = get_html_attrs(ttag)
assert_false('height' in attrs)
def test_assignment_tag():
ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}{{ th.height }}{{ th.width }}"""
clear_imagekit_cache()
ttag = r"""{% generateimage 'testspec' source=img as th %}{{ th.url }}"""
html = render_tag(ttag)
assert_not_equal(html.strip(), '')

View file

@ -1,5 +1,5 @@
from nose.tools import assert_false
from unittest.mock import Mock, PropertyMock, patch
from mock import Mock, PropertyMock, patch
from .models import Photo

View file

@ -1,6 +1,6 @@
from nose.tools import assert_true, assert_false
from imagekit.cachefiles import ImageCacheFile
from unittest.mock import Mock
from mock import Mock
from .utils import create_image
from django.core.files.storage import FileSystemStorage
from imagekit.cachefiles.backends import Simple as SimpleCFBackend

View file

@ -6,11 +6,10 @@ deserialized. This is important when using IK with Celery.
from imagekit.cachefiles import ImageCacheFile
from .imagegenerators import TestSpec
from .utils import create_photo, pickleback, get_unique_image_file, clear_imagekit_cache
from .utils import create_photo, pickleback, get_unique_image_file
def test_imagespecfield():
clear_imagekit_cache()
instance = create_photo('pickletest2.jpg')
thumbnail = pickleback(instance.thumbnail)
thumbnail.generate()
@ -23,21 +22,16 @@ def test_circular_ref():
This corresponds to #234
"""
clear_imagekit_cache()
instance = create_photo('pickletest3.jpg')
instance.thumbnail # Cause thumbnail to be added to instance's __dict__
pickleback(instance)
def test_cachefiles():
clear_imagekit_cache()
spec = TestSpec(source=get_unique_image_file())
file = ImageCacheFile(spec)
file.url
# remove link to file from spec source generator
# test __getstate__ of ImageCacheFile
file.generator.source = None
restored_file = pickleback(file)
assert file is not restored_file
# Assertion for #437 and #451
assert file.storage is restored_file.storage
pickleback(file)

View file

@ -1,12 +1,11 @@
from django.template import TemplateSyntaxError
from nose.tools import eq_, raises, assert_not_equal
from . import imagegenerators # noqa
from .utils import render_tag, get_html_attrs, clear_imagekit_cache
from .utils import render_tag, get_html_attrs
def test_img_tag():
ttag = r"""{% thumbnail '100x100' img %}"""
clear_imagekit_cache()
attrs = get_html_attrs(ttag)
expected_attrs = set(['src', 'width', 'height'])
eq_(set(attrs.keys()), expected_attrs)
@ -16,7 +15,6 @@ def test_img_tag():
def test_img_tag_attrs():
ttag = r"""{% thumbnail '100x100' img -- alt="Hello" %}"""
clear_imagekit_cache()
attrs = get_html_attrs(ttag)
eq_(attrs.get('alt'), 'Hello')
@ -42,7 +40,7 @@ def test_too_many_args():
@raises(TemplateSyntaxError)
def test_html_attrs_assignment():
"""
You can either use thumbnail as an assignment tag or specify html attrs,
You can either use thumbnail as an assigment tag or specify html attrs,
but not both.
"""
@ -52,20 +50,17 @@ def test_html_attrs_assignment():
def test_assignment_tag():
ttag = r"""{% thumbnail '100x100' img as th %}{{ th.url }}"""
clear_imagekit_cache()
html = render_tag(ttag)
assert_not_equal(html, '')
def test_single_dimension():
ttag = r"""{% thumbnail '100x' img as th %}{{ th.width }}"""
clear_imagekit_cache()
html = render_tag(ttag)
eq_(html, '100')
def test_alternate_generator():
ttag = r"""{% thumbnail '1pxsq' '100x' img as th %}{{ th.width }}"""
clear_imagekit_cache()
html = render_tag(ttag)
eq_(html, '1')

View file

@ -1,12 +1,10 @@
from bs4 import BeautifulSoup
import os
import shutil
from django.conf import settings
from django.core.files import File
from django.template import Context, Template
from imagekit.cachefiles.backends import Simple, CacheFileState
from imagekit.conf import settings
from imagekit.lib import Image, StringIO
from imagekit.utils import get_cache
from nose.tools import assert_true, assert_false
import pickle
from tempfile import NamedTemporaryFile
@ -19,10 +17,9 @@ def get_image_file():
http://en.wikipedia.org/wiki/Lenna
http://sipi.usc.edu/database/database.php?volume=misc&image=12
https://lintian.debian.org/tags/license-problem-non-free-img-lenna.html
https://github.com/libav/libav/commit/8895bf7b78650c0c21c88cec0484e138ec511a4b
"""
path = os.path.join(settings.MEDIA_ROOT, 'reference.png')
path = os.path.join(settings.MEDIA_ROOT, 'lenna.png')
return open(path, 'r+b')
@ -64,7 +61,7 @@ def render_tag(ttag):
def get_html_attrs(ttag):
return BeautifulSoup(render_tag(ttag), features="html.parser").img.attrs
return BeautifulSoup(render_tag(ttag)).img.attrs
def assert_file_is_falsy(file):
@ -84,23 +81,3 @@ class DummyAsyncCacheFileBackend(Simple):
def generate(self, file, force=False):
pass
def clear_imagekit_cache():
cache = get_cache()
cache.clear()
# Clear IMAGEKIT_CACHEFILE_DIR
cache_dir = os.path.join(settings.MEDIA_ROOT, settings.IMAGEKIT_CACHEFILE_DIR)
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
def clear_imagekit_test_files():
clear_imagekit_cache()
for fname in os.listdir(settings.MEDIA_ROOT):
if fname != 'reference.png':
path = os.path.join(settings.MEDIA_ROOT, fname)
if os.path.isdir(path):
shutil.rmtree(path)
else:
os.remove(path)

160
tox.ini
View file

@ -1,18 +1,156 @@
[tox]
envlist =
py38-django{master,30,22,21,20,111},
py37-django{master,30,22,21,20,111},
py36-django{master,30,22,21,20,111},
py35-django{21,20,111},
py35-django19,
py34-django19, py34-django18, py34-django17, py34-django16,
py33-django18, py33-django17, py33-django16, py33-django15,
py32-django18, py32-django17, py32-django16, py32-django15,
py27-django19, py27-django18, py27-django17, py27-django16, py27-django15, py27-django14, py27-django13, py27-django12,
py26-django16, py26-django15, py26-django14, py26-django13, py26-django12
[testenv]
commands = python setup.py test
[testenv:py35-django19]
basepython = python3.5
deps =
djangomaster: git+https://github.com/django/django.git@master#egg=Django
django30: Django>=3.0,<3.1
django22: Django>=2.2,<3.0
django21: Django>=2.1,<2.2
django20: Django>=2.0,<2.1
django111: Django>=1.11,<2.0
django{21,20,111}: django-nose==1.4.5
git+https://github.com/django/django.git@stable/1.9.x#egg=Django
django-nose==1.4.2
[testenv:py34-django19]
basepython = python3.4
deps =
git+https://github.com/django/django.git@stable/1.9.x#egg=Django
django-nose==1.4.2
[testenv:py34-django18]
basepython = python3.4
deps =
Django>=1.8,<1.9
django-nose==1.4
[testenv:py34-django17]
basepython = python3.4
deps =
Django>=1.7,<1.8
django-nose==1.4
[testenv:py34-django16]
basepython = python3.4
deps =
Django>=1.6,<1.7
[testenv:py33-django18]
basepython = python3.3
deps =
Django>=1.8,<1.9
django-nose==1.4
[testenv:py33-django17]
basepython = python3.3
deps =
Django>=1.7,<1.8
django-nose==1.4
[testenv:py33-django16]
basepython = python3.3
deps =
Django>=1.6,<1.7
[testenv:py33-django15]
basepython = python3.3
deps =
Django>=1.5,<1.6
[testenv:py32-django18]
basepython = python3.4
deps =
Django>=1.8,<1.9
django-nose==1.4
[testenv:py32-django17]
basepython = python3.4
deps =
Django>=1.7,<1.8
django-nose==1.4
[testenv:py32-django16]
basepython = python3.2
deps =
Django>=1.6,<1.7
[testenv:py32-django15]
basepython = python3.2
deps =
Django>=1.5,<1.6
[testenv:py27-django19]
basepython = python2.7
deps =
git+https://github.com/django/django.git@stable/1.9.x#egg=Django
git+https://github.com/django-nose/django-nose@master#egg=django-nose
[testenv:py27-django18]
basepython = python2.7
deps =
Django>=1.8,<1.9
django-nose==1.4
[testenv:py27-django17]
basepython = python2.7
deps =
Django>=1.7,<1.8
django-nose==1.4
[testenv:py27-django16]
basepython = python2.7
deps =
Django>=1.6,<1.7
[testenv:py27-django15]
basepython = python2.7
deps =
Django>=1.5,<1.6
[testenv:py27-django14]
basepython = python2.7
deps =
Django>=1.4,<1.5
[testenv:py27-django13]
basepython = python2.7
deps =
Django>=1.3,<1.4
django-nose==1.2
[testenv:py27-django12]
basepython = python2.7
deps =
Django>=1.2,<1.3
django-nose==1.2
[testenv:py26-django16]
basepython = python2.6
deps =
Django>=1.6,<1.7
[testenv:py26-django15]
basepython = python2.6
deps =
Django>=1.5,<1.6
[testenv:py26-django14]
basepython = python2.6
deps =
Django>=1.4,<1.5
[testenv:py26-django13]
basepython = python2.6
deps =
Django>=1.3,<1.4
django-nose==1.2
[testenv:py26-django12]
basepython = python2.6
deps =
Django>=1.2,<1.3
django-nose==1.2