diff --git a/AUTHORS b/AUTHORS index 255827b..6f4c238 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,6 +28,7 @@ Contributors * `Jannis Leidel`_ * `Sean Bell`_ * `Saul Shanabrook`_ +* `Venelin Stoykov`_ .. _Justin Driscoll: http://github.com/jdriscoll .. _HZDG: http://hzdg.com @@ -49,3 +50,4 @@ Contributors .. _Jannis Leidel: https://github.com/jezdez .. _Sean Bell: https://github.com/seanbell .. _Saul Shanabrook: https://github.com/saulshanabrook +.. _Venelin Stoykov: https://github.com/vstoykov diff --git a/imagekit/cachefiles/__init__.py b/imagekit/cachefiles/__init__.py index ec4b356..34892ec 100644 --- a/imagekit/cachefiles/__init__.py +++ b/imagekit/cachefiles/__init__.py @@ -121,7 +121,7 @@ class ImageCacheFile(BaseIKFile, ImageFile): ) ) - def __nonzero__(self): + def __bool__(self): if not self.name: return False @@ -138,6 +138,10 @@ class ImageCacheFile(BaseIKFile, ImageFile): return state + def __nonzero__(self): + # Python 2 compatibility + return self.__bool__() + class LazyImageCacheFile(SimpleLazyObject): def __init__(self, generator_id, *args, **kwargs): diff --git a/imagekit/cachefiles/strategies.py b/imagekit/cachefiles/strategies.py index fba6a0f..0d11dae 100644 --- a/imagekit/cachefiles/strategies.py +++ b/imagekit/cachefiles/strategies.py @@ -1,4 +1,7 @@ +import six + from django.utils.functional import LazyObject +from ..lib import force_text from ..utils import get_singleton @@ -35,7 +38,7 @@ class DictStrategy(object): class StrategyWrapper(LazyObject): def __init__(self, strategy): - if isinstance(strategy, basestring): + if isinstance(strategy, six.string_types): strategy = get_singleton(strategy, 'cache file strategy') elif isinstance(strategy, dict): strategy = DictStrategy(strategy) @@ -50,7 +53,7 @@ class StrategyWrapper(LazyObject): self._wrapped = state['_wrapped'] def __unicode__(self): - return unicode(self._wrapped) + return force_text(self._wrapped) def __str__(self): return str(self._wrapped) diff --git a/imagekit/files.py b/imagekit/files.py index fb4375c..e717888 100644 --- a/imagekit/files.py +++ b/imagekit/files.py @@ -1,6 +1,9 @@ -from django.core.files.base import File, ContentFile -from django.utils.encoding import smart_str, smart_unicode +from __future__ import unicode_literals import os + +from django.core.files.base import File, ContentFile +from django.utils.encoding import smart_str +from .lib import smart_text from .utils import format_to_mimetype, extension_to_mimetype @@ -92,4 +95,5 @@ class IKContentFile(ContentFile): return smart_str(self.file.name or '') def __unicode__(self): - return smart_unicode(self.file.name or u'') + # Python 2 + return smart_text(self.file.name or '') diff --git a/imagekit/hashers.py b/imagekit/hashers.py index 4231fa5..12825bd 100644 --- a/imagekit/hashers.py +++ b/imagekit/hashers.py @@ -1,12 +1,16 @@ from copy import copy from hashlib import md5 -from pickle import Pickler, MARK, DICT -from types import DictionaryType +from pickle import MARK, DICT +try: + from pickle import _Pickler +except ImportError: + # Python 2 compatible + from pickle import Pickler as _Pickler from .lib import StringIO -class CanonicalizingPickler(Pickler): - dispatch = copy(Pickler.dispatch) +class CanonicalizingPickler(_Pickler): + dispatch = copy(_Pickler.dispatch) def save_set(self, obj): rv = obj.__reduce_ex__(0) @@ -20,9 +24,9 @@ class CanonicalizingPickler(Pickler): write(MARK + DICT) self.memoize(obj) - self._batch_setitems(sorted(obj.iteritems())) + self._batch_setitems(sorted(obj.items())) - dispatch[DictionaryType] = save_dict + dispatch[dict] = save_dict def pickle(obj): diff --git a/imagekit/importers.py b/imagekit/importers.py index cddcd83..87ba2b6 100644 --- a/imagekit/importers.py +++ b/imagekit/importers.py @@ -1,4 +1,3 @@ -from django.utils.importlib import import_module import re import sys @@ -22,8 +21,11 @@ class ProcessorImporter(object): if name in sys.modules: return sys.modules[name] + from django.utils.importlib import import_module new_name = self.pattern.sub(r'pilkit.processors\1', name) - return import_module(new_name) + mod = import_module(new_name) + sys.modules[name] = mod + return mod -sys.meta_path.append(ProcessorImporter()) +sys.meta_path.insert(0, ProcessorImporter()) diff --git a/imagekit/lib.py b/imagekit/lib.py index 6da2bfb..5a4240b 100644 --- a/imagekit/lib.py +++ b/imagekit/lib.py @@ -19,9 +19,12 @@ except ImportError: raise ImportError('ImageKit was unable to import the Python Imaging Library. Please confirm it`s installed and available on your current Python path.') try: - from cStringIO import StringIO -except ImportError: - from StringIO import StringIO + from io import BytesIO as StringIO +except: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO try: from logging import NullHandler @@ -31,3 +34,19 @@ except ImportError: class NullHandler(Handler): def emit(self, record): pass + +# Try to import `force_text` available from Django 1.5 +# This function will replace `unicode` used in the code +# If Django version is under 1.5 then use `force_unicde` +# It is used for compatibility between Python 2 and Python 3 +try: + from django.utils.encoding import force_text, force_bytes, smart_text +except ImportError: + # Django < 1.5 + from django.utils.encoding import (force_unicode as force_text, + smart_str as force_bytes, + smart_unicode as smart_text) + +__all__ = ['Image', 'ImageColor', 'ImageChops', 'ImageEnhance', 'ImageFile', + 'ImageFilter', 'ImageDraw', 'ImageStat', 'StringIO', 'NullHandler', + 'force_text', 'force_bytes', 'smart_text'] diff --git a/imagekit/models/fields/__init__.py b/imagekit/models/fields/__init__.py index 91c6252..61a7dd3 100644 --- a/imagekit/models/fields/__init__.py +++ b/imagekit/models/fields/__init__.py @@ -1,4 +1,8 @@ +from __future__ import unicode_literals + from django.db import models +from django.db.models.signals import class_prepared +from django.dispatch import receiver from .files import ProcessedImageFieldFile from .utils import ImageSpecFileDescriptor from ...specs import SpecHost @@ -13,7 +17,7 @@ class SpecHostField(SpecHost): # Generate a spec_id to register the spec with. The default spec id is # ":_" if not spec_id: - spec_id = (u'%s:%s:%s' % (cls._meta.app_label, + spec_id = ('%s:%s:%s' % (cls._meta.app_label, cls._meta.object_name, name)).lower() # Register the spec with the id. This allows specs to be overridden @@ -44,27 +48,38 @@ class ImageSpecField(SpecHostField): def contribute_to_class(self, cls, name): # If the source field name isn't defined, figure it out. - source = self.source - if not source: - image_fields = [f.attname for f in cls._meta.fields if - isinstance(f, models.ImageField)] - if len(image_fields) == 0: - raise Exception( - '%s does not define any ImageFields, so your %s' - ' ImageSpecField has no image to act on.' % - (cls.__name__, name)) - elif len(image_fields) > 1: - raise Exception( - '%s defines multiple ImageFields, but you have not' - ' specified a source for your %s ImageSpecField.' % - (cls.__name__, name)) - source = image_fields[0] - setattr(cls, name, ImageSpecFileDescriptor(self, name, source)) - self._set_spec_id(cls, name) + def register_source_group(source): + setattr(cls, name, ImageSpecFileDescriptor(self, name, source)) + self._set_spec_id(cls, name) + + # Add the model and field as a source for this spec id + register.source_group(self.spec_id, ImageFieldSourceGroup(cls, source)) + + if self.source: + register_source_group(self.source) + else: + # The source argument is not defined + # Then we need to see if there is only one ImageField in that model + # But we need to do that after full model initialization + + @receiver(class_prepared, sender=cls, weak=False) + def handle_model_preparation(sender, **kwargs): + + image_fields = [f.attname for f in cls._meta.fields if + isinstance(f, models.ImageField)] + if len(image_fields) == 0: + raise Exception( + '%s does not define any ImageFields, so your %s' + ' ImageSpecField has no image to act on.' % + (cls.__name__, name)) + elif len(image_fields) > 1: + raise Exception( + '%s defines multiple ImageFields, but you have not' + ' specified a source for your %s ImageSpecField.' % + (cls.__name__, name)) + register_source_group(image_fields[0]) - # Add the model and field as a source for this spec id - register.source_group(self.spec_id, ImageFieldSourceGroup(cls, source)) class ProcessedImageField(models.ImageField, SpecHostField): diff --git a/imagekit/templatetags/imagekit.py b/imagekit/templatetags/imagekit.py index 2df5b4d..25c2cca 100644 --- a/imagekit/templatetags/imagekit.py +++ b/imagekit/templatetags/imagekit.py @@ -1,9 +1,13 @@ +from __future__ import unicode_literals + from django import template from django.utils.html import escape from django.utils.safestring import mark_safe + from .compat import parse_bits from ..cachefiles import ImageCacheFile from ..registry import generator_registry +from ..lib import force_text register = template.Library() @@ -40,7 +44,7 @@ class GenerateImageAssignmentNode(template.Node): self._variable_name = variable_name def get_variable_name(self, context): - return unicode(self._variable_name) + return force_text(self._variable_name) def render(self, context): variable_name = self.get_variable_name(context) @@ -70,7 +74,7 @@ class GenerateImageTagNode(template.Node): attrs['src'] = file.url attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in attrs.items()) - return mark_safe(u'' % attr_str) + return mark_safe('' % attr_str) class ThumbnailAssignmentNode(template.Node): @@ -83,7 +87,7 @@ class ThumbnailAssignmentNode(template.Node): self._generator_kwargs = generator_kwargs def get_variable_name(self, context): - return unicode(self._variable_name) + return force_text(self._variable_name) def render(self, context): variable_name = self.get_variable_name(context) @@ -131,7 +135,7 @@ class ThumbnailImageTagNode(template.Node): attrs['src'] = file.url attr_str = ' '.join('%s="%s"' % (escape(k), escape(v)) for k, v in attrs.items()) - return mark_safe(u'' % attr_str) + return mark_safe('' % attr_str) def parse_ik_tag_bits(parser, bits): diff --git a/imagekit/utils.py b/imagekit/utils.py index 13bd843..0973d35 100644 --- a/imagekit/utils.py +++ b/imagekit/utils.py @@ -1,17 +1,18 @@ +from __future__ import unicode_literals import logging +import re from tempfile import NamedTemporaryFile +from hashlib import md5 from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.files import File from django.utils.importlib import import_module -from hashlib import md5 from pilkit.utils import * -import re -from .lib import NullHandler +from .lib import NullHandler, force_bytes -bad_memcached_key_chars = re.compile(ur'[\u0000-\u001f\s]+') +bad_memcached_key_chars = re.compile('[\u0000-\u001f\\s]+') _autodiscovered = False @@ -32,7 +33,7 @@ def get_by_qname(path, desc): module, objname = path[:dot], path[dot + 1:] try: mod = import_module(module) - except ImportError, e: + except ImportError as e: raise ImproperlyConfigured('Error importing %s module %s: "%s"' % (desc, module, e)) try: @@ -147,7 +148,7 @@ def sanitize_cache_key(key): # The also can't be > 250 chars long. Since we don't know what the # user's cache ``KEY_FUNCTION`` setting is like, we'll limit it to 200. if len(new_key) >= 200: - new_key = '%s:%s' % (new_key[:200-33], md5(key).hexdigest()) + new_key = '%s:%s' % (new_key[:200-33], md5(force_bytes(key)).hexdigest()) key = new_key return key diff --git a/setup.py b/setup.py index da959a0..c54c041 100644 --- a/setup.py +++ b/setup.py @@ -19,10 +19,12 @@ if 'publish' in sys.argv: 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 = {} -execfile(os.path.join(os.path.dirname(__file__), +exec_file(os.path.join(os.path.dirname(__file__), 'imagekit', 'pkgmeta.py'), pkgmeta) @@ -42,15 +44,16 @@ setup( include_package_data=True, tests_require=[ 'beautifulsoup4==4.1.3', - 'nose==1.2.1', - 'nose-progressive==1.3', - 'django-nose==1.1', + 'nose==1.3.0', + 'nose-progressive==1.5', + 'django-nose==1.2', 'Pillow<3.0', ], test_suite='testrunner.run_tests', install_requires=[ 'django-appconf>=0.5', 'pilkit>=0.2.0', + 'six', ], extras_require={ 'async': ['django-celery>=3.0'], @@ -62,9 +65,10 @@ setup( 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.5', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', 'Topic :: Utilities' ], ) diff --git a/tests/test_cachefiles.py b/tests/test_cachefiles.py index 4338a82..8a93e71 100644 --- a/tests/test_cachefiles.py +++ b/tests/test_cachefiles.py @@ -2,6 +2,7 @@ from django.conf import settings from hashlib import md5 from imagekit.cachefiles import ImageCacheFile, LazyImageCacheFile from imagekit.cachefiles.backends import Simple +from imagekit.lib import force_bytes from nose.tools import raises, eq_ from .imagegenerators import TestSpec from .utils import (assert_file_is_truthy, assert_file_is_falsy, @@ -73,7 +74,7 @@ def test_memcached_cache_key(): eq_(backend.get_key(file), '%s%s:%s' % ( settings.IMAGEKIT_CACHE_PREFIX, '1' * (200 - len(':') - 32 - len(settings.IMAGEKIT_CACHE_PREFIX)), - md5('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename)).hexdigest())) + md5(force_bytes('%s%s-state' % (settings.IMAGEKIT_CACHE_PREFIX, filename))).hexdigest())) def test_lazyfile_stringification(): diff --git a/tox.ini b/tox.ini index 46ca387..1dc2b41 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,23 @@ [tox] envlist = + py33-django15, + py32-django15, py27-django15, py27-django14, py27-django13, py27-django12, py26-django15, py26-django14, py26-django13, py26-django12 [testenv] commands = python setup.py test +[testenv:py33-django15] +basepython = python3.3 +deps = + Django>=1.5,<1.6 + +[testenv:py32-django15] +basepython = python3.2 +deps = + Django>=1.5,<1.6 + [testenv:py27-django15] basepython = python2.7 deps =