Merge pull request #5768 from hugovk/rm-3.6

Drop support for soon-EOL Python 3.6
This commit is contained in:
Andrew Murray 2021-11-05 19:08:02 +11:00 committed by GitHub
commit a70e3c828f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 90 additions and 112 deletions

View File

@ -13,7 +13,7 @@ environment:
- PYTHON: C:/Python310 - PYTHON: C:/Python310
ARCHITECTURE: x86 ARCHITECTURE: x86
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019
- PYTHON: C:/Python36-x64 - PYTHON: C:/Python37-x64
ARCHITECTURE: x64 ARCHITECTURE: x64
APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017

View File

@ -3,7 +3,7 @@ repos:
rev: 911470a610e47d9da5ea938b0887c3df62819b85 # frozen: 21.9b0 rev: 911470a610e47d9da5ea938b0887c3df62819b85 # frozen: 21.9b0
hooks: hooks:
- id: black - id: black
args: ["--target-version", "py36"] args: ["--target-version", "py37"]
# Only .py files, until https://github.com/psf/black/issues/402 resolved # Only .py files, until https://github.com/psf/black/issues/402 resolved
files: \.py$ files: \.py$
types: [] types: []

View File

@ -122,5 +122,5 @@ lint:
.PHONY: lint-fix .PHONY: lint-fix
lint-fix: lint-fix:
black --target-version py36 . black --target-version py37 .
isort . isort .

View File

@ -818,7 +818,7 @@ def test_palette_save_P(tmp_path):
# Forcing a non-straight grayscale palette. # Forcing a non-straight grayscale palette.
im = hopper("P") im = hopper("P")
palette = bytes([255 - i // 3 for i in range(768)]) palette = bytes(255 - i // 3 for i in range(768))
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")
im.save(out, palette=palette) im.save(out, palette=palette)
@ -885,7 +885,7 @@ def test_getdata():
im.putpalette(ImagePalette.ImagePalette("RGB")) im.putpalette(ImagePalette.ImagePalette("RGB"))
im.info = {"background": 0} im.info = {"background": 0}
passed_palette = bytes([255 - i // 3 for i in range(768)]) passed_palette = bytes(255 - i // 3 for i in range(768))
GifImagePlugin._FORCE_OPTIMIZE = True GifImagePlugin._FORCE_OPTIMIZE = True
try: try:

View File

@ -85,26 +85,26 @@ class TestFileJpeg:
f = "Tests/images/pil_sample_cmyk.jpg" f = "Tests/images/pil_sample_cmyk.jpg"
with Image.open(f) as im: with Image.open(f) as im:
# the source image has red pixels in the upper left corner. # the source image has red pixels in the upper left corner.
c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
assert c == 0.0 assert c == 0.0
assert m > 0.8 assert m > 0.8
assert y > 0.8 assert y > 0.8
assert k == 0.0 assert k == 0.0
# the opposite corner is black # the opposite corner is black
c, m, y, k = [ c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
] )
assert k > 0.9 assert k > 0.9
# roundtrip, and check again # roundtrip, and check again
im = self.roundtrip(im) im = self.roundtrip(im)
c, m, y, k = [x / 255.0 for x in im.getpixel((0, 0))] c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0)))
assert c == 0.0 assert c == 0.0
assert m > 0.8 assert m > 0.8
assert y > 0.8 assert y > 0.8
assert k == 0.0 assert k == 0.0
c, m, y, k = [ c, m, y, k = (
x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1))
] )
assert k > 0.9 assert k > 0.9
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -188,9 +188,7 @@ class TestFileWebp:
with Image.open(out_gif) as reread: with Image.open(out_gif) as reread:
reread_value = reread.convert("RGB").getpixel((1, 1)) reread_value = reread.convert("RGB").getpixel((1, 1))
difference = sum( difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3))
[abs(original_value[i] - reread_value[i]) for i in range(0, 3)]
)
assert difference < 5 assert difference < 5
@skip_unless_feature("webp") @skip_unless_feature("webp")

View File

@ -1,7 +1,6 @@
import io import io
import os import os
import shutil import shutil
import sys
import tempfile import tempfile
import pytest import pytest
@ -782,9 +781,6 @@ class TestImage:
34665: 196, 34665: 196,
} }
@pytest.mark.skipif(
sys.version_info < (3, 7), reason="Python 3.7 or greater required"
)
def test_categories_deprecation(self): def test_categories_deprecation(self):
with pytest.warns(DeprecationWarning): with pytest.warns(DeprecationWarning):
assert hopper().category == 0 assert hopper().category == 0

