From 33793f7c3edd8ff144ff2e9434367267c20af26a Mon Sep 17 00:00:00 2001 From: Daniel Lindsley Date: Tue, 14 May 2013 19:31:16 -0700 Subject: [PATCH] Fixed #19934 - Use of Pillow is now preferred over PIL. This starts the deprecation period for PIL (support to end in 1.8). --- django/core/files/images.py | 8 +- django/core/management/validation.py | 8 +- django/forms/fields.py | 16 +-- django/utils/image.py | 146 ++++++++++++++++++++++++++ docs/faq/contributing.txt | 2 +- docs/internals/deprecation.txt | 6 ++ docs/ref/forms/fields.txt | 12 ++- docs/releases/1.6.txt | 7 ++ tests/file_storage/tests.py | 20 ++-- tests/model_fields/models.py | 17 ++- tests/model_fields/test_imagefield.py | 8 +- tests/model_forms/models.py | 12 +-- tests/serializers_regress/models.py | 2 +- 13 files changed, 198 insertions(+), 66 deletions(-) create mode 100644 django/utils/image.py diff --git a/django/core/files/images.py b/django/core/files/images.py index 0d87ae853e..e1d6091658 100644 --- a/django/core/files/images.py +++ b/django/core/files/images.py @@ -1,7 +1,7 @@ """ Utility functions for handling images. -Requires PIL, as you might imagine. +Requires Pillow (or PIL), as you might imagine. """ import zlib @@ -35,11 +35,7 @@ def get_image_dimensions(file_or_path, close=False): 'close' to True to close the file at the end if it is initially in an open state. """ - # Try to import PIL in either of the two ways it can end up installed. - try: - from PIL import ImageFile as PILImageFile - except ImportError: - import ImageFile as PILImageFile + from django.utils.image import ImageFile as PILImageFile p = PILImageFile.Parser() if hasattr(file_or_path, 'read'): diff --git a/django/core/management/validation.py b/django/core/management/validation.py index 94d604346b..0f0eade569 100644 --- a/django/core/management/validation.py +++ b/django/core/management/validation.py @@ -105,14 +105,10 @@ def get_validation_errors(outfile, app=None): if isinstance(f, models.FileField) and not f.upload_to: e.add(opts, '"%s": FileFields require an "upload_to" attribute.' % f.name) if isinstance(f, models.ImageField): - # Try to import PIL in either of the two ways it can end up installed. try: - from PIL import Image + from django.utils.image import Image except ImportError: - try: - import Image - except ImportError: - e.add(opts, '"%s": To use ImageFields, you need to install the Python Imaging Library. Get it at http://www.pythonware.com/products/pil/ .' % f.name) + e.add(opts, '"%s": To use ImageFields, you need to install Pillow. Get it at https://pypi.python.org/pypi/Pillow.' % f.name) if isinstance(f, models.BooleanField) and getattr(f, 'null', False): e.add(opts, '"%s": BooleanFields do not accept null values. Use a NullBooleanField instead.' % f.name) if isinstance(f, models.FilePathField) and not (f.allow_files or f.allow_folders): diff --git a/django/forms/fields.py b/django/forms/fields.py index 146a10d635..4ce57d34a3 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -602,13 +602,9 @@ class ImageField(FileField): if f is None: return None - # Try to import PIL in either of the two ways it can end up installed. - try: - from PIL import Image - except ImportError: - import Image + from django.utils.image import Image - # We need to get a file object for PIL. We might have a path or we might + # We need to get a file object for Pillow. We might have a path or we might # have to read the data into memory. if hasattr(data, 'temporary_file_path'): file = data.temporary_file_path() @@ -623,12 +619,8 @@ class ImageField(FileField): # image in memory, which is a DoS vector. See #3848 and #18520. # verify() must be called immediately after the constructor. Image.open(file).verify() - except ImportError: - # Under PyPy, it is possible to import PIL. However, the underlying - # _imaging C module isn't available, so an ImportError will be - # raised. Catch and re-raise. - raise - except Exception: # Python Imaging Library doesn't recognize it as an image + except Exception: + # Pillow (or PIL) doesn't recognize it as an image. six.reraise(ValidationError, ValidationError(self.error_messages['invalid_image']), sys.exc_info()[2]) if hasattr(f, 'seek') and callable(f.seek): f.seek(0) diff --git a/django/utils/image.py b/django/utils/image.py new file mode 100644 index 0000000000..ed9b210973 --- /dev/null +++ b/django/utils/image.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +""" +To provide a shim layer over Pillow/PIL situation until the PIL support is +removed. + + +Combinations To Account For +=========================== + +* Pillow: + + * never has ``_imaging`` under any Python + * has the ``Image.alpha_composite``, which may aid in detection + +* PIL + + * CPython 2.x may have _imaging (& work) + * CPython 2.x may *NOT* have _imaging (broken & needs a error message) + * CPython 3.x doesn't work + * PyPy will *NOT* have _imaging (but works?) + +Restated, that looks like: + +* If we're on Python 2.x, it could be either Pillow or PIL: + + * If ``import _imaging`` results in ``ImportError``, either they have a + working Pillow installation or a broken PIL installation, so we need to + detect further: + + * To detect, we first ``import Image``. + * If ``Image`` has a ``alpha_composite`` attribute present, only Pillow + has this, so we assume it's working. + * If ``Image`` DOES NOT have a ``alpha_composite``attribute, it must be + PIL & is a broken (likely C compiler-less) install, which we need to + warn the user about. + + * If ``import _imaging`` works, it must be PIL & is a working install. + +* Python 3.x + + * If ``import Image`` works, it must be Pillow, since PIL isn't Python 3.x + compatible. + +* PyPy + + * If ``import _imaging`` results in ``ImportError``, it could be either + Pillow or PIL, both of which work without it on PyPy, so we're fine. + + +Approach +======== + +* Attempt to import ``Image`` + + * ``ImportError`` - nothing is installed, toss an exception + * Either Pillow or the PIL is installed, so continue detecting + +* Attempt to ``hasattr(Image, 'alpha_composite')`` + + * If it works, it's Pillow & working + * If it fails, we've got a PIL install, continue detecting + + * The only option here is that we're on Python 2.x or PyPy, of which + we only care about if we're on CPython. + * If we're on CPython, attempt to ``import _imaging`` + + * ``ImportError`` - Bad install, toss an exception + +""" +import warnings + +from django.core.exceptions import ImproperlyConfigured +from django.utils.translation import ugettext_lazy as _ + + +Image = None +_imaging = None +ImageFile = None + + +def _detect_image_library(): + global Image + global _imaging + global ImageFile + + # Skip re-attempting to import if we've already run detection. + if Image is not None: + return Image, _imaging, ImageFile + + # Assume it's not there. + PIL_imaging = False + + try: + # Try from the Pillow (or one variant of PIL) install location first. + from PIL import Image as PILImage + except ImportError as err: + try: + # If that failed, try the alternate import syntax for PIL. + import Image as PILImage + except ImportError as err: + # Neither worked, so it's likely not installed. + raise ImproperlyConfigured( + _(u"Neither Pillow nor PIL could be imported: %s" % err) + ) + + # ``Image.alpha_composite`` was added to Pillow in SHA: e414c6 & is not + # available in any version of the PIL. + if hasattr(PILImage, u'alpha_composite'): + PIL_imaging = False + else: + # We're dealing with the PIL. Determine if we're on CPython & if + # ``_imaging`` is available. + import platform + + # This is the Alex Approved™ way. + # See http://mail.python.org/pipermail//pypy-dev/2011-November/008739.html + if platform.python_implementation().lower() == u'cpython': + # We're on CPython (likely 2.x). Since a C compiler is needed to + # produce a fully-working PIL & will create a ``_imaging`` module, + # we'll attempt to import it to verify their kit works. + try: + import _imaging as PIL_imaging + except ImportError as err: + raise ImproperlyConfigured( + _(u"The '_imaging' module for the PIL could not be " + + u"imported: %s" % err) + ) + + # Try to import ImageFile as well. + try: + from PIL import ImageFile as PILImageFile + except ImportError: + import ImageFile as PILImageFile + + # Finally, warn about deprecation... + if PIL_imaging is not False: + warnings.warn( + "Support for the PIL will be removed in Django 1.8. Please " + + "uninstall it & install Pillow instead.", + PendingDeprecationWarning + ) + + return PILImage, PIL_imaging, PILImageFile + + +Image, _imaging, ImageFile = _detect_image_library() diff --git a/docs/faq/contributing.txt b/docs/faq/contributing.txt index 6f2dfd906f..20950e88c5 100644 --- a/docs/faq/contributing.txt +++ b/docs/faq/contributing.txt @@ -27,7 +27,7 @@ to make it dead easy, even for someone who may not be intimately familiar with that area of the code, to understand the problem and verify the fix: * Are there clear instructions on how to reproduce the bug? If this - touches a dependency (such as PIL), a contrib module, or a specific + touches a dependency (such as Pillow/PIL), a contrib module, or a specific database, are those instructions clear enough even for someone not familiar with it? diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 862907b2a8..774de2a2fd 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -365,6 +365,12 @@ these changes. * ``django.conf.urls.shortcut`` and ``django.views.defaults.shortcut`` will be removed. +* Support for the Python Imaging Library (PIL) module will be removed, as it + no longer appears to be actively maintained & does not work on Python 3. + You are advised to install `Pillow`_, which should be used instead. + +.. _`Pillow`: https://pypi.python.org/pypi/Pillow + * The following private APIs will be removed: - ``django.db.close_connection()`` diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 29f889445d..054f45c430 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -608,19 +608,21 @@ For each field, we describe the default widget used if you don't specify * Normalizes to: An ``UploadedFile`` object that wraps the file content and file name into a single object. * Validates that file data has been bound to the form, and that the - file is of an image format understood by PIL. + file is of an image format understood by Pillow/PIL. * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``, ``invalid_image`` - Using an ``ImageField`` requires that the `Python Imaging Library`_ (PIL) - is installed and supports the image formats you use. If you encounter a - ``corrupt image`` error when you upload an image, it usually means PIL + Using an ``ImageField`` requires that either `Pillow`_ (recommended) or the + `Python Imaging Library`_ (PIL) are installed and supports the image + formats you use. If you encounter a ``corrupt image`` error when you + upload an image, it usually means either Pillow or PIL doesn't understand its format. To fix this, install the appropriate - library and reinstall PIL. + library and reinstall Pillow or PIL. When you use an ``ImageField`` on a form, you must also remember to :ref:`bind the file data to the form `. +.. _Pillow: http://python-imaging.github.io/Pillow/ .. _Python Imaging Library: http://www.pythonware.com/products/pil/ ``IntegerField`` diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 5780229eb5..7469783659 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -220,6 +220,13 @@ Minor features * Added ``BCryptSHA256PasswordHasher`` to resolve the password truncation issue with bcrypt. +* `Pillow`_ is now the preferred image manipulation library to use with Django. + `PIL`_ is pending deprecation (support to be removed in Django 1.8). + To upgrade, you should **first** uninstall PIL, **then** install Pillow. + +.. _`Pillow`: https://pypi.python.org/pypi/Pillow +.. _`PIL`: https://pypi.python.org/pypi/PIL + Backwards incompatible changes in 1.6 ===================================== diff --git a/tests/file_storage/tests.py b/tests/file_storage/tests.py index 5e6adee894..e4b71dba82 100644 --- a/tests/file_storage/tests.py +++ b/tests/file_storage/tests.py @@ -29,16 +29,10 @@ from django.utils._os import upath from django.test.utils import override_settings from servers.tests import LiveServerBase -# Try to import PIL in either of the two ways it can end up installed. -# Checking for the existence of Image is enough for CPython, but -# for PyPy, you need to check for the underlying modules try: - from PIL import Image, _imaging -except ImportError: - try: - import Image, _imaging - except ImportError: - Image = None + from django.utils.image import Image +except ImproperlyConfigured: + Image = None class GetStorageClassTests(SimpleTestCase): @@ -494,7 +488,7 @@ class DimensionClosingBug(unittest.TestCase): """ Test that get_image_dimensions() properly closes files (#8817) """ - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_not_closing_of_files(self): """ Open files passed into get_image_dimensions() should stay opened. @@ -505,7 +499,7 @@ class DimensionClosingBug(unittest.TestCase): finally: self.assertTrue(not empty_io.closed) - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_closing_of_filenames(self): """ get_image_dimensions() called with a filename should closed the file. @@ -542,7 +536,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): Test that get_image_dimensions() works properly after various calls using a file handler (#11158) """ - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_multiple_calls(self): """ Multiple calls of get_image_dimensions() should return the same size. @@ -556,7 +550,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase): self.assertEqual(image_pil.size, size_1) self.assertEqual(size_1, size_2) - @unittest.skipUnless(Image, "PIL not installed") + @unittest.skipUnless(Image, "Pillow/PIL not installed") def test_bug_19457(self): """ Regression test for #19457 diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index c3b2f7fccb..2d602d6412 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -1,17 +1,12 @@ import os import tempfile -# Try to import PIL in either of the two ways it can end up installed. -# Checking for the existence of Image is enough for CPython, but for PyPy, -# you need to check for the underlying modules. +from django.core.exceptions import ImproperlyConfigured try: - from PIL import Image, _imaging -except ImportError: - try: - import Image, _imaging - except ImportError: - Image = None + from django.utils.image import Image +except ImproperlyConfigured: + Image = None from django.core.files.storage import FileSystemStorage from django.db import models @@ -87,7 +82,7 @@ class VerboseNameField(models.Model): field9 = models.FileField("verbose field9", upload_to="unused") field10 = models.FilePathField("verbose field10") field11 = models.FloatField("verbose field11") - # Don't want to depend on PIL in this test + # Don't want to depend on Pillow/PIL in this test #field_image = models.ImageField("verbose field") field12 = models.IntegerField("verbose field12") field13 = models.IPAddressField("verbose field13") @@ -119,7 +114,7 @@ class Document(models.Model): ############################################################################### # ImageField -# If PIL available, do these tests. +# If Pillow/PIL available, do these tests. if Image: class TestImageFieldFile(ImageFieldFile): """ diff --git a/tests/model_fields/test_imagefield.py b/tests/model_fields/test_imagefield.py index df0215db3d..457892ddb8 100644 --- a/tests/model_fields/test_imagefield.py +++ b/tests/model_fields/test_imagefield.py @@ -3,20 +3,24 @@ from __future__ import absolute_import import os import shutil +from django.core.exceptions import ImproperlyConfigured from django.core.files import File from django.core.files.images import ImageFile from django.test import TestCase from django.utils._os import upath from django.utils.unittest import skipIf -from .models import Image +try: + from .models import Image +except ImproperlyConfigured: + Image = None if Image: from .models import (Person, PersonWithHeight, PersonWithHeightAndWidth, PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile) from .models import temp_storage_dir else: - # PIL not available, create dummy classes (tests will be skipped anyway) + # Pillow not available, create dummy classes (tests will be skipped anyway) class Person(): pass PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person diff --git a/tests/model_forms/models.py b/tests/model_forms/models.py index 25c780f1c2..a79d9b8c5b 100644 --- a/tests/model_forms/models.py +++ b/tests/model_forms/models.py @@ -11,6 +11,7 @@ from __future__ import unicode_literals import os import tempfile +from django.core.exceptions import ImproperlyConfigured from django.core.files.storage import FileSystemStorage from django.db import models from django.utils import six @@ -91,14 +92,7 @@ class TextFile(models.Model): return self.description try: - # If PIL is available, try testing ImageFields. Checking for the existence - # of Image is enough for CPython, but for PyPy, you need to check for the - # underlying modules If PIL is not available, ImageField tests are omitted. - # Try to import PIL in either of the two ways it can end up installed. - try: - from PIL import Image, _imaging - except ImportError: - import Image, _imaging + from django.utils.image import Image test_images = True @@ -137,7 +131,7 @@ try: def __str__(self): return self.description -except ImportError: +except ImproperlyConfigured: test_images = False @python_2_unicode_compatible diff --git a/tests/serializers_regress/models.py b/tests/serializers_regress/models.py index 21fe073122..21a3448a8e 100644 --- a/tests/serializers_regress/models.py +++ b/tests/serializers_regress/models.py @@ -2,7 +2,7 @@ A test spanning all the capabilities of all the serializers. This class sets up a model for each model field type -(except for image types, because of the PIL dependency). +(except for image types, because of the Pillow/PIL dependency). """ from django.db import models