Removed PIL compatability layer per deprecation timeline.

refs #19934.
This commit is contained in:
Tim Graham 2014-03-21 10:54:53 -04:00
parent 6d1ae5e27c
commit 4965a77407
15 changed files with 48 additions and 211 deletions

View File

@ -1,7 +1,7 @@
""" """
Utility functions for handling images. Utility functions for handling images.
Requires Pillow (or PIL), as you might imagine. Requires Pillow as you might imagine.
""" """
import zlib import zlib
@ -35,9 +35,9 @@ 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 'close' to True to close the file at the end if it is initially in an open
state. state.
""" """
from django.utils.image import ImageFile as PILImageFile from PIL import ImageFile as PillowImageFile
p = PILImageFile.Parser() p = PillowImageFile.Parser()
if hasattr(file_or_path, 'read'): if hasattr(file_or_path, 'read'):
file = file_or_path file = file_or_path
file_pos = file.tell() file_pos = file.tell()
@ -46,9 +46,9 @@ def get_image_dimensions(file_or_path, close=False):
file = open(file_or_path, 'rb') file = open(file_or_path, 'rb')
close = True close = True
try: try:
# Most of the time PIL only needs a small chunk to parse the image and # Most of the time Pillow only needs a small chunk to parse the image
# get the dimensions, but with some TIFF files PIL needs to parse the # and get the dimensions, but with some TIFF files Pillow needs to
# whole file. # parse the whole file.
chunk_size = 1024 chunk_size = 1024
while 1: while 1:
data = file.read(chunk_size) data = file.read(chunk_size)

View File