View File

@ -900,7 +900,7 @@ class TestImageFont:
d.text((10, 10), "\U0001f469", font=font, embedded_color=True) d.text((10, 10), "\U0001f469", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2) assert_image_similar_tofile(im, "Tests/images/cbdt_notocoloremoji.png", 6.2)
except IOError as e: # pragma: no cover except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format") assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support") pytest.skip("freetype compiled without libpng or CBDT support")
@ -920,7 +920,7 @@ class TestImageFont:
assert_image_similar_tofile( assert_image_similar_tofile(
im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2 im, "Tests/images/cbdt_notocoloremoji_mask.png", 6.2
) )
except IOError as e: # pragma: no cover except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format") assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or CBDT support") pytest.skip("freetype compiled without libpng or CBDT support")
@ -938,7 +938,7 @@ class TestImageFont:
d.text((50, 50), "\uE901", font=font, embedded_color=True) d.text((50, 50), "\uE901", font=font, embedded_color=True)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix.png", 1)
except IOError as e: # pragma: no cover except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format") assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or SBIX support") pytest.skip("freetype compiled without libpng or SBIX support")
@ -956,7 +956,7 @@ class TestImageFont:
d.text((50, 50), "\uE901", (100, 0, 0), font=font) d.text((50, 50), "\uE901", (100, 0, 0), font=font)
assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1) assert_image_similar_tofile(im, "Tests/images/chromacheck-sbix_mask.png", 1)
except IOError as e: # pragma: no cover except OSError as e: # pragma: no cover
assert str(e) in ("unimplemented feature", "unknown file format") assert str(e) in ("unimplemented feature", "unknown file format")
pytest.skip("freetype compiled without libpng or SBIX support") pytest.skip("freetype compiled without libpng or SBIX support")

View File

@ -3,7 +3,7 @@ from PIL import Image, ImageMath
def pixel(im): def pixel(im):
if hasattr(im, "im"): if hasattr(im, "im"):
return "{} {}".format(im.mode, repr(im.getpixel((0, 0)))) return f"{im.mode} {repr(im.getpixel((0, 0)))}"
else: else:
if isinstance(im, int): if isinstance(im, int):
return int(im) # hack to deal with booleans return int(im) # hack to deal with booleans

View File

@ -18,7 +18,9 @@ Pillow supports these Python versions.
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 | | Python |3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 3.5 | 3.4 | 2.7 |
+======================+=====+=====+=====+=====+=====+=====+=====+=====+ +======================+=====+=====+=====+=====+=====+=====+=====+=====+
| Pillow >= 8.3.2 | Yes | Yes | Yes | Yes | Yes | | | | | Pillow >= 9.0 | Yes | Yes | Yes | Yes | | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 8.3.2 - 8.4 | Yes | Yes | Yes | Yes | Yes | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
| Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | | | Pillow 8.0 - 8.3.1 | | Yes | Yes | Yes | Yes | | | |
+----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+
@ -443,42 +445,42 @@ Continuous Integration Targets
These platforms are built and tested for every change. These platforms are built and tested for every change.
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Operating system | Tested Python versions | Tested architecture | | Operating system | Tested Python versions | Tested architecture |
+==================================+=================================+=====================+ +==================================+============================+=====================+
| Alpine | 3.9 | x86-64 | | Alpine | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Amazon Linux 2 | 3.7 | x86-64 | | Amazon Linux 2 | 3.7 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Arch | 3.9 | x86-64 | | Arch | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS 7 | 3.6 | x86-64 | | CentOS 7 | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS 8 | 3.6 | x86-64 | | CentOS 8 | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| CentOS Stream 8 | 3.6 | x86-64 | | CentOS Stream 8 | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Debian 10 Buster | 3.7 | x86 | | Debian 10 Buster | 3.7 | x86 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 34 | 3.9 | x86-64 | | Fedora 34 | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Fedora 35 | 3.10 | x86-64 | | Fedora 35 | 3.10 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| macOS 10.15 Catalina | 3.6, 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | | macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 18.04 LTS (Bionic) | 3.6 | x86-64 | | Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 20.04 LTS (Focal) | 3.6, 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | | Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 |
| +---------------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.8 | arm64v8, ppc64le, | | | 3.8 | arm64v8, ppc64le, |
| | | s390x | | | | s390x |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2016 | 3.6 | x86-64 | | Windows Server 2016 | 3.7 | x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 | | Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 |
| +---------------------------------+---------------------+ | +----------------------------+---------------------+
| | 3.9/MinGW | x86, x86-64 | | | 3.9/MinGW | x86, x86-64 |
+----------------------------------+---------------------------------+---------------------+ +----------------------------------+----------------------------+---------------------+
Other Platforms Other Platforms

