diff --git a/.appveyor.yml b/.appveyor.yml index 72f79248f1d..6116c3b442f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -13,7 +13,7 @@ environment: - PYTHON: C:/Python310 ARCHITECTURE: x86 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2019 - - PYTHON: C:/Python36-x64 + - PYTHON: C:/Python37-x64 ARCHITECTURE: x64 APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4a0256085b0..5f1d16709c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: 911470a610e47d9da5ea938b0887c3df62819b85 # frozen: 21.9b0 hooks: - id: black - args: ["--target-version", "py36"] + args: ["--target-version", "py37"] # Only .py files, until https://github.com/psf/black/issues/402 resolved files: \.py$ types: [] diff --git a/Makefile b/Makefile index a15d6bb9732..546b9183844 100644 --- a/Makefile +++ b/Makefile @@ -122,5 +122,5 @@ lint: .PHONY: lint-fix lint-fix: - black --target-version py36 . + black --target-version py37 . isort . diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 985f8e52c63..fae7e912c8c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -818,7 +818,7 @@ def test_palette_save_P(tmp_path): # Forcing a non-straight grayscale palette. 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") im.save(out, palette=palette) @@ -885,7 +885,7 @@ def test_getdata(): im.putpalette(ImagePalette.ImagePalette("RGB")) 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 try: diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 13d99c15dc5..f2002ecb891 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -85,26 +85,26 @@ def test_cmyk(self): f = "Tests/images/pil_sample_cmyk.jpg" with Image.open(f) as im: # 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 m > 0.8 assert y > 0.8 assert k == 0.0 # 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)) - ] + ) assert k > 0.9 # roundtrip, and check again 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 m > 0.8 assert y > 0.8 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)) - ] + ) assert k > 0.9 @pytest.mark.parametrize( diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 420594b0cb2..e72b4993c63 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -188,9 +188,7 @@ def test_background_from_gif(self, tmp_path): with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum( - [abs(original_value[i] - reread_value[i]) for i in range(0, 3)] - ) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 @skip_unless_feature("webp") diff --git a/Tests/test_image.py b/Tests/test_image.py index c185a1cb454..cf60f42af6e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1,7 +1,6 @@ import io import os import shutil -import sys import tempfile import pytest @@ -782,9 +781,6 @@ def test_exif_load_from_fp(self): 34665: 196, } - @pytest.mark.skipif( - sys.version_info < (3, 7), reason="Python 3.7 or greater required" - ) def test_categories_deprecation(self): with pytest.warns(DeprecationWarning): assert hopper().category == 0 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 3ed3a2f025d..0d423aab7be 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -900,7 +900,7 @@ def test_cbdt(self): d.text((10, 10), "\U0001f469", font=font, embedded_color=True) 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") pytest.skip("freetype compiled without libpng or CBDT support") @@ -920,7 +920,7 @@ def test_cbdt_mask(self): assert_image_similar_tofile( 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") pytest.skip("freetype compiled without libpng or CBDT support") @@ -938,7 +938,7 @@ def test_sbix(self): d.text((50, 50), "\uE901", font=font, embedded_color=True) 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") pytest.skip("freetype compiled without libpng or SBIX support") @@ -956,7 +956,7 @@ def test_sbix_mask(self): d.text((50, 50), "\uE901", (100, 0, 0), font=font) 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") pytest.skip("freetype compiled without libpng or SBIX support") diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index 2398067964c..e7afd1abf47 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -3,7 +3,7 @@ def pixel(im): if hasattr(im, "im"): - return "{} {}".format(im.mode, repr(im.getpixel((0, 0)))) + return f"{im.mode} {repr(im.getpixel((0, 0)))}" else: if isinstance(im, int): return int(im) # hack to deal with booleans diff --git a/docs/installation.rst b/docs/installation.rst index 218a123bc60..d271d827aa7 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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 | +======================+=====+=====+=====+=====+=====+=====+=====+=====+ -| 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 | | | | +----------------------+-----+-----+-----+-----+-----+-----+-----+-----+ @@ -443,42 +445,42 @@ Continuous Integration Targets These platforms are built and tested for every change. -+----------------------------------+---------------------------------+---------------------+ -| Operating system | Tested Python versions | Tested architecture | -+==================================+=================================+=====================+ -| Alpine | 3.9 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| Amazon Linux 2 | 3.7 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| Arch | 3.9 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| CentOS 7 | 3.6 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| CentOS 8 | 3.6 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| CentOS Stream 8 | 3.6 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| Debian 10 Buster | 3.7 | x86 | -+----------------------------------+---------------------------------+---------------------+ -| Fedora 34 | 3.9 | 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 | -+----------------------------------+---------------------------------+---------------------+ -| Ubuntu Linux 18.04 LTS (Bionic) | 3.6 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| Ubuntu Linux 20.04 LTS (Focal) | 3.6, 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | -| +---------------------------------+---------------------+ -| | 3.8 | arm64v8, ppc64le, | -| | | s390x | -+----------------------------------+---------------------------------+---------------------+ -| Windows Server 2016 | 3.6 | x86-64 | -+----------------------------------+---------------------------------+---------------------+ -| Windows Server 2019 | 3.6, 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 | -| +---------------------------------+---------------------+ -| | 3.9/MinGW | x86, x86-64 | -+----------------------------------+---------------------------------+---------------------+ ++----------------------------------+----------------------------+---------------------+ +| Operating system | Tested Python versions | Tested architecture | ++==================================+============================+=====================+ +| Alpine | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Amazon Linux 2 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Arch | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 7 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| CentOS Stream 8 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Debian 10 Buster | 3.7 | x86 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 34 | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Fedora 35 | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 10.15 Catalina | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 18.04 LTS (Bionic) | 3.9 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Ubuntu Linux 20.04 LTS (Focal) | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86-64 | +| +----------------------------+---------------------+ +| | 3.8 | arm64v8, ppc64le, | +| | | s390x | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2016 | 3.7 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| Windows Server 2019 | 3.7, 3.8, 3.9, 3.10, PyPy3 | x86, x86-64 | +| +----------------------------+---------------------+ +| | 3.9/MinGW | x86, x86-64 | ++----------------------------------+----------------------------+---------------------+ Other Platforms diff --git a/docs/releasenotes/9.0.0.rst b/docs/releasenotes/9.0.0.rst index 1a46cf086fc..db3c16ac66d 100644 --- a/docs/releasenotes/9.0.0.rst +++ b/docs/releasenotes/9.0.0.rst @@ -4,6 +4,11 @@ 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 ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/setup.cfg b/setup.cfg index 7e43a33c926..c3b5a319750 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,7 +12,6 @@ classifiers = License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND) Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 @@ -34,7 +33,7 @@ project_urls = Twitter=https://twitter.com/PythonPillow [options] -python_requires = >=3.6 +python_requires = >=3.7 [flake8] extend-ignore = E203 diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 128afc42882..5b466884470 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -324,7 +324,7 @@ def load_prepare(self): if not self.im and "transparency" in self.info: self.im = Image.core.fill(self.mode, self.size, self.info["transparency"]) - super(GifImageFile, self).load_prepare() + super().load_prepare() def tell(self): return self.__frame diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6ecd5fac887..67365d79eb2 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -51,26 +51,18 @@ from ._binary import i32le from ._util import deferred_error, isPath -if sys.version_info >= (3, 7): - def __getattr__(name): - categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} - if name in categories: - warnings.warn( - "Image categories are deprecated and will be removed in Pillow 10 " - "(2023-07-01). Use is_animated instead.", - DeprecationWarning, - stacklevel=2, - ) - return categories[name] - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -else: - # categories - NORMAL = 0 - SEQUENCE = 1 - CONTAINER = 2 +def __getattr__(name): + categories = {"NORMAL": 0, "SEQUENCE": 1, "CONTAINER": 2} + if name in categories: + warnings.warn( + "Image categories are deprecated and will be removed in Pillow 10 " + "(2023-07-01). Use is_animated instead.", + DeprecationWarning, + stacklevel=2, + ) + return categories[name] + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") logger = logging.getLogger(__name__) @@ -926,12 +918,8 @@ def convert_transparency(m, v): transparency = convert_transparency(matrix, transparency) elif len(mode) == 3: transparency = tuple( - [ - convert_transparency( - matrix[i * 4 : i * 4 + 4], transparency - ) - for i in range(0, len(transparency)) - ] + convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) + for i in range(0, len(transparency)) ) new.info["transparency"] = transparency return new @@ -1934,7 +1922,7 @@ def resize(self, size, resample=None, box=None, reducing_gap=None): message = f"Unknown resampling filter ({resample})." filters = [ - "{} ({})".format(filter[1], filter[0]) + f"{filter[1]} ({filter[0]})" for filter in ( (NEAREST, "Image.NEAREST"), (LANCZOS, "Image.LANCZOS"), @@ -2529,7 +2517,7 @@ def __transformer(self, box, image, method, data, resample=NEAREST, fill=1): message = f"Unknown resampling filter ({resample})." filters = [ - "{} ({})".format(filter[1], filter[0]) + f"{filter[1]} ({filter[0]})" for filter in ( (NEAREST, "Image.NEAREST"), (BILINEAR, "Image.BILINEAR"), diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index aea0cc680a8..eeae1782a72 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -174,13 +174,11 @@ def coord_at_angle(coord, angle): angle -= 90 distance = width / 2 - 1 return tuple( - [ - p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) - for p, p_d in ( - (x, distance * math.cos(math.radians(angle))), - (y, distance * math.sin(math.radians(angle))), - ) - ] + p + (math.floor(p_d) if p_d > 0 else math.ceil(p_d)) + for p, p_d in ( + (x, distance * math.cos(math.radians(angle))), + (y, distance * math.sin(math.radians(angle))), + ) ) flipped = ( @@ -979,6 +977,6 @@ def _color_diff(color1, color2): Uses 1-norm distance to calculate difference between two values. """ 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: return abs(color1 - color2) diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index b5279e0d73b..b95abbe2f14 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -425,7 +425,7 @@ def write_header(self): self.f.write(b"%PDF-1.4\n") 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): self.del_root() @@ -862,7 +862,7 @@ def get_value(cls, data, offset, expect_indirect=None, max_nesting=-1): if m: # filter out whitespace 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: # append a 0 if the length is not even - yes, at the end diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 6b311bbf5b9..29de8a06b26 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -674,7 +674,7 @@ def _register_basic(idx_fmt_name): _load_dispatch[idx] = ( # noqa: F821 size, 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 @@ -718,7 +718,7 @@ def write_string(self, value): @_register_loader(5, 8) 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): return (a, b) if legacy_api else IFDRational(a, b) @@ -741,7 +741,7 @@ def write_undefined(self, value): @_register_loader(10, 8) 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): return (a, b) if legacy_api else IFDRational(a, b) diff --git a/src/libImaging/ImPlatform.h b/src/libImaging/ImPlatform.h index 9a2060edfd9..26b49d6d089 100644 --- a/src/libImaging/ImPlatform.h +++ b/src/libImaging/ImPlatform.h @@ -9,12 +9,6 @@ #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 */ #ifndef HAVE_PROTOTYPES #error Sorry, this library requires support for ANSI prototypes. diff --git a/tox.ini b/tox.ini index cdc2ab88177..96d84f93391 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ [tox] envlist = lint - py{36,37,38,39,310,py3} + py{37,38,39,310,py3} minversion = 1.9 [testenv] diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 807cbbf275a..7568d23596e 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -474,8 +474,6 @@ def build_pillow(): cmd_cd("{pillow_dir}"), *prefs["header"], 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 ]