From dc7d646db03bb34abd493a79ec2ceb78ec778265 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 1 Jul 2025 22:52:23 +1000 Subject: [PATCH 01/14] Use correct bands for 2 band histograms --- Tests/test_image_histogram.py | 3 +++ src/libImaging/Histo.c | 14 +++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Tests/test_image_histogram.py b/Tests/test_image_histogram.py index dbd55d4c2..436eb78a2 100644 --- a/Tests/test_image_histogram.py +++ b/Tests/test_image_histogram.py @@ -10,9 +10,12 @@ def test_histogram() -> None: assert histogram("1") == (256, 0, 10994) assert histogram("L") == (256, 0, 662) + assert histogram("LA") == (512, 0, 16384) + assert histogram("La") == (512, 0, 16384) assert histogram("I") == (256, 0, 662) assert histogram("F") == (256, 0, 662) assert histogram("P") == (256, 0, 1551) + assert histogram("PA") == (512, 0, 16384) assert histogram("RGB") == (768, 4, 675) assert histogram("RGBA") == (1024, 0, 16384) assert histogram("CMYK") == (1024, 0, 16384) diff --git a/src/libImaging/Histo.c b/src/libImaging/Histo.c index c5a547a64..87c09d3d4 100644 --- a/src/libImaging/Histo.c +++ b/src/libImaging/Histo.c @@ -132,11 +132,15 @@ ImagingGetHistogram(Imaging im, Imaging imMask, void *minmax) { ImagingSectionEnter(&cookie); for (y = 0; y < im->ysize; y++) { UINT8 *in = (UINT8 *)im->image[y]; - for (x = 0; x < im->xsize; x++) { - h->histogram[(*in++)]++; - h->histogram[(*in++) + 256]++; - h->histogram[(*in++) + 512]++; - h->histogram[(*in++) + 768]++; + for (x = 0; x < im->xsize; x++, in += 4) { + h->histogram[*in]++; + if (im->bands == 2) { + h->histogram[*(in + 3) + 256]++; + } else { + h->histogram[*(in + 1) + 256]++; + h->histogram[*(in + 2) + 512]++; + h->histogram[*(in + 3) + 768]++; + } } } ImagingSectionLeave(&cookie); From 31e6c716ac0141ca03aed750b8b326183a45b0fb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 9 Jul 2025 22:26:25 +1000 Subject: [PATCH 02/14] Improved features test coverage --- Tests/test_features.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b46..ddca99344 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -112,6 +112,25 @@ def test_unsupported_module() -> None: features.version_module(module) +def test_unsupported_feature() -> None: + # Arrange + feature = "unsupported_feature" + # Act / Assert + with pytest.raises(ValueError): + features.check_feature(feature) + with pytest.raises(ValueError): + features.version_feature(feature) + + +def test_unsupported_version() -> None: + assert features.version("unsupported_version") is None + + +def test_modulenotfound(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(features, "features", {"test": ("PIL._test", "", "")}) + assert features.check_feature("test") is None + + @pytest.mark.parametrize("supported_formats", (True, False)) def test_pilinfo(supported_formats: bool) -> None: buf = io.StringIO() From 91bbeb5dcb47ce6d3b5b1c9969c982910ebee56b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:54:13 +1000 Subject: [PATCH 03/14] Revert iOS change until the test runs again --- Tests/test_pyroma.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index a161d3f05..c2f7fe22e 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,7 +1,5 @@ from __future__ import annotations -from importlib.metadata import metadata - import pytest from PIL import __version__ @@ -11,7 +9,7 @@ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.map_metadata_keys(metadata("Pillow")) + data = pyroma.projectdata.get_data(".") # Act rating = pyroma.ratings.rate(data) From a426eb55afcb9e8a069d6aba21405c0d89f69bec Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:40:22 +1000 Subject: [PATCH 04/14] Remove file after test completion --- Tests/test_image_access.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index b3de5c13d..2609b1e34 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -276,10 +276,11 @@ class TestEmbeddable: except Exception: pytest.skip("Compiler could not be initialized") - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + try: + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" #include "Python.h" int main(int argc, char* argv[]) @@ -301,17 +302,19 @@ int main(int argc, char* argv[]) return 0; }} """ - ) + ) - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 + finally: + os.remove("embed_pil.c") From a39d14648bdbd6638e7097167c4b8c2964ce3752 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 16 Jul 2025 13:39:19 +1000 Subject: [PATCH 05/14] Updated manifest --- MANIFEST.in | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 95a6b1b92..6623f227d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ include LICENSE include Makefile include tox.ini graft Tests +graft Tests/images graft checks graft patches graft src @@ -28,8 +29,19 @@ exclude .editorconfig exclude .readthedocs.yml exclude codecov.yml exclude renovate.json +exclude Tests/images/README.md +exclude Tests/images/crash*.tif +exclude Tests/images/string_dimension.tiff global-exclude .git* global-exclude *.pyc global-exclude *.so prune .ci prune wheels +prune winbuild/build +prune winbuild/depends +prune Tests/errors +prune Tests/images/jpeg2000 +prune Tests/images/msp +prune Tests/images/picins +prune Tests/images/sunraster +prune Tests/test-images From f4d86e4f44dfe799b0a2d6484fa53945ab89220e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 24 Jul 2025 07:27:39 +1000 Subject: [PATCH 06/14] Use teardown_method --- Tests/test_image_access.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 2609b1e34..a847264d2 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -276,11 +276,10 @@ class TestEmbeddable: except Exception: pytest.skip("Compiler could not be initialized") - try: - with open("embed_pil.c", "w", encoding="utf-8") as fh: - home = sys.prefix.replace("\\", "\\\\") - fh.write( - f""" + with open("embed_pil.c", "w", encoding="utf-8") as fh: + home = sys.prefix.replace("\\", "\\\\") + fh.write( + f""" #include "Python.h" int main(int argc, char* argv[]) @@ -302,19 +301,20 @@ int main(int argc, char* argv[]) return 0; }} """ - ) + ) - objects = compiler.compile(["embed_pil.c"]) - compiler.link_executable(objects, "embed_pil") + objects = compiler.compile(["embed_pil.c"]) + compiler.link_executable(objects, "embed_pil") - env = os.environ.copy() - env["PATH"] = sys.prefix + ";" + env["PATH"] + env = os.environ.copy() + env["PATH"] = sys.prefix + ";" + env["PATH"] - # Do not display the Windows Error Reporting dialog - getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) + # Do not display the Windows Error Reporting dialog + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) - process = subprocess.Popen(["embed_pil.exe"], env=env) - process.communicate() - assert process.returncode == 0 - finally: - os.remove("embed_pil.c") + process = subprocess.Popen(["embed_pil.exe"], env=env) + process.communicate() + assert process.returncode == 0 + + def teardown_method(self) -> None: + os.remove("embed_pil.c") From 63163d065d632cb75466d554fb1d6ea27cc43577 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 17 Jul 2025 19:59:47 +1000 Subject: [PATCH 07/14] Removed WebP feature handling --- Tests/test_features.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b46..7af3fffea 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -18,11 +18,7 @@ def test_check() -> None: for codec in features.codecs: assert features.check_codec(codec) == features.check(codec) for feature in features.features: - if "webp" in feature: - with pytest.warns(DeprecationWarning, match="webp"): - assert features.check_feature(feature) == features.check(feature) - else: - assert features.check_feature(feature) == features.check(feature) + assert features.check_feature(feature) == features.check(feature) def test_version() -> None: @@ -48,11 +44,7 @@ def test_version() -> None: for codec in features.codecs: test(codec, features.version_codec) for feature in features.features: - if "webp" in feature: - with pytest.warns(DeprecationWarning, match="webp"): - test(feature, features.version_feature) - else: - test(feature, features.version_feature) + test(feature, features.version_feature) @skip_unless_feature("libjpeg_turbo") From ec6d5efe4d02dc6d68e569abfd7523e21a89539f Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sat, 26 Jul 2025 08:33:11 +0100 Subject: [PATCH 08/14] Deprecate ImageCmsProfile product_name and product_info (#8995) Co-authored-by: Andrew Murray --- Tests/test_imagecms.py | 14 ++++++++++++++ docs/deprecations.rst | 9 +++++++++ docs/releasenotes/12.0.0.rst | 8 +++++--- src/PIL/ImageCms.py | 14 ++++++++++---- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 55a4a87fb..8b5d88ac8 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -690,3 +690,17 @@ def test_cmyk_lab() -> None: im = Image.new("CMYK", (1, 1)) converted_im = im.convert("LAB") assert converted_im.getpixel((0, 0)) == (255, 128, 128) + + +def test_deprecation() -> None: + profile = ImageCmsProfile(ImageCms.createProfile("sRGB")) + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_name" + ): + profile.product_name + with pytest.warns( + DeprecationWarning, match="ImageCms.ImageCmsProfile.product_info" + ): + profile.product_info + with pytest.raises(AttributeError): + profile.this_attribute_does_not_exist diff --git a/docs/deprecations.rst b/docs/deprecations.rst index 4e65dc807..3f95cf7f5 100644 --- a/docs/deprecations.rst +++ b/docs/deprecations.rst @@ -52,6 +52,15 @@ another mode before saving:: im = Image.new("I", (1, 1)) im.convert("I;16").save("out.png") +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. deprecated:: 12.0.0 + +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.product_info`` attributes have been deprecated, and will be removed in +Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. + Removed features ---------------- diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 68b664443..6c0cd4dba 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -110,10 +110,12 @@ vulnerability introduced in FreeType 2.6 (:cve:`2020-15999`). Deprecations ============ -TODO -^^^^ +ImageCms.ImageCmsProfile.product_name and .product_info +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TODO +``ImageCms.ImageCmsProfile.product_name`` and the corresponding +``.product_info`` attributes have been deprecated, and will be removed in +Pillow 13 (2026-10-15). They have been set to ``None`` since Pillow 2.3.0. API changes =========== diff --git a/src/PIL/ImageCms.py b/src/PIL/ImageCms.py index d3555694a..513e28acf 100644 --- a/src/PIL/ImageCms.py +++ b/src/PIL/ImageCms.py @@ -23,9 +23,10 @@ import operator import sys from enum import IntEnum, IntFlag from functools import reduce -from typing import Literal, SupportsFloat, SupportsInt, Union +from typing import Any, Literal, SupportsFloat, SupportsInt, Union from . import Image +from ._deprecate import deprecate from ._typing import SupportsRead try: @@ -233,9 +234,7 @@ class ImageCmsProfile: low-level profile object """ - self.filename = None - self.product_name = None # profile.product_name - self.product_info = None # profile.product_info + self.filename: str | None = None if isinstance(profile, str): if sys.platform == "win32": @@ -256,6 +255,13 @@ class ImageCmsProfile: msg = "Invalid type for Profile" # type: ignore[unreachable] raise TypeError(msg) + def __getattr__(self, name: str) -> Any: + if name in ("product_name", "product_info"): + deprecate(f"ImageCms.ImageCmsProfile.{name}", 13) + return None + msg = f"'{self.__class__.__name__}' object has no attribute '{name}'" + raise AttributeError(msg) + def tobytes(self) -> bytes: """ Returns the profile in a format suitable for embedding in From 7dbcb32cbe524a8ec4c12f21c762cd7153b2b03b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 19:32:57 +1000 Subject: [PATCH 09/14] Update cygwin/cygwin-install-action action to v6 (#9108) Co-authored-by: Andrew Murray --- .github/workflows/test-cygwin.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml index abfeaa77f..581cd6370 100644 --- a/.github/workflows/test-cygwin.yml +++ b/.github/workflows/test-cygwin.yml @@ -52,7 +52,7 @@ jobs: persist-credentials: false - name: Install Cygwin - uses: cygwin/cygwin-install-action@v5 + uses: cygwin/cygwin-install-action@v6 with: packages: > gcc-g++ @@ -89,10 +89,6 @@ jobs: with: dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - name: Select Python version - run: | - ln -sf c:/cygwin/bin/python3.${{ matrix.python-minor-version }} c:/cygwin/bin/python3 - - name: pip cache uses: actions/cache@v4 with: From 53b6d57b730a68ea58680483f8628c5e25301a1e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 26 Jul 2025 19:39:54 +1000 Subject: [PATCH 10/14] Drop support for PyPy3.10 --- .github/workflows/test-windows.yml | 2 +- .github/workflows/test.yml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 6d8acc44f..766c506e7 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "pypy3.10", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b4b516228..d18023dbc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,7 +42,6 @@ jobs: ] python-version: [ "pypy3.11", - "pypy3.10", "3.14t", "3.14", "3.13t", From 98d38a3bffe572459939cbb6ab730229b4a5a833 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:52:06 +1000 Subject: [PATCH 11/14] Updated libpng to 1.6.50 (#9058) --- .github/workflows/wheels-dependencies.sh | 2 +- winbuild/build_prepare.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wheels-dependencies.sh b/.github/workflows/wheels-dependencies.sh index 6b5aedb69..4519271b9 100755 --- a/.github/workflows/wheels-dependencies.sh +++ b/.github/workflows/wheels-dependencies.sh @@ -95,7 +95,7 @@ ARCHIVE_SDIR=pillow-depends-main # you change those versions, ensure the patch is also updated. FREETYPE_VERSION=2.13.3 HARFBUZZ_VERSION=11.2.1 -LIBPNG_VERSION=1.6.49 +LIBPNG_VERSION=1.6.50 JPEGTURBO_VERSION=3.1.1 OPENJPEG_VERSION=2.5.3 XZ_VERSION=5.8.1 diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 2307fc8b2..fbff0daf2 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -121,7 +121,7 @@ V = { "LCMS2": "2.17", "LIBAVIF": "1.3.0", "LIBIMAGEQUANT": "4.3.4", - "LIBPNG": "1.6.49", + "LIBPNG": "1.6.50", "LIBWEBP": "1.6.0", "OPENJPEG": "2.5.3", "TIFF": "4.7.0", From bae97e1a2b75a1e3c01efc168d10b8d7ecdf3392 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 30 Jul 2025 21:50:45 +1000 Subject: [PATCH 12/14] Update dependency cibuildwheel to v3.1.2 (#9118) --- .ci/requirements-cibw.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-cibw.txt b/.ci/requirements-cibw.txt index e1eb52eb8..823671828 100644 --- a/.ci/requirements-cibw.txt +++ b/.ci/requirements-cibw.txt @@ -1 +1 @@ -cibuildwheel==3.0.1 +cibuildwheel==3.1.2 From ba5f81fb6b4bd143b2ceba6875b33870eaa366ce Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 30 Jul 2025 15:23:39 +0300 Subject: [PATCH 13/14] Add support for Python 3.14 (#9120) Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/installation/newer-versions.csv | 19 ++++++++++--------- docs/releasenotes/12.0.0.rst | 10 +++++++--- pyproject.toml | 3 ++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/installation/newer-versions.csv b/docs/installation/newer-versions.csv index 19816af58..e948dd540 100644 --- a/docs/installation/newer-versions.csv +++ b/docs/installation/newer-versions.csv @@ -1,9 +1,10 @@ -Python,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 -Pillow >= 11,Yes,Yes,Yes,Yes,Yes,,,, -Pillow 10.1 - 10.4,,Yes,Yes,Yes,Yes,Yes,,, -Pillow 10.0,,,Yes,Yes,Yes,Yes,,, -Pillow 9.3 - 9.5,,,Yes,Yes,Yes,Yes,Yes,, -Pillow 9.0 - 9.2,,,,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 7.0 - 7.2,,,,,,Yes,Yes,Yes,Yes +Python,3.14,3.13,3.12,3.11,3.10,3.9,3.8,3.7,3.6,3.5 +Pillow 12,Yes,Yes,Yes,Yes,Yes,,,,, +Pillow 11,,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 10.1 - 10.4,,,Yes,Yes,Yes,Yes,Yes,,, +Pillow 10.0,,,,Yes,Yes,Yes,Yes,,, +Pillow 9.3 - 9.5,,,,Yes,Yes,Yes,Yes,Yes,, +Pillow 9.0 - 9.2,,,,,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 7.0 - 7.2,,,,,,,Yes,Yes,Yes,Yes diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 6c0cd4dba..46cf64cf1 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -136,7 +136,11 @@ TODO Other changes ============= -TODO -^^^^ +Python 3.14 +^^^^^^^^^^^ -TODO +Pillow 11.3.0 had wheels built against Python 3.14 beta, available as a preview to help +others prepare for 3.14, and to ensure Pillow could be used immediately at the release +of 3.14.0 final (2025-10-07, :pep:`745`). + +Pillow 12.0.0 now officially supports Python 3.14. diff --git a/pyproject.toml b/pyproject.toml index 4e8623118..3693ddb8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Multimedia :: Graphics", @@ -206,7 +207,7 @@ lint.isort.required-imports = [ ] [tool.pyproject-fmt] -max_supported_python = "3.13" +max_supported_python = "3.14" [tool.pytest.ini_options] addopts = "-ra --color=auto" From 98d6c3bf8818849e2414ef4de8c9e02b03de3886 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 1 Aug 2025 08:22:28 +0800 Subject: [PATCH 14/14] Restore pyroma test for iOS (#9116) Co-authored-by: Andrew Murray --- Tests/test_image_access.py | 6 +++++- Tests/test_pyroma.py | 25 ++++++++++++++++++++++++- pyproject.toml | 2 +- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index a847264d2..07c12594a 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -317,4 +317,8 @@ int main(int argc, char* argv[]) assert process.returncode == 0 def teardown_method(self) -> None: - os.remove("embed_pil.c") + try: + os.remove("embed_pil.c") + except FileNotFoundError: + # If the test was skipped or failed, the file won't exist + pass diff --git a/Tests/test_pyroma.py b/Tests/test_pyroma.py index c2f7fe22e..35f3fd076 100644 --- a/Tests/test_pyroma.py +++ b/Tests/test_pyroma.py @@ -1,5 +1,7 @@ from __future__ import annotations +from importlib.metadata import metadata + import pytest from PIL import __version__ @@ -7,9 +9,30 @@ from PIL import __version__ pyroma = pytest.importorskip("pyroma", reason="Pyroma not installed") +def map_metadata_keys(metadata): + # Convert installed wheel metadata into canonical Core Metadata 2.4 format. + # This was a utility method in pyroma 4.3.3; it was removed in 5.0. + # This implementation is constructed from the relevant logic from + # Pyroma 5.0's `build_metadata()` implementation. This has been submitted + # upstream to Pyroma as https://github.com/regebro/pyroma/pull/116, + # so it may be possible to simplify this test in future. + data = {} + for key in set(metadata.keys()): + value = metadata.get_all(key) + key = pyroma.projectdata.normalize(key) + + if len(value) == 1: + value = value[0] + if value.strip() == "UNKNOWN": + continue + + data[key] = value + return data + + def test_pyroma() -> None: # Arrange - data = pyroma.projectdata.get_data(".") + data = map_metadata_keys(metadata("Pillow")) # Act rating = pyroma.ratings.rate(data) diff --git a/pyproject.toml b/pyproject.toml index 3693ddb8d..4980a9cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -68,7 +68,7 @@ optional-dependencies.tests = [ "markdown2", "olefile", "packaging", - "pyroma", + "pyroma>=5", "pytest", "pytest-cov", "pytest-timeout",