From fda19a5101d85bda04a86d2c58265eb42ca1c1b1 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 1 Jul 2026 15:33:15 +0300 Subject: [PATCH 1/9] Drop support for Python 3.10 --- .github/compare-dist-sizes.py | 2 +- .github/generate-sbom.py | 2 +- .github/workflows/test-docker.yml | 1 - .github/workflows/test-windows.yml | 4 ++-- .github/workflows/test.yml | 9 ++++----- .github/workflows/wheels.yml | 2 +- Tests/test_bmp_reference.py | 4 ++-- Tests/test_file_avif.py | 4 ++-- Tests/test_file_gif.py | 2 +- Tests/test_file_jpeg2k.py | 2 +- Tests/test_file_pdf.py | 2 +- Tests/test_file_tiff.py | 2 +- Tests/test_file_webp_animated.py | 2 +- Tests/test_image_resample.py | 6 ++---- Tests/test_image_resize.py | 2 +- Tests/test_imageops_usm.py | 2 +- docs/installation/platform-support.rst | 10 +++++----- docs/releasenotes/13.0.0.rst | 6 ++++++ pyproject.toml | 5 ++--- setup.py | 2 +- src/PIL/Image.py | 6 +++--- src/PIL/ImageFont.py | 2 +- src/PIL/PcfFontFile.py | 2 +- src/PIL/TiffImagePlugin.py | 4 +--- tox.ini | 2 +- 25 files changed, 43 insertions(+), 44 deletions(-) diff --git a/.github/compare-dist-sizes.py b/.github/compare-dist-sizes.py index ed7b9be0ed6..0b4f71793d2 100644 --- a/.github/compare-dist-sizes.py +++ b/.github/compare-dist-sizes.py @@ -10,7 +10,7 @@ """ # /// script -# requires-python = ">=3.10" +# requires-python = ">=3.11" # dependencies = [ # "humanize", # "prettytable", diff --git a/.github/generate-sbom.py b/.github/generate-sbom.py index e0324d1429e..6fecfc29ae4 100755 --- a/.github/generate-sbom.py +++ b/.github/generate-sbom.py @@ -59,7 +59,7 @@ def upstream_diff_b64( def generate(version: str) -> dict: serial = str(uuid.uuid4()) - now = dt.datetime.now(dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + now = dt.datetime.now(dt.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") purl = f"pkg:pypi/pillow@{version}" root = Path(__file__).parent.parent thirdparty = root / "src" / "thirdparty" diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 96ec3e392e6..e11d08fc6ef 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,7 +47,6 @@ jobs: fedora-43-amd64, fedora-44-amd64, gentoo, - ubuntu-22.04-jammy-amd64, ubuntu-24.04-noble-amd64, ubuntu-26.04-resolute-amd64, ] diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 52163fd781b..ff8377a503b 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -30,12 +30,12 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14", "3.15"] + python-version: ["pypy3.11", "3.12", "3.13", "3.14", "3.15"] architecture: ["x64"] os: ["windows-latest"] include: # Test the oldest Python on 32-bit - - { python-version: "3.10", architecture: "x86", os: "windows-2022" } + - { python-version: "3.11", architecture: "x86", os: "windows-2022" } timeout-minutes: 45 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40db871e97f..9cd227808e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,15 +44,14 @@ jobs: "3.13", "3.12", "3.11", - "3.10", ] include: - - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - - { python-version: "3.11", PYTHONOPTIMIZE: 2 } + - { python-version: "3.13", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } + - { python-version: "3.12", PYTHONOPTIMIZE: 2 } # Intel - - { os: "macos-26-intel", python-version: "3.10" } + - { os: "macos-26-intel", python-version: "3.11" } exclude: - - { os: "macos-latest", python-version: "3.10" } + - { os: "macos-latest", python-version: "3.11" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 3cb477df777..8d568ecb0a4 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -81,7 +81,7 @@ jobs: platform: macos os: macos-26-intel cibw_arch: x86_64 - build: "cp3{10,11}*" + build: "cp311*" macosx_deployment_target: "10.10" - name: "macOS 10.13 x86_64" platform: macos diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index ea0853100cc..cab4fbdf16d 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -56,7 +56,7 @@ def test_questionable() -> None: im.load() if os.path.basename(f) not in supported: print(f"Please add {f} to the partially supported bmp specs.") - except Exception: # noqa: PERF203 + except Exception: if os.path.basename(f) in supported: raise @@ -106,7 +106,7 @@ def get_compare(f: str) -> str: assert_image_similar(im_converted, compare_converted, 5) - except Exception as msg: # noqa: PERF203 + except Exception as msg: # there are three here that are unsupported: unsupported = ( os.path.join(base, "g", "rgb32bf.bmp"), diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index f8b29cb2c93..c0fec6d8f32 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -603,7 +603,7 @@ def test_svt_optimizations(self, tmp_path: Path) -> None: @skip_unless_feature("avif") class TestAvifAnimation: @contextmanager - def star_frames(self) -> Generator[list[Image.Image], None, None]: + def star_frames(self) -> Generator[list[Image.Image]]: with Image.open("Tests/images/avif/star.png") as f: yield [f, f.rotate(90), f.rotate(180), f.rotate(270)] @@ -676,7 +676,7 @@ def check(temp_file: Path) -> None: # Test appending using a generator def imGenerator( ims: list[Image.Image], - ) -> Generator[Image.Image, None, None]: + ) -> Generator[Image.Image]: yield from ims temp_file2 = tmp_path / "temp_generator.avif" diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 7b504233d56..5293094c43a 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -1191,7 +1191,7 @@ def test_append_images(tmp_path: Path) -> None: assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 8976aea0924..705dac880e5 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -33,7 +33,7 @@ @pytest.fixture -def card() -> Generator[ImageFile.ImageFile, None, None]: +def card() -> Generator[ImageFile.ImageFile]: with Image.open("Tests/images/test-card.png") as im: im.load() try: diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 8bb4ff8db92..3fb8ae62b84 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -164,7 +164,7 @@ def test_save_all(tmp_path: Path) -> None: assert os.path.getsize(outfile) > 0 # Test appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image]: yield from ims im.save(outfile, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index e442471d1ca..0400934cff0 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -775,7 +775,7 @@ def test_tiff_save_all(self) -> None: assert reread.n_frames == 3 # Test appending using a generator - def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image]: yield from ims mp = BytesIO() diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 600448fb9e1..7ae93e76b09 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -95,7 +95,7 @@ def check(temp_file: Path) -> None: # Tests appending using a generator def im_generator( ims: list[Image.Image], - ) -> Generator[Image.Image, None, None]: + ) -> Generator[Image.Image]: yield from ims temp_file2 = tmp_path / "temp_generator.webp" diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f188be81ecb..61b2842cc66 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -372,7 +372,7 @@ def test_dirty_pixels_la(self) -> None: class TestCoreResamplePasses: @contextmanager - def count(self, diff: int) -> Generator[None, None, None]: + def count(self, diff: int) -> Generator[None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff @@ -485,9 +485,7 @@ def test_wrong_arguments(self, resample: Image.Resampling) -> None: def resize_tiled( self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int ) -> Image.Image: - def split_range( - size: int, tiles: int - ) -> Generator[tuple[int, int], None, None]: + def split_range(size: int, tiles: int) -> Generator[tuple[int, int]]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index 3e8979a5b11..b2abe4c9810 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -179,7 +179,7 @@ def test_cross_platform(self, tmp_path: Path) -> None: @pytest.fixture -def gradients_image() -> Generator[ImageFile.ImageFile, None, None]: +def gradients_image() -> Generator[ImageFile.ImageFile]: with Image.open("Tests/images/radial_gradients.png") as im: im.load() try: diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index 920012d8639..14620c98e47 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -8,7 +8,7 @@ @pytest.fixture -def test_images() -> Generator[dict[str, ImageFile.ImageFile], None, None]: +def test_images() -> Generator[dict[str, ImageFile.ImageFile]]: ims = { "im": Image.open("Tests/images/hopper.ppm"), "snakes": Image.open("Tests/images/color_snakes.png"), diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 3cbfebb3b40..93c6ecdf895 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -23,7 +23,7 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Arch | 3.14 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| CentOS Stream 9 | 3.10 | x86-64 | +| CentOS Stream 9 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 10 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -38,19 +38,19 @@ These platforms are built and tested for every change. | macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | | | 3.15, PyPy3 | | +----------------------------------+----------------------------+---------------------+ -| macOS 26 Tahoe | 3.10 | x86-64 | +| macOS 26 Tahoe | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| Ubuntu Linux 24.04 LTS (Noble) | 3.11, 3.12, 3.13, | x86-64 | | | 3.14, 3.15, PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 26.04 LTS (Resolute)| 3.14 | x86-64, arm64v8, | | | | ppc64le, s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.10 | x86 | +| Windows Server 2022 | 3.11 | x86 | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2025 | 3.11, 3.12, 3.13, 3.14, | x86-64 | +| Windows Server 2025 | 3.12, 3.13, 3.14, | x86-64 | | | 3.15, PyPy3 | | | +----------------------------+---------------------+ | | 3.14 (MinGW) | x86-64 | diff --git a/docs/releasenotes/13.0.0.rst b/docs/releasenotes/13.0.0.rst index e7a97e3cab7..45667d9ec07 100644 --- a/docs/releasenotes/13.0.0.rst +++ b/docs/releasenotes/13.0.0.rst @@ -17,6 +17,12 @@ TODO Backwards incompatible changes ============================== +Python 3.10 +^^^^^^^^^^^ + +Pillow has dropped support for Python 3.10, +which reached end-of-life in October 2026. + Saving I mode images as PNG ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pyproject.toml b/pyproject.toml index dab8602d762..392151970ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,11 +20,10 @@ license-files = [ "LICENSE" ] authors = [ { name = "Jeffrey 'Alex' Clark", email = "aclark@aclark.net" }, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 6 - Mature", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -190,7 +189,7 @@ max_supported_python = "3.15" [tool.mypy] follow_imports = "silent" -python_version = "3.10" +python_version = "3.11" disallow_any_generics = true disallow_untyped_defs = true warn_redundant_casts = true diff --git a/setup.py b/setup.py index 476b595e696..168d4f237c9 100644 --- a/setup.py +++ b/setup.py @@ -301,7 +301,7 @@ def _pkg_config(name: str) -> tuple[list[str], list[str]] | None: subprocess.check_output(command_cflags).decode("utf8").strip(), )[::2][1:] return libs, cflags - except Exception: # noqa: PERF203 + except Exception: pass return None diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6bb1a6a4a46..adf8ac0444b 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -488,7 +488,7 @@ def init() -> bool: try: logger.debug("Importing %s", plugin) __import__(f"{__spec__.parent}.{plugin}", globals(), locals(), []) - except ImportError as e: # noqa: PERF203 + except ImportError as e: logger.debug("Image: failed to import %s: %s", plugin, e) if OPEN or SAVE: @@ -3359,8 +3359,8 @@ class SupportsArrowArrayInterface(Protocol): """ def __arrow_c_array__( - self, requested_schema: "PyCapsule" = None # type: ignore[name-defined] # noqa: F821, UP037 - ) -> tuple["PyCapsule", "PyCapsule"]: # type: ignore[name-defined] # noqa: F821, UP037 + self, requested_schema: PyCapsule = None # type: ignore[name-defined] # noqa: F821 + ) -> tuple[PyCapsule, PyCapsule]: # type: ignore[name-defined] # noqa: F821 raise NotImplementedError() diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 65edf5659fa..0eb686d07a7 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -951,7 +951,7 @@ def load_path(filename: str | bytes) -> ImageFont: for directory in sys.path: try: return load(os.path.join(directory, filename)) - except OSError: # noqa: PERF203 + except OSError: pass msg = f'cannot find font file "{filename}" in sys.path' if os.path.exists(filename): diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 985444106a1..f12506a6666 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -249,7 +249,7 @@ def _load_encoding(self) -> list[int | None]: ] if encoding_offset != 0xFFFF: encoding[i] = encoding_offset - except UnicodeDecodeError: # noqa: PERF203 + except UnicodeDecodeError: # character is not supported in selected encoding pass diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 472dfcf5802..a820e7ead35 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -465,9 +465,7 @@ def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: __floor__ = _delegate("__floor__") __round__ = _delegate("__round__") __float__ = _delegate("__float__") - # Python >= 3.11 - if hasattr(Fraction, "__int__"): - __int__ = _delegate("__int__") + __int__ = _delegate("__int__") _LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] diff --git a/tox.ini b/tox.ini index 01912dc6cd1..32ccae6d87d 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ env_list = lint mypy py{py3} - py{310-315} + py{311-315} py{314-315}t [testenv] From c37371a4e14d6a0903ac937ac6144fc6c2601923 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 2 Jul 2026 09:08:27 +1000 Subject: [PATCH 2/9] Update wheel count --- .github/workflows/wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 8d568ecb0a4..d4f99b5faf6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -37,7 +37,7 @@ concurrency: cancel-in-progress: true env: - EXPECTED_DISTS: 87 + EXPECTED_DISTS: 78 FORCE_COLOR: 1 jobs: From c1d3f7b53d1e2d856875b90b8d0142e3b1ae295f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 9 Jul 2026 18:59:56 +1000 Subject: [PATCH 3/9] Update Valgrind from Ubuntu 22.04 to 24.04 --- .github/workflows/test-valgrind-memory.yml | 2 +- .github/workflows/test-valgrind.yml | 2 +- docs/installation/platform-support.rst | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index 3a4957651b1..3fe09e03afe 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -38,7 +38,7 @@ jobs: fail-fast: false matrix: docker: [ - ubuntu-22.04-jammy-amd64-valgrind, + ubuntu-24.04-noble-amd64-valgrind, ] dockerTag: [main] diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index ecc00666b96..695728126f7 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -32,7 +32,7 @@ jobs: fail-fast: false matrix: docker: [ - ubuntu-22.04-jammy-amd64-valgrind, + ubuntu-24.04-noble-amd64-valgrind, ] dockerTag: [main] diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 93c6ecdf895..ba2625ef270 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -40,8 +40,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | macOS 26 Tahoe | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.11, 3.12, 3.13, | x86-64 | | | 3.14, 3.15, PyPy3 | | +----------------------------------+----------------------------+---------------------+ From 09c72fa8f31226b808e699a26f8b9adae51d3d99 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:59:00 +0300 Subject: [PATCH 4/9] Split slow valgrind tests in four batches --- .github/workflows/test-valgrind-memory.yml | 5 +++-- .github/workflows/test-valgrind.yml | 5 +++-- Makefile | 2 +- depends/docker-test-valgrind-memory.sh | 7 ++++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test-valgrind-memory.yml b/.github/workflows/test-valgrind-memory.yml index 3fe09e03afe..7d9b4537da6 100644 --- a/.github/workflows/test-valgrind-memory.yml +++ b/.github/workflows/test-valgrind-memory.yml @@ -41,8 +41,9 @@ jobs: ubuntu-24.04-noble-amd64-valgrind, ] dockerTag: [main] + split: [1, 2, 3, 4] - name: ${{ matrix.docker }} + name: ${{ matrix.docker }} ${{ matrix.split }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -60,5 +61,5 @@ jobs: run: | # The Pillow user in the docker container is UID 1001 sudo chown -R 1001 $GITHUB_WORKSPACE - docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -e "SPLIT_COUNT=4" -e "SPLIT_INDEX=${{ matrix.split }}" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} /Pillow/depends/docker-test-valgrind-memory.sh sudo chown -R runner $GITHUB_WORKSPACE diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml index 695728126f7..3c6d0e19eeb 100644 --- a/.github/workflows/test-valgrind.yml +++ b/.github/workflows/test-valgrind.yml @@ -35,8 +35,9 @@ jobs: ubuntu-24.04-noble-amd64-valgrind, ] dockerTag: [main] + split: [1, 2, 3, 4] - name: ${{ matrix.docker }} + name: ${{ matrix.docker }} ${{ matrix.split }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -54,5 +55,5 @@ jobs: run: | # The Pillow user in the docker container is UID 1001 sudo chown -R 1001 $GITHUB_WORKSPACE - docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} + docker run --name pillow_container -e "PILLOW_VALGRIND_TEST=true" -e "SPLIT_COUNT=4" -e "SPLIT_INDEX=${{ matrix.split }}" -v $GITHUB_WORKSPACE:/Pillow pythonpillow/${{ matrix.docker }}:${{ matrix.dockerTag }} sudo chown -R runner $GITHUB_WORKSPACE diff --git a/Makefile b/Makefile index 0ab4651689c..ba8ccd095f4 100644 --- a/Makefile +++ b/Makefile @@ -114,7 +114,7 @@ valgrind-leak: PILLOW_VALGRIND_TEST=true PYTHONMALLOC=malloc valgrind --suppressions=Tests/oss-fuzz/python.supp \ --leak-check=full --show-leak-kinds=definite --errors-for-leak-kinds=definite \ --log-file=/tmp/valgrind-output \ - python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output + python3 -m pytest -vv --valgrind --valgrind-log=/tmp/valgrind-output $(PYTEST_ARGS) .PHONY: readme readme: diff --git a/depends/docker-test-valgrind-memory.sh b/depends/docker-test-valgrind-memory.sh index f0d1d851dd1..9fb97300356 100755 --- a/depends/docker-test-valgrind-memory.sh +++ b/depends/docker-test-valgrind-memory.sh @@ -8,4 +8,9 @@ source /vpy3/bin/activate cd /Pillow make clean make install -make valgrind-leak + +SPLIT_ARGS="" +if [ "${SPLIT_COUNT:-1}" -gt 1 ]; then + SPLIT_ARGS="--splits ${SPLIT_COUNT} --group ${SPLIT_INDEX} --durations-path /depends/.test_durations" +fi +make valgrind-leak PYTEST_ARGS="$SPLIT_ARGS" From 2f15eb194b11f8ba831ceab58d39bf22db1d4993 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 10 Jul 2026 06:56:01 +1000 Subject: [PATCH 5/9] Use typing.Self --- src/PIL/ContainerIO.py | 8 ++++++-- src/PIL/GimpPaletteFile.py | 4 ++-- src/PIL/Image.py | 4 ++-- src/PIL/ImageFile.py | 9 ++++----- src/PIL/ImageMath.py | 4 ++-- src/PIL/ImageSequence.py | 3 ++- src/PIL/PdfParser.py | 4 ++-- src/PIL/PngImagePlugin.py | 9 +++------ src/PIL/TiffImagePlugin.py | 4 ++-- 9 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/PIL/ContainerIO.py b/src/PIL/ContainerIO.py index ec9e66c714f..31427a7abff 100644 --- a/src/PIL/ContainerIO.py +++ b/src/PIL/ContainerIO.py @@ -19,6 +19,10 @@ from collections.abc import Iterable from typing import IO, AnyStr, NoReturn +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Self + class ContainerIO(IO[AnyStr]): """ @@ -147,13 +151,13 @@ def writelines(self, lines: Iterable[AnyStr]) -> NoReturn: def truncate(self, size: int | None = None) -> int: raise NotImplementedError() - def __enter__(self) -> ContainerIO[AnyStr]: + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: self.close() - def __iter__(self) -> ContainerIO[AnyStr]: + def __iter__(self) -> Self: return self def __next__(self) -> AnyStr: diff --git a/src/PIL/GimpPaletteFile.py b/src/PIL/GimpPaletteFile.py index 016257d3dd2..5dc83086dd1 100644 --- a/src/PIL/GimpPaletteFile.py +++ b/src/PIL/GimpPaletteFile.py @@ -20,7 +20,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from typing import IO + from typing import IO, Self class GimpPaletteFile: @@ -66,7 +66,7 @@ def __init__(self, fp: IO[bytes]) -> None: self._read(fp) @classmethod - def frombytes(cls, data: bytes) -> GimpPaletteFile: + def frombytes(cls, data: bytes) -> Self: self = cls.__new__(cls) self._read(BytesIO(data), False) return self diff --git a/src/PIL/Image.py b/src/PIL/Image.py index adf8ac0444b..de9fe1e398e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -67,7 +67,7 @@ if TYPE_CHECKING: from collections.abc import Callable, Iterator, Sequence from types import ModuleType - from typing import Any, Literal + from typing import Any, Literal, Self logger = logging.getLogger(__name__) @@ -691,7 +691,7 @@ def _new(self, im: core.ImagingCore) -> Image: return new # Context manager support - def __enter__(self) -> Image: + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2d69225b5c0..eaa1e45812c 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -41,6 +41,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from typing import Self + from ._typing import StrOrBytesPath logger = logging.getLogger(__name__) @@ -173,10 +175,6 @@ def __init__( def _open(self) -> None: pass - # Context manager support - def __enter__(self) -> ImageFile: - return self - def _close_fp(self) -> None: if getattr(self, "_fp", False) and not isinstance(self._fp, DeferredError): if self._fp != self.fp: @@ -185,6 +183,7 @@ def _close_fp(self) -> None: if self.fp: self.fp.close() + # Context manager support def __exit__(self, *args: object) -> None: if getattr(self, "_exclusive_fp", False): self._close_fp() @@ -601,7 +600,7 @@ def feed(self, data: bytes) -> None: self.image = im - def __enter__(self) -> Parser: + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index f62c6a3f845..b129ebaad9a 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -24,7 +24,7 @@ if TYPE_CHECKING: from collections.abc import Callable from types import CodeType - from typing import Any + from typing import Any, Self class _Operand: @@ -104,7 +104,7 @@ def __bool__(self) -> bool: def __abs__(self) -> _Operand: return self.apply("abs", self) - def __pos__(self) -> _Operand: + def __pos__(self) -> Self: return self def __neg__(self) -> _Operand: diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index 361be48971e..9d3553a75a8 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -21,6 +21,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Callable + from typing import Self class Iterator: @@ -50,7 +51,7 @@ def __getitem__(self, ix: int) -> Image.Image: msg = "end of sequence" raise IndexError(msg) from e - def __iter__(self) -> Iterator: + def __iter__(self) -> Self: return self def __next__(self) -> Image.Image: diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index c2e1568f969..5ba70ba209d 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -14,7 +14,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: - from typing import IO + from typing import IO, Self _DictBase = collections.UserDict[str | bytes, Any] else: @@ -427,7 +427,7 @@ def __init__( if f: self.seek_end() - def __enter__(self) -> PdfParser: + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c777b4d3162..77af87861fb 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -53,7 +53,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Callable - from typing import Any, NoReturn + from typing import Any, NoReturn, Self from . import _imaging @@ -184,7 +184,7 @@ def read(self) -> tuple[bytes, int, int]: return cid, pos, length - def __enter__(self) -> ChunkStream: + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: @@ -265,7 +265,7 @@ class iTXt(str): @staticmethod def __new__( cls, text: str, lang: str | None = None, tkey: str | None = None - ) -> iTXt: + ) -> Self: """ :param cls: the class to use when creating the instance :param text: value for this key @@ -399,9 +399,6 @@ def __init__(self, fp: IO[bytes]) -> None: self.text_memory = 0 - def __enter__(self) -> PngStream: - return self - def check_text_memory(self, chunklen: int) -> None: self.text_memory += chunklen if self.text_memory > MAX_TEXT_MEMORY: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index a820e7ead35..c3a1852ab9c 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -62,7 +62,7 @@ TYPE_CHECKING = False if TYPE_CHECKING: from collections.abc import Iterator - from typing import NoReturn + from typing import NoReturn, Self from ._typing import Buffer, IntegralLike, StrOrBytesPath @@ -2117,7 +2117,7 @@ def newFrame(self) -> None: self.finalize() self.setup() - def __enter__(self) -> AppendingTiffWriter: + def __enter__(self) -> Self: return self def __exit__(self, *args: object) -> None: From e77dc81867bb27d9c5344c2f85ff42cc940767e3 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:48:23 +0300 Subject: [PATCH 6/9] Use typing.NotRequired --- src/PIL/ImageFont.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 0eb686d07a7..224beef695a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -35,7 +35,7 @@ from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, Any, BinaryIO, TypedDict, cast +from typing import IO, Any, BinaryIO, NotRequired, TypedDict, cast from . import Image from ._typing import StrOrBytesPath @@ -52,7 +52,7 @@ class Axis(TypedDict): minimum: int | None default: int | None maximum: int | None - name: bytes | None + name: NotRequired[bytes] class Layout(IntEnum): @@ -732,7 +732,7 @@ def get_variation_axes(self) -> list[Axis]: """ axes = self.font.getvaraxes() for axis in axes: - if axis["name"]: + if "name" in axis: axis["name"] = axis["name"].replace(b"\x00", b"") return axes From fd3f92c075a0e8dd5c7df8621a613cdac3b398d2 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:03:31 +0300 Subject: [PATCH 7/9] Simplify warnings.catch_warnings(action=...) --- Tests/test_bmp_reference.py | 4 +--- Tests/test_file_avif.py | 4 +--- Tests/test_file_dcx.py | 8 ++------ Tests/test_file_fli.py | 8 ++------ Tests/test_file_gif.py | 8 ++------ Tests/test_file_icns.py | 4 +--- Tests/test_file_im.py | 8 ++------ Tests/test_file_jpeg.py | 4 +--- Tests/test_file_mpo.py | 8 ++------ Tests/test_file_png.py | 4 +--- Tests/test_file_psd.py | 8 ++------ Tests/test_file_spider.py | 8 ++------ Tests/test_file_tar.py | 8 ++------ Tests/test_file_tiff.py | 8 ++------ Tests/test_file_webp.py | 4 +--- Tests/test_image.py | 4 +--- Tests/test_image_fromqimage.py | 3 +-- Tests/test_imageqt.py | 4 +--- Tests/test_numpy.py | 4 +--- 19 files changed, 28 insertions(+), 83 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index cab4fbdf16d..d0a3c4ed5d8 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -21,9 +21,7 @@ def test_bad() -> None: either""" for f in get_files("b"): # Assert that there is no unclosed file warning - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): try: with Image.open(f) as im: im.load() diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py index c0fec6d8f32..d667c416919 100644 --- a/Tests/test_file_avif.py +++ b/Tests/test_file_avif.py @@ -208,9 +208,7 @@ def finish(self) -> None: def test_no_resource_warning(self, tmp_path: Path) -> None: with Image.open(TEST_AVIF_FILE) as im: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im.save(tmp_path / "temp.avif") @pytest.mark.parametrize("major_brand", [b"avif", b"avis", b"mif1", b"msf1"]) diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index e9d88dd3964..7836d13d2a4 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -35,18 +35,14 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(TEST_FILE) im.load() im.close() def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index 13c6a43239b..9000c0b01ba 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -73,9 +73,7 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(static_test_file) im.load() im.close() @@ -91,9 +89,7 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(static_test_file) as im: im.load() diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 5293094c43a..52997e694c1 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -43,9 +43,7 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(TEST_GIF) im.load() im.close() @@ -66,9 +64,7 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(TEST_GIF) as im: im.load() diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index b9b81850665..c492a343cb3 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -20,9 +20,7 @@ def test_sanity() -> None: # (512x512@2x) being loaded with Image.open(TEST_FILE) as im: # Assert that there is no unclosed file warning - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im.load() assert im.mode == "RGBA" diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 55c6b730599..dfd33078847 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -40,18 +40,14 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(TEST_IM) im.load() im.close() def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(TEST_IM) as im: im.load() diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 145916ed87d..e6ab1c915e6 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -880,9 +880,7 @@ def test_exif_x_resolution(self, tmp_path: Path) -> None: assert exif[282] == 180 out = tmp_path / "out.jpg" - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im.save(out, exif=exif) with Image.open(out) as reloaded: diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 7f35693f504..c682fd661cf 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -59,9 +59,7 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(test_files[0]) im.load() im.close() @@ -76,9 +74,7 @@ def test_seek_after_close() -> None: def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(test_files[0]) as im: im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 9f81f3288a7..f4fe1de013d 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -367,9 +367,7 @@ def test_load_verify(self) -> None: with Image.open(TEST_PNG_FILE) as im: # Assert that there is no unclosed file warning - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im.verify() with Image.open(TEST_PNG_FILE) as im: diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index 79cc8bc00a0..a376ba2e887 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -41,18 +41,14 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(test_file) im.load() im.close() def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(test_file) as im: im.load() diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 71fb434ccf4..933f2f8b4e8 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -33,18 +33,14 @@ def open_test_image() -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open(TEST_FILE) im.load() im.close() def test_context_manager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open(TEST_FILE) as im: im.load() diff --git a/Tests/test_file_tar.py b/Tests/test_file_tar.py index 084d0f2880d..a6a6fa64727 100644 --- a/Tests/test_file_tar.py +++ b/Tests/test_file_tar.py @@ -53,16 +53,12 @@ def test_unclosed_file() -> None: def test_close() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): tar = TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg") tar.close() def test_contextmanager() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with TarIO.TarIO(TEST_TAR_FILE, "hopper.jpg"): pass diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 0400934cff0..145ab3e8920 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -65,9 +65,7 @@ def open_test_image() -> None: open_test_image() def test_closed_file(self) -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im = Image.open("Tests/images/multipage.tiff") im.load() im.close() @@ -83,9 +81,7 @@ def test_seek_after_close(self) -> None: im.seek(1) def test_context_manager(self) -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): with Image.open("Tests/images/multipage.tiff") as im: im.load() diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e419c29f036..7360c1df06f 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -190,9 +190,7 @@ def test_WebPAnimDecoder_with_invalid_args(self) -> None: def test_no_resource_warning(self, tmp_path: Path) -> None: file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): image.save(tmp_path / "temp.webp") def test_file_pointer_could_be_reused(self) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 300ea81e00e..fa095f4597f 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -752,9 +752,7 @@ def test_no_resource_warning_on_save(self, tmp_path: Path) -> None: # Act/Assert with Image.open(test_file) as im: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): im.save(temp_file) def test_no_new_file_on_error(self, tmp_path: Path) -> None: diff --git a/Tests/test_image_fromqimage.py b/Tests/test_image_fromqimage.py index 1149e29649e..4c6f17267af 100644 --- a/Tests/test_image_fromqimage.py +++ b/Tests/test_image_fromqimage.py @@ -6,8 +6,7 @@ from PIL import Image -with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=DeprecationWarning) +with warnings.catch_warnings(action="ignore", category=DeprecationWarning): from PIL import ImageQt from .helper import assert_image_equal, hopper diff --git a/Tests/test_imageqt.py b/Tests/test_imageqt.py index 2d7ca0ae0fd..552a9df0783 100644 --- a/Tests/test_imageqt.py +++ b/Tests/test_imageqt.py @@ -51,7 +51,5 @@ def test_image(mode: str) -> None: def test_closed_file() -> None: - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): ImageQt.ImageQt("Tests/images/hopper.gif") diff --git a/Tests/test_numpy.py b/Tests/test_numpy.py index 96478a144f3..290c9914d54 100644 --- a/Tests/test_numpy.py +++ b/Tests/test_numpy.py @@ -256,7 +256,5 @@ def test_no_resource_warning_for_numpy_array() -> None: test_file = "Tests/images/hopper.png" with Image.open(test_file) as im: # Act/Assert - with warnings.catch_warnings(): - warnings.simplefilter("error") - + with warnings.catch_warnings(action="error"): array(im) From db8e5794e7dbeea8e0094ab3f10de5b74a2e3ab6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 10 Jul 2026 15:19:48 +0300 Subject: [PATCH 8/9] Re-enable PYI034 and use typing.Self --- pyproject.toml | 1 - src/PIL/TiffTags.py | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 392151970ee..eb53829d21f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -168,7 +168,6 @@ lint.ignore = [ "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements "PT017", # pytest-assert-in-except - "PYI034", # flake8-pyi: typing.Self added in Python 3.11 ] lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ "I002", diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 613a3b7def7..8ce84a175c0 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -20,6 +20,10 @@ from typing import NamedTuple +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Self + class _TagInfo(NamedTuple): value: int | None @@ -39,7 +43,7 @@ def __new__( type: int | None = None, length: int | None = None, enum: dict[str, int] | None = None, - ) -> TagInfo: + ) -> Self: return super().__new__(cls, value, name, type, length, enum or {}) def cvt_enum(self, value: str) -> int | str: From ee5464bc95014c04efd34ce2a4db413ec6615ad6 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:02:26 +1000 Subject: [PATCH 9/9] Update macOS Python versions in CI targets (#176) Co-authored-by: Andrew Murray --- docs/installation/platform-support.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index ba2625ef270..c15eb64a491 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -35,10 +35,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 15 Sequoia | 3.11, 3.12, 3.13, 3.14, | arm64 | -| | 3.15, PyPy3 | | -+----------------------------------+----------------------------+---------------------+ -| macOS 26 Tahoe | 3.11 | x86-64 | +| macOS 26 Tahoe | 3.12, 3.13, 3.14, 3.15, | arm64 | +| | PyPy3 | | +| +----------------------------+---------------------+ +| | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 24.04 LTS (Noble) | 3.11, 3.12, 3.13, | x86-64 | | | 3.14, 3.15, PyPy3 | |