From 6f86bad2cc825ee45696261c035345ff133e6b12 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:24:07 +0300 Subject: [PATCH 1/9] Drop support for Python 3.9 --- .ci/install.sh | 57 ++++------ .github/mergify.yml | 1 - .github/workflows/test-cygwin.yml | 150 ------------------------- .github/workflows/test-windows.yml | 4 +- .github/workflows/test.yml | 5 - README.md | 3 - Tests/helper.py | 9 +- Tests/test_features.py | 5 +- Tests/test_format_hsv.py | 5 +- Tests/test_image_transform.py | 5 +- Tests/test_imagechops.py | 6 +- Tests/test_imagedraw.py | 9 +- Tests/test_qt_image_qapplication.py | 46 ++++---- Tests/test_qt_image_toqimage.py | 8 +- Tests/test_shell_injection.py | 8 +- checks/check_imaging_leaks.py | 3 +- docs/index.rst | 4 - docs/installation/newer-versions.csv | 3 +- docs/installation/platform-support.rst | 8 +- docs/reference/internal_modules.rst | 5 - docs/releasenotes/12.0.0.rst | 6 + pyproject.toml | 9 +- src/PIL/GifImagePlugin.py | 4 +- src/PIL/GimpGradientFile.py | 6 +- src/PIL/ImageDraw.py | 12 +- src/PIL/ImageFilter.py | 7 +- src/PIL/ImageMath.py | 8 +- src/PIL/ImageQt.py | 21 ++-- src/PIL/ImageSequence.py | 6 +- src/PIL/PcfFontFile.py | 6 +- src/PIL/PdfParser.py | 17 +-- src/PIL/TiffImagePlugin.py | 10 +- src/PIL/_imagingft.pyi | 3 +- src/PIL/_typing.py | 19 +--- src/PIL/_util.py | 7 +- tox.ini | 4 +- 36 files changed, 169 insertions(+), 320 deletions(-) delete mode 100644 .github/workflows/test-cygwin.yml diff --git a/.ci/install.sh b/.ci/install.sh index acb84f046d1..a5c525b5661 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -13,18 +13,14 @@ aptget_update() return 1 fi } -if [[ $(uname) != CYGWIN* ]]; then - aptget_update || aptget_update retry || aptget_update retry -fi +aptget_update || aptget_update retry || aptget_update retry set -e -if [[ $(uname) != CYGWIN* ]]; then - sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ - ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ - cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ - sway wl-clipboard libopenblas-dev nasm -fi +sudo apt-get -qq install libfreetype6-dev liblcms2-dev libtiff-dev python3-tk\ + ghostscript libjpeg-turbo8-dev libopenjp2-7-dev\ + cmake meson imagemagick libharfbuzz-dev libfribidi-dev\ + sway wl-clipboard libopenblas-dev nasm python3 -m pip install --upgrade pip python3 -m pip install --upgrade wheel @@ -40,36 +36,27 @@ python3 -m pip install pyroma # fails on beta 3.14 and PyPy python3 -m pip install --only-binary=:all: pyarrow || true -if [[ $(uname) != CYGWIN* ]]; then - python3 -m pip install numpy - # PyQt6 doesn't support PyPy3 - if [[ $GHA_PYTHON_VERSION == 3.* ]]; then - sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 - # TODO Update condition when pyqt6 supports free-threading - if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi - fi +python3 -m pip install numpy - # Pyroma uses non-isolated build and fails with old setuptools - if [[ $GHA_PYTHON_VERSION == 3.9 ]]; then - # To match pyproject.toml - python3 -m pip install "setuptools>=77" - fi +# PyQt6 doesn't support PyPy3 +if [[ $GHA_PYTHON_VERSION == 3.* ]]; then + sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 + # TODO Update condition when pyqt6 supports free-threading + if ! [[ "$PYTHON_GIL" == "0" ]]; then python3 -m pip install pyqt6 ; fi +fi - # webp - pushd depends && ./install_webp.sh && popd +# webp +pushd depends && ./install_webp.sh && popd - # libimagequant - pushd depends && ./install_imagequant.sh && popd +# libimagequant +pushd depends && ./install_imagequant.sh && popd - # raqm - pushd depends && ./install_raqm.sh && popd +# raqm +pushd depends && ./install_raqm.sh && popd - # libavif - pushd depends && ./install_libavif.sh && popd +# libavif +pushd depends && ./install_libavif.sh && popd - # extra test images - pushd depends && ./install_extra_test_images.sh && popd -else - cd depends && ./install_extra_test_images.sh && cd .. -fi +# extra test images +pushd depends && ./install_extra_test_images.sh && popd diff --git a/.github/mergify.yml b/.github/mergify.yml index 9bb089615be..14222db1094 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -8,7 +8,6 @@ pull_request_rules: - status-success=Docker Test Successful - status-success=Windows Test Successful - status-success=MinGW - - status-success=Cygwin Test Successful actions: merge: method: merge diff --git a/.github/workflows/test-cygwin.yml b/.github/workflows/test-cygwin.yml deleted file mode 100644 index 581cd63704b..00000000000 --- a/.github/workflows/test-cygwin.yml +++ /dev/null @@ -1,150 +0,0 @@ -name: Test Cygwin - -on: - push: - branches: - - "**" - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - pull_request: - paths-ignore: - - ".github/workflows/docs.yml" - - ".github/workflows/wheels*" - - ".gitmodules" - - "docs/**" - - "wheels/**" - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - COVERAGE_CORE: sysmon - -jobs: - build: - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-minor-version: [9] - - timeout-minutes: 40 - - name: Python 3.${{ matrix.python-minor-version }} - - steps: - - name: Fix line endings - run: | - git config --global core.autocrlf input - - - name: Checkout Pillow - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Install Cygwin - uses: cygwin/cygwin-install-action@v6 - with: - packages: > - gcc-g++ - ghostscript - git - ImageMagick - jpeg - libfreetype-devel - libimagequant-devel - libjpeg-devel - liblapack-devel - liblcms2-devel - libopenjp2-devel - libraqm-devel - libtiff-devel - libwebp-devel - libxcb-devel - libxcb-xinerama0 - make - netpbm - perl - python3${{ matrix.python-minor-version }}-cython - python3${{ matrix.python-minor-version }}-devel - python3${{ matrix.python-minor-version }}-ipython - python3${{ matrix.python-minor-version }}-numpy - python3${{ matrix.python-minor-version }}-sip - python3${{ matrix.python-minor-version }}-tkinter - wget - xorg-server-extra - zlib-devel - - - name: Add Lapack to PATH - uses: egor-tensin/cleanup-path@v4 - with: - dirs: 'C:\cygwin\bin;C:\cygwin\lib\lapack' - - - name: pip cache - uses: actions/cache@v4 - with: - path: 'C:\cygwin\home\runneradmin\.cache\pip' - key: ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}-${{ hashFiles('.ci/install.sh') }} - restore-keys: | - ${{ runner.os }}-cygwin-pip3.${{ matrix.python-minor-version }}- - - - name: Build system information - run: | - dash.exe -c "python3 .github/workflows/system-info.py" - - - name: Install dependencies - run: | - bash.exe .ci/install.sh - - - name: Build - shell: bash.exe -eo pipefail -o igncr "{0}" - run: | - .ci/build.sh - - - name: Test - run: | - bash.exe xvfb-run -s '-screen 0 1024x768x24' .ci/test.sh - - - name: Prepare to upload errors - if: failure() - run: | - dash.exe -c "mkdir -p Tests/errors" - - - name: Upload errors - uses: actions/upload-artifact@v4 - if: failure() - with: - name: errors - path: Tests/errors - - - name: After success - run: | - bash.exe .ci/after_success.sh - rm C:\cygwin\bin\bash.EXE - - - name: Upload coverage - uses: codecov/codecov-action@v5 - with: - files: ./coverage.xml - flags: GHA_Cygwin - name: Cygwin Python 3.${{ matrix.python-minor-version }} - token: ${{ secrets.CODECOV_ORG_TOKEN }} - - success: - permissions: - contents: none - needs: build - runs-on: ubuntu-latest - name: Cygwin Test Successful - steps: - - name: Success - run: echo Cygwin Test Successful diff --git a/.github/workflows/test-windows.yml b/.github/workflows/test-windows.yml index 766c506e761..c80bb6eb602 100644 --- a/.github/workflows/test-windows.yml +++ b/.github/workflows/test-windows.yml @@ -35,11 +35,11 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["pypy3.11", "3.10", "3.11", "3.12", ">=3.13.5", "3.14"] + python-version: ["pypy3.11", "3.11", "3.12", "3.13", "3.14"] architecture: ["x64"] include: # Test the oldest Python on 32-bit - - { python-version: "3.9", architecture: "x86" } + - { python-version: "3.10", architecture: "x86" } timeout-minutes: 45 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d18023dbc97..967af2d69ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,6 @@ jobs: "3.12", "3.11", "3.10", - "3.9", ] include: - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } @@ -57,10 +56,6 @@ jobs: # Free-threaded - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } - # M1 only available for 3.10+ - - { os: "macos-13", python-version: "3.9" } - exclude: - - { os: "macos-latest", python-version: "3.9" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} diff --git a/README.md b/README.md index 365d356a00c..8585ef6cbd4 100644 --- a/README.md +++ b/README.md @@ -36,9 +36,6 @@ As of 2019, Pillow development is GitHub Actions build status (Test MinGW) - GitHub Actions build status (Test Cygwin) GitHub Actions build status (Test Docker) diff --git a/Tests/helper.py b/Tests/helper.py index df99f5f5571..e0dc8a9d4aa 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -10,17 +10,20 @@ import subprocess import sys import tempfile -from collections.abc import Sequence from functools import lru_cache from io import BytesIO -from pathlib import Path -from typing import Any, Callable import pytest from packaging.version import parse as parse_version from PIL import Image, ImageFile, ImageMath, features +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from pathlib import Path + from typing import Any + logger = logging.getLogger(__name__) uploader = None diff --git a/Tests/test_features.py b/Tests/test_features.py index 520c25b4645..d11a31ce3d2 100644 --- a/Tests/test_features.py +++ b/Tests/test_features.py @@ -2,7 +2,6 @@ import io import re -from typing import Callable import pytest @@ -10,6 +9,10 @@ from .helper import skip_unless_feature +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def test_check() -> None: # Check the correctness of the convenience function diff --git a/Tests/test_format_hsv.py b/Tests/test_format_hsv.py index 9cbf18566ca..861eccc1170 100644 --- a/Tests/test_format_hsv.py +++ b/Tests/test_format_hsv.py @@ -2,12 +2,15 @@ import colorsys import itertools -from typing import Callable from PIL import Image from .helper import assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + def int_to_float(i: int) -> float: return i / 255 diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 0429eb99d83..7cf52ddbabe 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,7 +1,6 @@ from __future__ import annotations import math -from typing import Callable import pytest @@ -9,6 +8,10 @@ from .helper import assert_image_equal, assert_image_similar, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class TestImageTransform: def test_sanity(self) -> None: diff --git a/Tests/test_imagechops.py b/Tests/test_imagechops.py index 4309214f5cc..61812ca7dc9 100644 --- a/Tests/test_imagechops.py +++ b/Tests/test_imagechops.py @@ -1,11 +1,13 @@ from __future__ import annotations -from typing import Callable - from PIL import Image, ImageChops from .helper import assert_image_equal, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + BLACK = (0, 0, 0) BROWN = (127, 64, 0) CYAN = (0, 255, 255) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index e1dcbc52c61..406d965b4e5 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1,13 +1,10 @@ from __future__ import annotations import os.path -from collections.abc import Sequence -from typing import Callable import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features -from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -17,6 +14,12 @@ skip_unless_feature, ) +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + + from PIL._typing import Coords + BLACK = (0, 0, 0) WHITE = (255, 255, 255) GRAY = (190, 190, 190) diff --git a/Tests/test_qt_image_qapplication.py b/Tests/test_qt_image_qapplication.py index 82a3e074120..b31e2a4ef25 100644 --- a/Tests/test_qt_image_qapplication.py +++ b/Tests/test_qt_image_qapplication.py @@ -1,8 +1,5 @@ from __future__ import annotations -from pathlib import Path -from typing import Union - import pytest from PIL import Image, ImageQt @@ -11,18 +8,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: - import PyQt6 - import PySide6 - - QApplication = Union[PyQt6.QtWidgets.QApplication, PySide6.QtWidgets.QApplication] - QHBoxLayout = Union[PyQt6.QtWidgets.QHBoxLayout, PySide6.QtWidgets.QHBoxLayout] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QLabel = Union[PyQt6.QtWidgets.QLabel, PySide6.QtWidgets.QLabel] - QPainter = Union[PyQt6.QtGui.QPainter, PySide6.QtGui.QPainter] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] - QPoint = Union[PyQt6.QtCore.QPoint, PySide6.QtCore.QPoint] - QRegion = Union[PyQt6.QtGui.QRegion, PySide6.QtGui.QRegion] - QWidget = Union[PyQt6.QtWidgets.QWidget, PySide6.QtWidgets.QWidget] + from pathlib import Path + if ImageQt.qt_is_installed: from PIL.ImageQt import QPixmap @@ -32,11 +19,16 @@ from PyQt6.QtGui import QImage, QPainter, QRegion from PyQt6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget elif ImageQt.qt_version == "side6": - from PySide6.QtCore import QPoint - from PySide6.QtGui import QImage, QPainter, QRegion - from PySide6.QtWidgets import QApplication, QHBoxLayout, QLabel, QWidget - - class Example(QWidget): # type: ignore[misc] + from PySide6.QtCore import QPoint # type: ignore[assignment] + from PySide6.QtGui import QImage, QPainter, QRegion # type: ignore[assignment] + from PySide6.QtWidgets import ( # type: ignore[assignment] + QApplication, + QHBoxLayout, + QLabel, + QWidget, + ) + + class Example(QWidget): def __init__(self) -> None: super().__init__() @@ -47,9 +39,9 @@ def __init__(self) -> None: pixmap1 = getattr(ImageQt.QPixmap, "fromImage")(qimage) # hbox - QHBoxLayout(self) # type: ignore[operator] + QHBoxLayout(self) - lbl = QLabel(self) # type: ignore[operator] + lbl = QLabel(self) # Segfault in the problem lbl.setPixmap(pixmap1.copy()) @@ -63,7 +55,7 @@ def roundtrip(expected: Image.Image) -> None: @pytest.mark.skipif(not ImageQt.qt_is_installed, reason="Qt bindings are not installed") def test_sanity(tmp_path: Path) -> None: # Segfault test - app: QApplication | None = QApplication([]) # type: ignore[operator] + app: QApplication | None = QApplication([]) ex = Example() assert app # Silence warning assert ex # Silence warning @@ -84,11 +76,11 @@ def test_sanity(tmp_path: Path) -> None: imageqt = ImageQt.ImageQt(im) data = getattr(QPixmap, "fromImage")(imageqt) qt_format = getattr(QImage, "Format") if ImageQt.qt_version == "6" else QImage - qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) # type: ignore[operator] - painter = QPainter(qimage) # type: ignore[operator] - image_label = QLabel() # type: ignore[operator] + qimage = QImage(128, 128, getattr(qt_format, "Format_ARGB32")) + painter = QPainter(qimage) + image_label = QLabel() image_label.setPixmap(data) - image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) # type: ignore[operator] + image_label.render(painter, QPoint(0, 0), QRegion(0, 0, 128, 128)) painter.end() rendered_tempfile = str(tmp_path / f"temp_rendered_{mode}.png") qimage.save(rendered_tempfile) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 8cb7ffb9b40..0004b552153 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path - import pytest from PIL import ImageQt from .helper import assert_image_equal, assert_image_equal_tofile, hopper +TYPE_CHECKING = False +if TYPE_CHECKING: + from pathlib import Path + pytestmark = pytest.mark.skipif( not ImageQt.qt_is_installed, reason="Qt bindings are not installed" ) @@ -21,7 +23,7 @@ def test_sanity(mode: str, tmp_path: Path) -> None: src = hopper(mode) data = ImageQt.toqimage(src) - assert isinstance(data, QImage) # type: ignore[arg-type, misc] + assert isinstance(data, QImage) assert not data.isNull() # reload directly from the qimage diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index 38d46f312ed..465517bb699 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -2,8 +2,6 @@ import shutil from io import BytesIO -from pathlib import Path -from typing import IO, Callable import pytest @@ -11,6 +9,12 @@ from .helper import djpeg_available, is_win32, netpbm_available +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from pathlib import Path + from typing import IO + TEST_JPG = "Tests/images/hopper.jpg" TEST_GIF = "Tests/images/hopper.gif" diff --git a/checks/check_imaging_leaks.py b/checks/check_imaging_leaks.py index 231789ca0a0..a1d59ed9c8b 100755 --- a/checks/check_imaging_leaks.py +++ b/checks/check_imaging_leaks.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 from __future__ import annotations -from typing import Any, Callable +from collections.abc import Callable +from typing import Any import pytest diff --git a/docs/index.rst b/docs/index.rst index 689088d48ce..ee51621ac7a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more = 11,Yes,Yes,Yes,Yes,Yes,,,, +Pillow 12,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,, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index c2227f1d29f..0026b42892a 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -44,20 +44,18 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 3.10, 3.11, | x86-64 | +| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, | x86-64 | | | 3.12, 3.13, 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ -| Windows Server 2022 | 3.9 | x86 | +| Windows Server 2022 | 3.10 | x86 | | +----------------------------+---------------------+ -| | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| | 3.11, 3.12, 3.13, | x86-64 | | | 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | -| +----------------------------+---------------------+ -| | 3.9 (Cygwin) | x86-64 | +----------------------------------+----------------------------+---------------------+ diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 19f78864d1d..41a8837b30a 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -53,11 +53,6 @@ on some Python versions. An object that supports the read method. -.. py:data:: TypeGuard - :value: typing.TypeGuard - - See :py:obj:`typing.TypeGuard`. - :mod:`~PIL._util` module ------------------------ diff --git a/docs/releasenotes/12.0.0.rst b/docs/releasenotes/12.0.0.rst index 6c0cd4dba01..5959ac6af1e 100644 --- a/docs/releasenotes/12.0.0.rst +++ b/docs/releasenotes/12.0.0.rst @@ -17,6 +17,12 @@ TODO Backwards incompatible changes ============================== +Python 3.9 +^^^^^^^^^^ + +Pillow has dropped support for Python 3.9, +which reached end-of-life in October 2025. + ImageFile.raise_oserror ^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/pyproject.toml b/pyproject.toml index 4e8623118ba..2e5346016ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,11 +20,10 @@ license-files = [ "LICENSE" ] authors = [ { name = "Jeffrey A. Clark", email = "aclark@aclark.net" }, ] -requires-python = ">=3.9" +requires-python = ">=3.10" classifiers = [ "Development Status :: 6 - Mature", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -75,9 +74,6 @@ optional-dependencies.tests = [ "trove-classifiers>=2024.10.12", ] -optional-dependencies.typing = [ - "typing-extensions; python_version<'3.10'", -] optional-dependencies.xmp = [ "defusedxml", ] @@ -190,6 +186,7 @@ lint.ignore = [ "PT017", # pytest-assert-in-except "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 + "UP038", # pyupgrade: deprecated rule ] lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [ "I002", @@ -215,7 +212,7 @@ testpaths = [ ] [tool.mypy] -python_version = "3.9" +python_version = "3.10" pretty = true disallow_any_generics = true enable_error_code = "ignore-without-code" diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index b03aa7f1505..0ca965d38a5 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, Union, cast +from typing import IO, Any, Literal, NamedTuple, cast from . import ( Image, @@ -535,7 +535,7 @@ def _normalize_mode(im: Image.Image) -> Image.Image: return im.convert("L") -_Palette = Union[bytes, bytearray, list[int], ImagePalette.ImagePalette] +_Palette = bytes | bytearray | list[int] | ImagePalette.ImagePalette def _normalize_palette( diff --git a/src/PIL/GimpGradientFile.py b/src/PIL/GimpGradientFile.py index ec62f8e4ebc..5f2691882c4 100644 --- a/src/PIL/GimpGradientFile.py +++ b/src/PIL/GimpGradientFile.py @@ -21,10 +21,14 @@ from __future__ import annotations from math import log, pi, sin, sqrt -from typing import IO, Callable from ._binary import o8 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import IO + EPSILON = 1e-10 """""" # Enable auto-doc for data member diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index e95fa91f8b3..ae092345ac2 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -33,21 +33,23 @@ import math import struct -from collections.abc import Sequence -from types import ModuleType -from typing import Any, AnyStr, Callable, Union, cast +from collections.abc import Callable, Sequence +from typing import cast from . import Image, ImageColor -from ._typing import Coords # experimental access to the outline API Outline: Callable[[], Image.core._Outline] = Image.core.outline TYPE_CHECKING = False if TYPE_CHECKING: + from types import ModuleType + from typing import Any, AnyStr + from . import ImageDraw2, ImageFont + from ._typing import Coords -_Ink = Union[float, tuple[int, ...], str] +_Ink = float | tuple[int, ...] | str """ A simple 2D drawing interface for PIL images. diff --git a/src/PIL/ImageFilter.py b/src/PIL/ImageFilter.py index b9ed54ab20a..9326eeeda9d 100644 --- a/src/PIL/ImageFilter.py +++ b/src/PIL/ImageFilter.py @@ -19,11 +19,14 @@ import abc import functools from collections.abc import Sequence -from types import ModuleType -from typing import Any, Callable, cast +from typing import cast TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable + from types import ModuleType + from typing import Any + from . import _imaging from ._typing import NumpyArray diff --git a/src/PIL/ImageMath.py b/src/PIL/ImageMath.py index d2504b1ae5a..dfdc50c0552 100644 --- a/src/PIL/ImageMath.py +++ b/src/PIL/ImageMath.py @@ -17,11 +17,15 @@ from __future__ import annotations import builtins -from types import CodeType -from typing import Any, Callable from . import Image, _imagingmath +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from types import CodeType + from typing import Any + class _Operand: """Wraps an image operand, providing standard operators""" diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index df7a57b652c..af4d0742d6b 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,23 +19,18 @@ import sys from io import BytesIO -from typing import Any, Callable, Union from . import Image from ._util import is_path TYPE_CHECKING = False if TYPE_CHECKING: - import PyQt6 - import PySide6 + from collections.abc import Callable + from typing import Any from . import ImageFile QBuffer: type - QByteArray = Union[PyQt6.QtCore.QByteArray, PySide6.QtCore.QByteArray] - QIODevice = Union[PyQt6.QtCore.QIODevice, PySide6.QtCore.QIODevice] - QImage = Union[PyQt6.QtGui.QImage, PySide6.QtGui.QImage] - QPixmap = Union[PyQt6.QtGui.QPixmap, PySide6.QtGui.QPixmap] qt_version: str | None qt_versions = [ @@ -49,11 +44,15 @@ try: qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": - from PyQt6.QtCore import QBuffer, QIODevice + from PyQt6.QtCore import QBuffer, QByteArray, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba elif qt_module == "PySide6": - from PySide6.QtCore import QBuffer, QIODevice - from PySide6.QtGui import QImage, QPixmap, qRgba + from PySide6.QtCore import ( # type: ignore[assignment] + QBuffer, + QByteArray, + QIODevice, + ) + from PySide6.QtGui import QImage, QPixmap, qRgba # type: ignore[assignment] except (ImportError, RuntimeError): continue qt_is_installed = True @@ -183,7 +182,7 @@ def _toqclass_helper(im: Image.Image | str | QByteArray) -> dict[str, Any]: if qt_is_installed: - class ImageQt(QImage): # type: ignore[misc] + class ImageQt(QImage): def __init__(self, im: Image.Image | str | QByteArray) -> None: """ An PIL image wrapper for Qt. This is a subclass of PyQt's QImage diff --git a/src/PIL/ImageSequence.py b/src/PIL/ImageSequence.py index a6fc340d55f..361be48971e 100644 --- a/src/PIL/ImageSequence.py +++ b/src/PIL/ImageSequence.py @@ -16,10 +16,12 @@ ## from __future__ import annotations -from typing import Callable - from . import Image +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + class Iterator: """ diff --git a/src/PIL/PcfFontFile.py b/src/PIL/PcfFontFile.py index 0d1968b140a..a00e9b91984 100644 --- a/src/PIL/PcfFontFile.py +++ b/src/PIL/PcfFontFile.py @@ -18,7 +18,6 @@ from __future__ import annotations import io -from typing import BinaryIO, Callable from . import FontFile, Image from ._binary import i8 @@ -27,6 +26,11 @@ from ._binary import i32be as b32 from ._binary import i32le as l32 +TYPE_CHECKING = False +if TYPE_CHECKING: + from collections.abc import Callable + from typing import BinaryIO + # -------------------------------------------------------------------- # declarations diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 73d8c21c023..2c9031469ad 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,7 +8,15 @@ import re import time import zlib -from typing import IO, Any, NamedTuple, Union +from typing import Any, NamedTuple + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import IO + + _DictBase = collections.UserDict[str | bytes, Any] +else: + _DictBase = collections.UserDict # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -251,13 +259,6 @@ def __bytes__(self) -> bytes: return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -TYPE_CHECKING = False -if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] -else: - _DictBase = collections.UserDict - - class PdfDict(_DictBase): def __setattr__(self, key: str, value: Any) -> None: if key == "data": diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index c1850f084c0..c1741284b9f 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,22 +47,24 @@ import os import struct import warnings -from collections.abc import Iterator, MutableMapping +from collections.abc import Callable, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, Any, Callable, NoReturn, cast +from typing import IO, Any, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 -from ._typing import StrOrBytesPath from ._util import DeferredError, is_path from .TiffTags import TYPES TYPE_CHECKING = False if TYPE_CHECKING: - from ._typing import Buffer, IntegralLike + from collections.abc import Iterator + from typing import NoReturn + + from ._typing import Buffer, IntegralLike, StrOrBytesPath logger = logging.getLogger(__name__) diff --git a/src/PIL/_imagingft.pyi b/src/PIL/_imagingft.pyi index 1cb1429d6cf..2136810ba6a 100644 --- a/src/PIL/_imagingft.pyi +++ b/src/PIL/_imagingft.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any from . import ImageFont, _imaging diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 373938e71e0..e568a469bd8 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -3,7 +3,7 @@ import os import sys from collections.abc import Sequence -from typing import Any, Protocol, TypeVar, Union +from typing import Any, Protocol, TypeVar TYPE_CHECKING = False if TYPE_CHECKING: @@ -26,19 +26,8 @@ else: Buffer = Any -if sys.version_info >= (3, 10): - from typing import TypeGuard -else: - try: - from typing_extensions import TypeGuard - except ImportError: - - class TypeGuard: # type: ignore[no-redef] - def __class_getitem__(cls, item: Any) -> type[bool]: - return bool - -Coords = Union[Sequence[float], Sequence[Sequence[float]]] +Coords = Sequence[float] | Sequence[Sequence[float]] _T_co = TypeVar("_T_co", covariant=True) @@ -48,7 +37,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, length: int = ..., /) -> _T_co: ... -StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] +StrOrBytesPath = str | bytes | os.PathLike[str] | os.PathLike[bytes] -__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"] +__all__ = ["Buffer", "IntegralLike", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 8ef0d36f754..b1fa6a0f39e 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,9 +1,12 @@ from __future__ import annotations import os -from typing import Any, NoReturn -from ._typing import StrOrBytesPath, TypeGuard +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Any, NoReturn, TypeGuard + + from ._typing import StrOrBytesPath def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: diff --git a/tox.ini b/tox.ini index 967d4b53768..8933945b1af 100644 --- a/tox.ini +++ b/tox.ini @@ -3,7 +3,7 @@ requires = tox>=4.2 env_list = lint - py{py3, 314, 313, 312, 311, 310, 39} + py{py3, 314, 313, 312, 311, 310} [testenv] deps = @@ -29,7 +29,5 @@ commands = skip_install = true deps = -r .ci/requirements-mypy.txt -extras = - typing commands = mypy conftest.py selftest.py setup.py docs src winbuild Tests {posargs} From c688d40bf3620c44593563bf6b17b3aad1e1d1ad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 30 Jul 2025 20:04:03 +1000 Subject: [PATCH 2/9] Python 3.9 is no longer tested on macOS --- docs/installation/platform-support.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index 0026b42892a..f8d6714d04c 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -37,8 +37,6 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 13 Ventura | 3.9 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | | | 3.14, PyPy3 | | +----------------------------------+----------------------------+---------------------+ From b0542eae1204e4073b4575f49ddfbc2d2afb8339 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 11:15:16 +1000 Subject: [PATCH 3/9] Apply PYI026 --- pyproject.toml | 1 - src/PIL/_imagingcms.pyi | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9d7ea03116..137726a1c04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -185,7 +185,6 @@ lint.ignore = [ "PT011", # pytest-raises-too-broad "PT012", # pytest-raises-with-multiple-statements "PT017", # pytest-assert-in-except - "PYI026", # flake8-pyi: typing.TypeAlias added in Python 3.10 "PYI034", # flake8-pyi: typing.Self added in Python 3.11 "UP038", # pyupgrade: deprecated rule ] diff --git a/src/PIL/_imagingcms.pyi b/src/PIL/_imagingcms.pyi index ddcf93ab1eb..4fc0d60ab79 100644 --- a/src/PIL/_imagingcms.pyi +++ b/src/PIL/_imagingcms.pyi @@ -1,14 +1,14 @@ import datetime import sys -from typing import Literal, SupportsFloat, TypedDict +from typing import Literal, SupportsFloat, TypeAlias, TypedDict from ._typing import CapsuleType littlecms_version: str | None -_Tuple3f = tuple[float, float, float] -_Tuple2x3f = tuple[_Tuple3f, _Tuple3f] -_Tuple3x3f = tuple[_Tuple3f, _Tuple3f, _Tuple3f] +_Tuple3f: TypeAlias = tuple[float, float, float] +_Tuple2x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f] +_Tuple3x3f: TypeAlias = tuple[_Tuple3f, _Tuple3f, _Tuple3f] class _IccMeasurementCondition(TypedDict): observer: int From ed22b7bc78f4f4bace61a5e825066661f61cbae1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 11:19:06 +1000 Subject: [PATCH 4/9] Updated match --- Tests/test_imagecms.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py index 8b5d88ac883..1953458bcae 100644 --- a/Tests/test_imagecms.py +++ b/Tests/test_imagecms.py @@ -208,9 +208,10 @@ def test_exceptions() -> None: ImageCms.getProfileName(None) # type: ignore[arg-type] skip_missing() - # Python <= 3.9: "an integer is required (got type NoneType)" - # Python > 3.9: "'NoneType' object cannot be interpreted as an integer" - with pytest.raises(ImageCms.PyCMSError, match="integer"): + with pytest.raises( + ImageCms.PyCMSError, + match="'NoneType' object cannot be interpreted as an integer", + ): ImageCms.isIntentSupported(SRGB, None, None) # type: ignore[arg-type] From 002c085d49545130020a35fe7e7208eddb6a1a69 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 11:20:26 +1000 Subject: [PATCH 5/9] Moved install into alphabetical order --- .ci/install.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.ci/install.sh b/.ci/install.sh index a5c525b5661..2178c664626 100755 --- a/.ci/install.sh +++ b/.ci/install.sh @@ -27,6 +27,7 @@ python3 -m pip install --upgrade wheel python3 -m pip install coverage python3 -m pip install defusedxml python3 -m pip install ipython +python3 -m pip install numpy python3 -m pip install olefile python3 -m pip install -U pytest python3 -m pip install -U pytest-cov @@ -36,9 +37,6 @@ python3 -m pip install pyroma # fails on beta 3.14 and PyPy python3 -m pip install --only-binary=:all: pyarrow || true - -python3 -m pip install numpy - # PyQt6 doesn't support PyPy3 if [[ $GHA_PYTHON_VERSION == 3.* ]]; then sudo apt-get -qq install libegl1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxkbcommon-x11-0 From 83685110fd8d80ab4475957806f7cb5573291fa1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 13:43:35 +1000 Subject: [PATCH 6/9] Updated CI targets --- docs/installation/platform-support.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index f8d6714d04c..c7875914e20 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -19,13 +19,13 @@ These platforms are built and tested for every change. +==================================+============================+=====================+ | Alpine | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2 | 3.9 | x86-64 | +| Amazon Linux 2 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Amazon Linux 2023 | 3.9 | x86-64 | +| Amazon Linux 2023 | 3.11 | x86-64 | +----------------------------------+----------------------------+---------------------+ | Arch | 3.13 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| CentOS Stream 9 | 3.9 | x86-64 | +| CentOS Stream 9 | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ | CentOS Stream 10 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ @@ -42,16 +42,16 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, | x86-64 | -| | 3.12, 3.13, 3.14, PyPy3 | | +| Ubuntu Linux 24.04 LTS (Noble) | 3.10, 3.11, 3.12, 3.13, | x86-64 | +| | 3.14, PyPy3 | | | +----------------------------+---------------------+ | | 3.12 | arm64v8, ppc64le, | | | | s390x | +----------------------------------+----------------------------+---------------------+ | Windows Server 2022 | 3.10 | x86 | | +----------------------------+---------------------+ -| | 3.11, 3.12, 3.13, | x86-64 | -| | 3.14, PyPy3 | | +| | 3.11, 3.12, 3.13, 3.14, | x86-64 | +| | PyPy3 | | | +----------------------------+---------------------+ | | 3.12 (MinGW) | x86-64 | +----------------------------------+----------------------------+---------------------+ From 60fb0fc9388bea906ee083ad1c7e6198f49d4852 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 12:27:44 +1000 Subject: [PATCH 7/9] Rearranged imports --- src/PIL/GifImagePlugin.py | 4 +++- src/PIL/ImageDraw.py | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 0ca965d38a5..58c460ef3db 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, Any, Literal, NamedTuple, cast +from typing import Any, NamedTuple, cast from . import ( Image, @@ -49,6 +49,8 @@ TYPE_CHECKING = False if TYPE_CHECKING: + from typing import IO, Literal + from . import _imaging from ._typing import Buffer diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index ae092345ac2..ed46899b455 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -33,22 +33,23 @@ import math import struct -from collections.abc import Callable, Sequence +from collections.abc import Sequence from typing import cast from . import Image, ImageColor -# experimental access to the outline API -Outline: Callable[[], Image.core._Outline] = Image.core.outline - TYPE_CHECKING = False if TYPE_CHECKING: + from collections.abc import Callable from types import ModuleType from typing import Any, AnyStr from . import ImageDraw2, ImageFont from ._typing import Coords +# experimental access to the outline API +Outline: Callable[[], Image.core._Outline] = Image.core.outline + _Ink = float | tuple[int, ...] | str """ From 97beaee58e76c4dcf6991a49e4a3ded3c3913ad7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 2 Aug 2025 11:40:03 +0300 Subject: [PATCH 8/9] Test macOS Intel with Python 3.10 --- .github/workflows/test.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 967af2d69ca..c075f04d7cc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,11 +51,15 @@ jobs: "3.10", ] include: - - { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } - - { python-version: "3.10", PYTHONOPTIMIZE: 2 } + - { python-version: "3.12", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" } + - { python-version: "3.11", PYTHONOPTIMIZE: 2 } # Free-threaded - { python-version: "3.14t", disable-gil: true } - { python-version: "3.13t", disable-gil: true } + # Intel + - { os: "macos-13", python-version: "3.10" } + exclude: + - { os: "macos-latest", python-version: "3.10" } runs-on: ${{ matrix.os }} name: ${{ matrix.os }} Python ${{ matrix.python-version }} From eeda633439ca62f6e2ac5d7e94d9c4cee1e402cc Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 2 Aug 2025 20:12:49 +1000 Subject: [PATCH 9/9] Updated CI targets --- docs/installation/platform-support.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index c7875914e20..5cf0276d188 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -37,8 +37,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Gentoo | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ -| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 | -| | 3.14, PyPy3 | | +| macOS 13 Ventura | 3.10 | x86-64 | ++----------------------------------+----------------------------+---------------------+ +| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 | +| | PyPy3 | | +----------------------------------+----------------------------+---------------------+ | Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 | +----------------------------------+----------------------------+---------------------+