View File

@ -4,6 +4,11 @@
Backwards Incompatible Changes Backwards Incompatible Changes
============================== ==============================
Python 3.6
^^^^^^^^^^
Pillow has dropped support for Python 3.6, which reached end-of-life on 2021-12-23.
PILLOW_VERSION constant PILLOW_VERSION constant
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -12,7 +12,6 @@ classifiers =
License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND) License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
@ -34,7 +33,7 @@ project_urls =
Twitter=https://twitter.com/PythonPillow Twitter=https://twitter.com/PythonPillow
[options] [options]
python_requires = >=3.6 python_requires = >=3.7
[flake8] [flake8]
extend-ignore = E203 extend-ignore = E203

View File

@ -324,7 +324,7 @@ class GifImageFile(ImageFile.ImageFile):
if not self.im and "transparency" in self.info: if not self.im and "transparency" in self.info:
self.im = Image.core.fill(self.mode, self.size, self.info["transparency"]) self.im = Image.core.fill(self.mode, self.size, self.info["transparency"])
super(GifImageFile, self).load_prepare() super().load_prepare()
def tell(self): def tell(self):
return self.__frame return self.__frame

View File

@ -51,9 +51,8 @@ from . import ImageMode, TiffTags, UnidentifiedImageError, __version__, _plugins
from ._binary import i32le from ._binary import i32le
from ._util import deferred_error, isPath from ._util import deferred_error, isPath
if sys.version_info >= (3, 7):
def __getattr__(name): def __getattr__(name):
categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2}
if name in categories: if name in categories:
warnings.warn( warnings.warn(
@ -66,13 +65,6 @@ if sys.version_info >= (3, 7):
raise AttributeError(f"module '{__name__}' has no attribute '{name}'") raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
else:
# categories
NORMAL = 0
SEQUENCE = 1
CONTAINER = 2
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -926,12 +918,8 @@ class Image:
transparency = convert_transparency(matrix, transparency) transparency = convert_transparency(matrix, transparency)
elif len(mode) == 3: elif len(mode) == 3:
transparency = tuple( transparency = tuple(
[ convert_transparency(matrix[i * 4 : i * 4 + 4], transparency)
convert_transparency(
matrix[i * 4 : i * 4 + 4], transparency
)
for i in range(0, len(transparency)) for i in range(0, len(transparency))
]
) )
new.info["transparency"] = transparency new.info["transparency"] = transparency
return new return new
@ -1934,7 +1922,7 @@ class Image:
message = f"Unknown resampling filter ({resample})." message = f"Unknown resampling filter ({resample})."
filters = [ filters = [
"{} ({})".format(filter[1], filter[0]) f"{filter[1]} ({filter[0]})"
for filter in ( for filter in (
(NEAREST, "Image.NEAREST"), (NEAREST, "Image.NEAREST"),
(LANCZOS, "Image.LANCZOS"), (LANCZOS, "Image.LANCZOS"),
@ -2529,7 +2517,7 @@ class Image:
message = f"Unknown resampling filter ({resample})." message = f"Unknown resampling filter ({resample})."
filters = [ filters = [
"{} ({})".format(filter[1], filter[0]) f"{filter[1]} ({filter[0]})"
for filter in ( for filter in (
(NEAREST, "Image.NEAREST"), (NEAREST, "Image.NEAREST"),
(BILINEAR, "Image.BILINEAR"), (BILINEAR, "Image.BILINEAR"),

View File

@ -174,13 +174,11 @@ class ImageDraw:
angle -= 90 angle -= 90
distance = width / 2 - 1 distance = width / 2 - 1
return tuple( return tuple(
[
p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d))
for p, p_d in ( for p, p_d in (
(x, distance * math.cos(math.radians(angle))), (x, distance * math.cos(math.radians(angle))),
(y, distance * math.sin(math.radians(angle))), (y, distance * math.sin(math.radians(angle))),
) )
]
) )
flipped = ( flipped = (
@ -979,6 +977,6 @@ def _color_diff(color1, color2):
Uses 1-norm distance to calculate difference between two values. Uses 1-norm distance to calculate difference between two values.
""" """
if isinstance(color2, tuple): if isinstance(color2, tuple):
return sum([abs(color1[i] - color2[i]) for i in range(0, len(color2))]) return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2)))
else: else:
return abs(color1 - color2) return abs(color1 - color2)

View File

@ -425,7 +425,7 @@ class PdfParser:
self.f.write(b"%PDF-1.4\n") self.f.write(b"%PDF-1.4\n")
def write_comment(self, s): def write_comment(self, s):
self.f.write(f"% {s}\n".encode("utf-8")) self.f.write(f"% {s}\n".encode())
def write_catalog(self): def write_catalog(self):
self.del_root() self.del_root()
@ -862,7 +862,7 @@ class PdfParser:
if m: if m:
# filter out whitespace # filter out whitespace
hex_string = bytearray( hex_string = bytearray(
[b for b in m.group(1) if b in b"0123456789abcdefABCDEF"] b for b in m.group(1) if b in b"0123456789abcdefABCDEF"
) )
if len(hex_string) % 2 == 1: if len(hex_string) % 2 == 1:
# append a 0 if the length is not even - yes, at the end # append a 0 if the length is not even - yes, at the end

View File

@ -674,7 +674,7 @@ class ImageFileDirectory_v2(MutableMapping):
_load_dispatch[idx] = ( # noqa: F821 _load_dispatch[idx] = ( # noqa: F821
size, size,
lambda self, data, legacy_api=True: ( lambda self, data, legacy_api=True: (
self._unpack("{}{}".format(len(data) // size, fmt), data) self._unpack(f"{len(data) // size}{fmt}", data)
), ),
) )
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821
@ -718,7 +718,7 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_loader(5, 8) @_register_loader(5, 8)
def load_rational(self, data, legacy_api=True): def load_rational(self, data, legacy_api=True):
vals = self._unpack("{}L".format(len(data) // 4), data) vals = self._unpack(f"{len(data) // 4}L", data)
def combine(a, b): def combine(a, b):
return (a, b) if legacy_api else IFDRational(a, b) return (a, b) if legacy_api else IFDRational(a, b)
@ -741,7 +741,7 @@ class ImageFileDirectory_v2(MutableMapping):
@_register_loader(10, 8) @_register_loader(10, 8)
def load_signed_rational(self, data, legacy_api=True): def load_signed_rational(self, data, legacy_api=True):
vals = self._unpack("{}l".format(len(data) // 4), data) vals = self._unpack(f"{len(data) // 4}l", data)
def combine(a, b): def combine(a, b):
return (a, b) if legacy_api else IFDRational(a, b) return (a, b) if legacy_api else IFDRational(a, b)

View File

@ -9,12 +9,6 @@
#include "Python.h" #include "Python.h"
/* Workaround issue #2479 */
#if PY_VERSION_HEX < 0x03070000 && defined(PySlice_GetIndicesEx) && \
!defined(PYPY_VERSION)
#undef PySlice_GetIndicesEx
#endif
/* Check that we have an ANSI compliant compiler */ /* Check that we have an ANSI compliant compiler */
#ifndef HAVE_PROTOTYPES #ifndef HAVE_PROTOTYPES
#error Sorry, this library requires support for ANSI prototypes. #error Sorry, this library requires support for ANSI prototypes.

View File

@ -6,7 +6,7 @@
[tox] [tox]
envlist = envlist =
lint lint
py{36,37,38,39,310,py3} py{37,38,39,310,py3}
minversion = 1.9 minversion = 1.9
[testenv] [testenv]

View File

@ -474,8 +474,6 @@ def build_pillow():
cmd_cd("{pillow_dir}"), cmd_cd("{pillow_dir}"),
*prefs["header"], *prefs["header"],
cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow cmd_set("DISTUTILS_USE_SDK", "1"), # use same compiler to build Pillow
cmd_set("MSSdk", "1"), # for PyPy3.6
cmd_set("py_vcruntime_redist", "true"), # use /MD, not /MT
r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501 r'"{python_dir}\{python_exe}" setup.py build_ext --vendor-raqm --vendor-fribidi %*', # noqa: E501
] ]