@ -4,7 +4,6 @@ import os
from django import forms from django import forms
from django.db.models.fields import Field from django.db.models.fields import Field
from django.core import checks from django.core import checks
from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import File from django.core.files.base import File
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.files.images import ImageFile from django.core.files.images import ImageFile
@ -386,8 +385,8 @@ class ImageField(FileField):
def _check_image_library_installed(self): def _check_image_library_installed(self):
try: try:
from django.utils.image import Image # NOQA from PIL import Image # NOQA
except ImproperlyConfigured: except ImportError:
return [ return [
checks.Error( checks.Error(
'Cannot use ImageField because Pillow is not installed.', 'Cannot use ImageField because Pillow is not installed.',

View File

@ -641,7 +641,7 @@ class ImageField(FileField):
if f is None: if f is None:
return None return None
from django.utils.image import Image from PIL import Image
# We need to get a file object for Pillow. 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. # have to read the data into memory.
@ -659,7 +659,7 @@ class ImageField(FileField):
# verify() must be called immediately after the constructor. # verify() must be called immediately after the constructor.
Image.open(file).verify() Image.open(file).verify()
except Exception: except Exception:
# Pillow (or PIL) doesn't recognize it as an image. # Pillow doesn't recognize it as an image.
six.reraise(ValidationError, ValidationError( six.reraise(ValidationError, ValidationError(
self.error_messages['invalid_image'], self.error_messages['invalid_image'],
code='invalid_image', code='invalid_image',

View File

@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
"""
To provide a shim layer over Pillow/PIL situation until the PIL support is
removed. See #19934.
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?)
* On some platforms (Homebrew and RHEL6 reported) _imaging isn't available,
the needed import is from PIL import _imaging (refs #21355)
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 ``from PIL import _imaging`` and
``import _imaging``
* ``ImportError`` - Bad install, toss an exception
"""
from __future__ import unicode_literals
import warnings
from django.core.exceptions import ImproperlyConfigured
from django.utils.deprecation import RemovedInDjango18Warning
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(
_("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, '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() == '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:
from PIL import _imaging as PIL_imaging
except ImportError:
try:
import _imaging as PIL_imaging
except ImportError as err:
raise ImproperlyConfigured(
_("The '_imaging' module for the PIL could not be "
"imported: %s") % err
)
# Try to import ImageFile as well.
try:
from PIL import ImageFile as PILImageFile
except ImportError:
# This import cannot fail unless Pillow/PIL install is completely
# broken (e.g. missing Python modules).
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.",
RemovedInDjango18Warning
)
return PILImage, PIL_imaging, PILImageFile
Image, _imaging, ImageFile = _detect_image_library()

View File

@ -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: 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 * Are there clear instructions on how to reproduce the bug? If this
touches a dependency (such as Pillow/PIL), a contrib module, or a specific touches a dependency (such as Pillow), a contrib module, or a specific
database, are those instructions clear enough even for someone not database, are those instructions clear enough even for someone not
familiar with it? familiar with it?

View File

@ -619,22 +619,20 @@ For each field, we describe the default widget used if you don't specify
* Normalizes to: An ``UploadedFile`` object that wraps the file content * Normalizes to: An ``UploadedFile`` object that wraps the file content
and file name into a single object. and file name into a single object.
* Validates that file data has been bound to the form, and that the * Validates that file data has been bound to the form, and that the
file is of an image format understood by Pillow/PIL. file is of an image format understood by Pillow.
* Error message keys: ``required``, ``invalid``, ``missing``, ``empty``, * Error message keys: ``required``, ``invalid``, ``missing``, ``empty``,
``invalid_image`` ``invalid_image``
Using an ``ImageField`` requires that either `Pillow`_ (recommended) or the Using an ``ImageField`` requires that `Pillow`_ is installed with support
`Python Imaging Library`_ (PIL) are installed and supports the image for the image formats you use. If you encounter a ``corrupt image`` error
formats you use. If you encounter a ``corrupt image`` error when you when you upload an image, it usually means that Pillow doesn't understand
upload an image, it usually means either Pillow or PIL its format. To fix this, install the appropriate library and reinstall
doesn't understand its format. To fix this, install the appropriate Pillow.
library and reinstall Pillow or PIL.
When you use an ``ImageField`` on a form, you must also remember to When you use an ``ImageField`` on a form, you must also remember to
:ref:`bind the file data to the form <binding-uploaded-files>`. :ref:`bind the file data to the form <binding-uploaded-files>`.
.. _Pillow: http://python-imaging.github.io/Pillow/ .. _Pillow: http://python-imaging.github.io/Pillow/
.. _Python Imaging Library: http://www.pythonware.com/products/pil/
``IntegerField`` ``IntegerField``
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~

View File

@ -841,9 +841,9 @@ optional arguments:
Name of a model field which will be auto-populated with the width of the Name of a model field which will be auto-populated with the width of the
image each time the model instance is saved. image each time the model instance is saved.
Requires the `Python Imaging Library`_. Requires the `Pillow`_ library.
.. _Python Imaging Library: http://www.pythonware.com/products/pil/ .. _Pillow: http://python-imaging.github.io/Pillow/
By default, :class:`ImageField` instances are created as ``varchar(100)`` By default, :class:`ImageField` instances are created as ``varchar(100)``
columns in your database. As with other fields, you can change the maximum columns in your database. As with other fields, you can change the maximum

View File

@ -226,8 +226,8 @@ User-uploaded content
served in ways that do not follow security best practices. Specifically, an served in ways that do not follow security best practices. Specifically, an
HTML file can be uploaded as an image if that file contains a valid PNG HTML file can be uploaded as an image if that file contains a valid PNG
header followed by malicious HTML. This file will pass verification of the header followed by malicious HTML. This file will pass verification of the
libraries that Django uses for :class:`~django.db.models.ImageField` image library that Django uses for :class:`~django.db.models.ImageField` image
processing (PIL or Pillow). When this file is subsequently displayed to a processing (Pillow). When this file is subsequently displayed to a
user, it may be displayed as HTML depending on the type and configuration of user, it may be displayed as HTML depending on the type and configuration of
your web server. your web server.

View File

@ -8,7 +8,6 @@ import tempfile
import unittest import unittest
import zlib import zlib
from django.core.exceptions import ImproperlyConfigured
from django.core.files import File from django.core.files import File
from django.core.files.move import file_move_safe from django.core.files.move import file_move_safe
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -18,10 +17,11 @@ from django.utils._os import upath
from django.utils import six from django.utils import six
try: try:
from django.utils.image import Image from PIL import Image
from django.core.files import images except ImportError:
except ImproperlyConfigured:
Image = None Image = None
else:
from django.core.files import images
class FileTests(unittest.TestCase): class FileTests(unittest.TestCase):
@ -112,7 +112,7 @@ class DimensionClosingBug(unittest.TestCase):
""" """
Test that get_image_dimensions() properly closes files (#8817) Test that get_image_dimensions() properly closes files (#8817)
""" """
@unittest.skipUnless(Image, "Pillow/PIL not installed") @unittest.skipUnless(Image, "Pillow not installed")
def test_not_closing_of_files(self): def test_not_closing_of_files(self):
""" """
Open files passed into get_image_dimensions() should stay opened. Open files passed into get_image_dimensions() should stay opened.
@ -123,7 +123,7 @@ class DimensionClosingBug(unittest.TestCase):
finally: finally:
self.assertTrue(not empty_io.closed) self.assertTrue(not empty_io.closed)
@unittest.skipUnless(Image, "Pillow/PIL not installed") @unittest.skipUnless(Image, "Pillow not installed")
def test_closing_of_filenames(self): def test_closing_of_filenames(self):
""" """
get_image_dimensions() called with a filename should closed the file. get_image_dimensions() called with a filename should closed the file.
@ -163,7 +163,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase):
Test that get_image_dimensions() works properly after various calls Test that get_image_dimensions() works properly after various calls
using a file handler (#11158) using a file handler (#11158)
""" """
@unittest.skipUnless(Image, "Pillow/PIL not installed") @unittest.skipUnless(Image, "Pillow not installed")
def test_multiple_calls(self): def test_multiple_calls(self):
""" """
Multiple calls of get_image_dimensions() should return the same size. Multiple calls of get_image_dimensions() should return the same size.
@ -177,7 +177,7 @@ class InconsistentGetImageDimensionsBug(unittest.TestCase):
self.assertEqual(image_pil.size, size_1) self.assertEqual(image_pil.size, size_1)
self.assertEqual(size_1, size_2) self.assertEqual(size_1, size_2)
@unittest.skipUnless(Image, "Pillow/PIL not installed") @unittest.skipUnless(Image, "Pillow not installed")
def test_bug_19457(self): def test_bug_19457(self):
""" """
Regression test for #19457 Regression test for #19457

View File

@ -4,7 +4,6 @@ from __future__ import unicode_literals
import unittest import unittest
from django.core.checks import Error from django.core.checks import Error
from django.core.exceptions import ImproperlyConfigured
from django.db import connection, models from django.db import connection, models
from .base import IsolatedModelsTestCase from .base import IsolatedModelsTestCase
@ -379,8 +378,8 @@ class ImageFieldTests(IsolatedModelsTestCase):
def test_pillow_installed(self): def test_pillow_installed(self):
try: try:
import django.utils.image # NOQA from PIL import Image # NOQA
except ImproperlyConfigured: except ImportError:
pillow_installed = False pillow_installed = False
else: else:
pillow_installed = True pillow_installed = True

View File

@ -2,11 +2,9 @@ import os
import tempfile import tempfile
import warnings import warnings
from django.core.exceptions import ImproperlyConfigured
try: try:
from django.utils.image import Image from PIL import Image
except ImproperlyConfigured: except ImportError:
Image = None Image = None
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
@ -114,7 +112,7 @@ class VerboseNameField(models.Model):
field9 = models.FileField("verbose field9", upload_to="unused") field9 = models.FileField("verbose field9", upload_to="unused")
field10 = models.FilePathField("verbose field10") field10 = models.FilePathField("verbose field10")
field11 = models.FloatField("verbose field11") field11 = models.FloatField("verbose field11")
# Don't want to depend on Pillow/PIL in this test # Don't want to depend on Pillow in this test
#field_image = models.ImageField("verbose field") #field_image = models.ImageField("verbose field")
field12 = models.IntegerField("verbose field12") field12 = models.IntegerField("verbose field12")
with warnings.catch_warnings(record=True) as w: with warnings.catch_warnings(record=True) as w:
@ -151,7 +149,7 @@ class Document(models.Model):
############################################################################### ###############################################################################
# ImageField # ImageField
# If Pillow/PIL available, do these tests. # If Pillow available, do these tests.
if Image: if Image:
class TestImageFieldFile(ImageFieldFile): class TestImageFieldFile(ImageFieldFile):
""" """

View File

@ -20,7 +20,7 @@ if Image:
PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile) PersonDimensionsFirst, PersonTwoImages, TestImageFieldFile)
from .models import temp_storage_dir from .models import temp_storage_dir
else: else:
# Pillow/PIL not available, create dummy classes (tests will be skipped anyway) # Pillow not available, create dummy classes (tests will be skipped anyway)
class Person(): class Person():
pass pass
PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person PersonWithHeight = PersonWithHeightAndWidth = PersonDimensionsFirst = Person
@ -93,7 +93,7 @@ class ImageFieldTestMixin(object):
self.assertEqual(getattr(instance, height_field_name), height) self.assertEqual(getattr(instance, height_field_name), height)
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class ImageFieldTests(ImageFieldTestMixin, TestCase): class ImageFieldTests(ImageFieldTestMixin, TestCase):
""" """
Tests for ImageField that don't need to be run with each of the Tests for ImageField that don't need to be run with each of the
@ -180,7 +180,7 @@ class ImageFieldTests(ImageFieldTestMixin, TestCase):
self.assertEqual(p.mugshot, loaded_p.mugshot) self.assertEqual(p.mugshot, loaded_p.mugshot)
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase): class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase):
""" """
Tests behavior of an ImageField and its dimensions fields. Tests behavior of an ImageField and its dimensions fields.
@ -294,7 +294,7 @@ class ImageFieldTwoDimensionsTests(ImageFieldTestMixin, TestCase):
self.assertEqual(p.mugshot.was_opened, True) self.assertEqual(p.mugshot.was_opened, True)
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests): class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests):
""" """
Tests behavior of an ImageField with no dimension fields. Tests behavior of an ImageField with no dimension fields.
@ -303,7 +303,7 @@ class ImageFieldNoDimensionsTests(ImageFieldTwoDimensionsTests):
PersonModel = Person PersonModel = Person
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests): class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests):
""" """
Tests behavior of an ImageField with one dimensions field. Tests behavior of an ImageField with one dimensions field.
@ -312,7 +312,7 @@ class ImageFieldOneDimensionTests(ImageFieldTwoDimensionsTests):
PersonModel = PersonWithHeight PersonModel = PersonWithHeight
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests): class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests):
""" """
Tests behavior of an ImageField where the dimensions fields are Tests behavior of an ImageField where the dimensions fields are
@ -322,7 +322,7 @@ class ImageFieldDimensionsFirstTests(ImageFieldTwoDimensionsTests):
PersonModel = PersonDimensionsFirst PersonModel = PersonDimensionsFirst
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests): class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests):
""" """
Tests behavior of an ImageField when assigning it a File instance Tests behavior of an ImageField when assigning it a File instance
@ -333,7 +333,7 @@ class ImageFieldUsingFileTests(ImageFieldTwoDimensionsTests):
File = File File = File
@skipIf(Image is None, "Pillow/PIL is required to test ImageField") @skipIf(Image is None, "Pillow is required to test ImageField")
class TwoImageFieldTests(ImageFieldTestMixin, TestCase): class TwoImageFieldTests(ImageFieldTestMixin, TestCase):
""" """
Tests a model with two ImageFields. Tests a model with two ImageFields.

