diff --git a/.gitignore b/.gitignore index 366c33b0..253a70a6 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ build ghostdriver.log test/media dist/ +.python-version diff --git a/.travis.yml b/.travis.yml index 669ed76c..0107e04f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,13 +18,19 @@ matrix: - { python: 2.7, env: S3=0 TOXENV=py27-dj111 } - { python: 2.7, env: S3=1 TOXENV=py27-dj111 } - { python: 2.7, env: S3=0 TOXENV=py27-dj111-grp } - - { python: 3.6, env: S3=0 TOXENV=py36-dj111 } - - { python: 3.6, env: S3=0 TOXENV=py36-dj20 } - - { python: 3.6, env: S3=1 TOXENV=py36-dj20 } - - { python: 3.6, env: S3=0 TOXENV=py36-dj20-grp } - - { python: 3.6, env: S3=0 TOXENV=py36-dj21 } + - { python: 3.7, env: S3=0 TOXENV=py37-dj111 } + - { python: 3.7, env: S3=0 TOXENV=py37-dj20 } + - { python: 3.7, env: S3=1 TOXENV=py37-dj20 } + - { python: 3.7, env: S3=0 TOXENV=py37-dj20-grp } + - { python: 3.7, env: S3=0 TOXENV=py37-dj21 } + - { python: 3.7, env: S3=0 TOXENV=py37-dj22 } + - { python: 3.7, env: S3=0 TOXENV=py37-dj22-grp } + - { python: 3.7, env: S3=0 TOXENV=py37-dj30 } + - { python: 3.7, env: S3=0 TOXENV=py37-dj30-grp } allow_failures: - - env: S3=0 TOXENV=py36-dj21 + - env: + S3=0 TOXENV=py37-dj30 + S3=0 TOXENV=py37-dj30-grp cache: pip diff --git a/cropduster/__init__.py b/cropduster/__init__.py index 781b02f3..91b226a4 100644 --- a/cropduster/__init__.py +++ b/cropduster/__init__.py @@ -1 +1 @@ -__version__ = '4.12.8' +__version__ = '4.12.9' diff --git a/cropduster/exceptions.py b/cropduster/exceptions.py index eec29ee0..b7b34ac8 100644 --- a/cropduster/exceptions.py +++ b/cropduster/exceptions.py @@ -4,15 +4,15 @@ import copy import errno +import six +from six.moves import xrange + try: from django.urls import get_urlconf, get_resolver except ImportError: from django.core.urlresolvers import get_urlconf, get_resolver from django.http import HttpResponse from django.utils.safestring import mark_safe -from django.utils import six -from django.utils.six.moves import xrange - from django.utils.encoding import force_text diff --git a/cropduster/fields.py b/cropduster/fields.py index 6f452c3e..fd311043 100644 --- a/cropduster/fields.py +++ b/cropduster/fields.py @@ -1,6 +1,7 @@ import contextlib from operator import attrgetter +import six import django from django import forms from django.db import models, transaction, router, DEFAULT_DB_ALIAS @@ -8,7 +9,6 @@ from django.db.models.fields.files import ImageFileDescriptor, ImageFieldFile from django.db.models.fields.related import ManyToManyRel, ManyToManyField from django.utils.functional import cached_property -from django.utils import six from django.contrib.contenttypes.models import ContentType from generic_plus.fields import GenericForeignFileField diff --git a/cropduster/files.py b/cropduster/files.py index 87d1a456..bd938821 100644 --- a/cropduster/files.py +++ b/cropduster/files.py @@ -4,6 +4,9 @@ import re import hashlib +from six.moves.urllib import parse as urlparse +from six.moves.urllib.request import urlopen + from django.core.files.images import get_image_dimensions from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.storage import default_storage @@ -11,8 +14,6 @@ from django.db.models.fields.files import FieldFile, FileField from django.utils.functional import cached_property from django.utils.http import urlunquote_plus -from django.utils.six.moves.urllib import parse as urlparse -from django.utils.six.moves.urllib.request import urlopen from generic_plus.utils import get_relative_media_url, get_media_path diff --git a/cropduster/forms.py b/cropduster/forms.py index c9cdc4e8..90d21be4 100644 --- a/cropduster/forms.py +++ b/cropduster/forms.py @@ -1,3 +1,4 @@ +import six from django import forms from django.core.exceptions import ValidationError from django.forms.models import ModelChoiceIterator @@ -5,7 +6,6 @@ from django.forms.utils import flatatt from django.utils.encoding import force_text from django.utils.html import escape, conditional_escape -from django.utils import six from generic_plus.forms import BaseGenericFileInlineFormSet, GenericForeignFileWidget diff --git a/cropduster/models.py b/cropduster/models.py index e46021cf..3f0e5d4e 100644 --- a/cropduster/models.py +++ b/cropduster/models.py @@ -7,14 +7,14 @@ import time from datetime import datetime +import six +from six.moves import xrange + from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist from django.core.files.storage import FileSystemStorage from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.db import connection, models -from django.utils import six -from django.utils.encoding import python_2_unicode_compatible -from django.utils.six.moves import xrange from django.core.files.storage import default_storage, FileSystemStorage import PIL.Image @@ -44,7 +44,7 @@ def safe_str_path(file_path): return file_path -@python_2_unicode_compatible +@six.python_2_unicode_compatible class Thumb(models.Model): name = models.CharField(max_length=255, db_index=True) @@ -213,7 +213,7 @@ def generate_filename(instance, filename): return filename -@python_2_unicode_compatible +@six.python_2_unicode_compatible class Image(models.Model): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) diff --git a/cropduster/resizing.py b/cropduster/resizing.py index fc694d6e..a9c82088 100644 --- a/cropduster/resizing.py +++ b/cropduster/resizing.py @@ -6,12 +6,11 @@ import hashlib import tempfile +import six +from six.moves import filter import PIL.Image from django.core.exceptions import ImproperlyConfigured -from django.utils import six -from django.utils.encoding import python_2_unicode_compatible -from django.utils.six.moves import filter from django.core.files.storage import default_storage from .settings import CROPDUSTER_RETAIN_METADATA @@ -23,7 +22,7 @@ INFINITY = float('inf') -@python_2_unicode_compatible +@six.python_2_unicode_compatible class SizeAlias(object): is_alias = True @@ -46,7 +45,7 @@ def add_to_sizes_dict(self, sizes): ctx.update(size_to) -@python_2_unicode_compatible +@six.python_2_unicode_compatible class Size(object): is_alias = False diff --git a/cropduster/settings.py b/cropduster/settings.py index 66ea0288..5c4c8be5 100644 --- a/cropduster/settings.py +++ b/cropduster/settings.py @@ -2,9 +2,9 @@ import PIL import distutils.spawn from distutils.version import LooseVersion +import six from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.utils import six CROPDUSTER_MEDIA_ROOT = getattr(settings, 'CROPDUSTER_MEDIA_ROOT', settings.MEDIA_ROOT) @@ -45,8 +45,12 @@ def get_jpeg_quality(width, height): "CROPDUSTER_JPEG_QUALITY setting must be either a callable " "or a numeric value, got type %s" % (type(CROPDUSTER_JPEG_QUALITY).__name__)) -JPEG_SAVE_ICC_SUPPORTED = (LooseVersion(getattr(PIL, 'PILLOW_VERSION', '0')) - >= LooseVersion('2.2.1')) +try: + PILLOW_VERSION = getattr(PIL, '__version__', '0') +except AttributeError: + PILLOW_VERSION = getattr(PIL, 'PILLOW_VERSION', '0') + +JPEG_SAVE_ICC_SUPPORTED = (LooseVersion(PILLOW_VERSION) >= LooseVersion('2.2.1')) CROPDUSTER_GIFSICLE_PATH = getattr(settings, 'CROPDUSTER_GIFSICLE_PATH', None) diff --git a/cropduster/standalone/metadata.py b/cropduster/standalone/metadata.py index 6590fd38..de8f5798 100644 --- a/cropduster/standalone/metadata.py +++ b/cropduster/standalone/metadata.py @@ -5,10 +5,10 @@ import tempfile from io import open +import six from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import default_storage from django.utils.encoding import force_bytes -from django.utils import six from cropduster.files import ImageFile from cropduster.utils import json diff --git a/cropduster/tests/models.py b/cropduster/tests/models.py index 400eab83..c3c05539 100644 --- a/cropduster/tests/models.py +++ b/cropduster/tests/models.py @@ -1,5 +1,5 @@ +import six from django.db import models -from django.utils.encoding import python_2_unicode_compatible from cropduster.fields import ReverseForeignRelation from cropduster.models import CropDusterField, Size @@ -76,7 +76,7 @@ class TestMultipleFieldsInheritanceChild(TestMultipleFieldsInheritanceParent): field_identifier="2") -@python_2_unicode_compatible +@six.python_2_unicode_compatible class TestReverseForeignRelA(models.Model): slug = models.SlugField() c = models.ForeignKey('TestReverseForeignRelC', on_delete=models.CASCADE) @@ -90,7 +90,7 @@ def __str__(self): return self.slug -@python_2_unicode_compatible +@six.python_2_unicode_compatible class TestReverseForeignRelB(models.Model): slug = models.SlugField() c = models.ForeignKey('TestReverseForeignRelC', on_delete=models.CASCADE) @@ -99,7 +99,7 @@ def __str__(self): return self.slug -@python_2_unicode_compatible +@six.python_2_unicode_compatible class TestReverseForeignRelC(models.Model): slug = models.SlugField() rel_a = ReverseForeignRelation( @@ -110,7 +110,7 @@ def __str__(self): return self.slug -@python_2_unicode_compatible +@six.python_2_unicode_compatible class TestReverseForeignRelM2M(models.Model): slug = models.SlugField() m2m = models.ManyToManyField(TestReverseForeignRelC) diff --git a/cropduster/tests/test_models.py b/cropduster/tests/test_models.py index 79001edd..9ba86b4e 100644 --- a/cropduster/tests/test_models.py +++ b/cropduster/tests/test_models.py @@ -4,10 +4,10 @@ import os import PIL +from six.moves import range from django.core.files.storage import default_storage from django.test import TestCase from django.contrib.contenttypes.models import ContentType -from django.utils.six.moves import range from .helpers import CropdusterTestCaseMediaMixin from .models import ( diff --git a/cropduster/utils/image.py b/cropduster/utils/image.py index e2ff3166..4c9bce53 100644 --- a/cropduster/utils/image.py +++ b/cropduster/utils/image.py @@ -6,15 +6,16 @@ import math from distutils.version import LooseVersion +import six +from six.moves import xrange + import PIL.Image from PIL import ImageFile, JpegImagePlugin from django.core.files.storage import default_storage -from django.utils import six -from django.utils.six.moves import xrange from cropduster.settings import ( - get_jpeg_quality, JPEG_SAVE_ICC_SUPPORTED, CROPDUSTER_GIFSICLE_PATH) + get_jpeg_quality, JPEG_SAVE_ICC_SUPPORTED, CROPDUSTER_GIFSICLE_PATH, PILLOW_VERSION) from .gifsicle import GifsicleImage @@ -178,8 +179,7 @@ def smart_resize(im, final_w, final_h): # Pillow 2.7.0 greatly improved the bicubic resize algorithm, which makes # our multiple-step resizing unnecessary - pillow_version = getattr(PIL, 'PILLOW_VERSION', None) - if pillow_version and LooseVersion(pillow_version) >= LooseVersion('2.7.0'): + if PILLOW_VERSION and LooseVersion(PILLOW_VERSION) >= LooseVersion('2.7.0'): return im.resize((final_w, final_h), PIL.Image.BICUBIC) # Attempt to resize the image 1/8, 2/8, such that it is at least 1.5x bigger diff --git a/cropduster/utils/jsonutils.py b/cropduster/utils/jsonutils.py index 57820952..08df44ca 100644 --- a/cropduster/utils/jsonutils.py +++ b/cropduster/utils/jsonutils.py @@ -1,6 +1,6 @@ import json -from django.utils import six -from django.utils.six.moves import filter +import six +from six.moves import filter from cropduster.resizing import Size diff --git a/cropduster/utils/paths.py b/cropduster/utils/paths.py index 5979d238..21b8b317 100644 --- a/cropduster/utils/paths.py +++ b/cropduster/utils/paths.py @@ -3,10 +3,10 @@ import os import re +import six from django.core.files.storage import default_storage, FileSystemStorage from django.conf import settings from django.db.models.fields.files import FileField -from django.utils import six __all__ = ('get_upload_foldername') diff --git a/cropduster/utils/sizes.py b/cropduster/utils/sizes.py index 2abde9f3..89f2baad 100644 --- a/cropduster/utils/sizes.py +++ b/cropduster/utils/sizes.py @@ -1,6 +1,6 @@ from __future__ import division -from django.utils import six +import six from . import jsonutils as json from ..resizing import Size diff --git a/cropduster/views/__init__.py b/cropduster/views/__init__.py index e42ceead..7cb35a01 100644 --- a/cropduster/views/__init__.py +++ b/cropduster/views/__init__.py @@ -32,6 +32,9 @@ import shutil import time +import six +from six.moves import filter, map, zip + import django from django.conf import settings from django.contrib.auth.decorators import login_required @@ -45,8 +48,6 @@ from django.utils.decorators import method_decorator from django.utils.encoding import force_text from django.utils.functional import cached_property -from django.utils import six -from django.utils.six.moves import filter, map, zip from django.views.decorators.csrf import csrf_exempt import PIL.Image diff --git a/cropduster/views/base.py b/cropduster/views/base.py index eed4ca22..fa676fe9 100644 --- a/cropduster/views/base.py +++ b/cropduster/views/base.py @@ -1,10 +1,9 @@ from functools import update_wrapper - from logging import getLogger +import six from django import http from django.utils.decorators import classonlymethod -from django.utils import six logger = getLogger('django.request') diff --git a/cropduster/views/forms.py b/cropduster/views/forms.py index 2fb6991f..c63704b1 100644 --- a/cropduster/views/forms.py +++ b/cropduster/views/forms.py @@ -3,6 +3,7 @@ import os import hashlib +import six import PIL.Image from django import forms @@ -16,7 +17,6 @@ from django.utils.html import conditional_escape from django.utils.safestring import mark_safe from django.templatetags.static import static -from django.utils import six from cropduster.models import Thumb from cropduster.utils import (json, get_upload_foldername, get_min_size, diff --git a/runtests.py b/runtests.py index e21dbc7b..64d82c87 100755 --- a/runtests.py +++ b/runtests.py @@ -8,6 +8,13 @@ class RunTests(selenosis.RunTests): def __call__(self, *args, **kwargs): warnings.simplefilter("error", Warning) warnings.filterwarnings('ignore', message='.*?ckeditor') + + # Introduced in Python 3.7 + warnings.filterwarnings( + 'ignore', + category=DeprecationWarning, + message="Using or importing the ABCs from 'collections' instead of from 'collections.abc'", + ) super(RunTests, self).__call__(*args, **kwargs) diff --git a/setup.py b/setup.py index c561a484..7c823d25 100755 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ license='BSD', platforms='any', install_requires=[ + 'six', 'Pillow', 'python-xmp-toolkit', 'django-generic-plus>=2.0.3', @@ -37,7 +38,7 @@ "Programming Language :: Python :: 2", 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', ]) diff --git a/tox.ini b/tox.ini index 978007bd..f4a1b94f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,10 @@ [tox] envlist = - py{27,36}-dj111{,-grp} - py36-dj20{,-grp} - py36-dj21 + py{27,37}-dj111{,-grp} + py37-dj20{,-grp} + py37-dj21 + py37-dj22{,-grp} + py37-dj30{,-grp} [testenv] commands = @@ -20,12 +22,19 @@ deps = django-selenosis boto3==1.12.18 django-storages==1.9.1 - dj18: Django>=1.8,<1.8.99 - dj111: Django>=1.11a1,<1.11.99 - dj20: Django>=2.0.0,<2.0.99 - dj18-grp: django-grappelli==2.7.3 - dj111-grp: django-grappelli==2.10.2 + dj111: Django==1.11.29 + dj20: Django==2.0.13 + dj21: Django==2.1.15 + dj22: Django>=2.0,<3.0 + dj30: Django>=3.0,<3.1 + dj111-grp: django-grappelli==2.10.4 dj20-grp: django-grappelli==2.11.1 - dj21: https://github.com/django/django/archive/master.tar.gz + dj21-grp: django-grappelli==2.11.1 + dj22-grp: django-grappelli>=2.13.4 + dj30-grp: django-grappelli>=2.14.2 lxml - -e git+https://github.com/theatlantic/django-ckeditor.git@v4.5.7+atl.6.1#egg=django-ckeditor + # TODO - Once changes for https://github.com/theatlantic/django-ckeditor.git are complete, + # uncomment next line and remove next line with the specific git SHA hash + # -e git+https://github.com/theatlantic/django-ckeditor.git@v4.5.7+atl.7.1#egg=django-ckeditor + -e git+https://github.com/theatlantic/django-ckeditor.git@2100ec804e8fc142585c0b6c373cfeeccddf7618#egg=django-ckeditor +