View File

@ -13,7 +13,7 @@ import os
import tempfile import tempfile
from django.core import validators from django.core import validators
from django.core.exceptions import ImproperlyConfigured, ValidationError from django.core.exceptions import ValidationError
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.db import models from django.db import models
from django.utils import six from django.utils import six
@ -154,7 +154,7 @@ class FilePathModel(models.Model):
try: try:
from django.utils.image import Image # NOQA: detect if Pillow is installed from PIL import Image # NOQA: detect if Pillow is installed
test_images = True test_images = True
@ -193,7 +193,7 @@ try:
def __str__(self): def __str__(self):
return self.description return self.description
except ImproperlyConfigured: except ImportError:
test_images = False test_images = False

View File

@ -1852,7 +1852,7 @@ class FileAndImageFieldTests(TestCase):
names.sort() names.sort()
self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py']) self.assertEqual(names, ['---------', '__init__.py', 'models.py', 'tests.py'])
@skipUnless(test_images, "Pillow/PIL not installed") @skipUnless(test_images, "Pillow not installed")
def test_image_field(self): def test_image_field(self):
# ImageField and FileField are nearly identical, but they differ slighty when # ImageField and FileField are nearly identical, but they differ slighty when
# it comes to validation. This specifically tests that #6302 is fixed for # it comes to validation. This specifically tests that #6302 is fixed for

View File

@ -2,7 +2,7 @@
A test spanning all the capabilities of all the serializers. A test spanning all the capabilities of all the serializers.
This class sets up a model for each model field type This class sets up a model for each model field type
(except for image types, because of the Pillow/PIL dependency). (except for image types, because of the Pillow dependency).
""" """
import warnings import warnings