Drop support for Python 3.9 (#9119)

Co-authored-by: Andrew Murray <radarhere@users.noreply.github.com>
Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
This commit is contained in:
Hugo van Kemenade 2025-08-02 15:02:20 +03:00 committed by GitHub
parent eb59176b09
commit 2ab301dcc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 196 additions and 343 deletions

View File

@ -13,24 +13,21 @@ 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
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
@ -40,36 +37,24 @@ 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
# 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
# webp
pushd depends && ./install_webp.sh && popd
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && ./install_raqm.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 ..
# 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
# libimagequant
pushd depends && ./install_imagequant.sh && popd
# raqm
pushd depends && ./install_raqm.sh && popd
# libavif
pushd depends && ./install_libavif.sh && popd
# extra test images
pushd depends && ./install_extra_test_images.sh && popd

1
.github/mergify.yml vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -49,18 +49,17 @@ jobs:
"3.12",
"3.11",
"3.10",
"3.9",
]
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 }
# M1 only available for 3.10+
- { os: "macos-13", python-version: "3.9" }
# Intel
- { os: "macos-13", python-version: "3.10" }
exclude:
- { os: "macos-latest", python-version: "3.9" }
- { os: "macos-latest", python-version: "3.10" }
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}

View File

@ -36,9 +36,6 @@ As of 2019, Pillow development is
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml"><img
alt="GitHub Actions build status (Test MinGW)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20MinGW/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml"><img
alt="GitHub Actions build status (Test Cygwin)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg"></a>
<a href="https://github.com/python-pillow/Pillow/actions/workflows/test-docker.yml"><img
alt="GitHub Actions build status (Test Docker)"
src="https://github.com/python-pillow/Pillow/workflows/Test%20Docker/badge.svg"></a>

View File

@ -10,17 +10,20 @@ import shutil
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

View File

@ -2,7 +2,6 @@ from __future__ import annotations
import io
import re
from typing import Callable
import pytest
@ -10,6 +9,10 @@ from PIL import features
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

View File

@ -2,12 +2,15 @@ from __future__ import annotations
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

View File

@ -1,7 +1,6 @@
from __future__ import annotations
import math
from typing import Callable
import pytest
@ -9,6 +8,10 @@ from PIL import Image, ImageTransform
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:

View File

@ -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)

View File

@ -211,9 +211,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]

View File

@ -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 @@ from .helper import (
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)

View File

@ -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 @@ from .helper import assert_image_equal_tofile, assert_image_similar, hopper
TYPE_CHECKING = False
if TYPE_CHECKING:
import PyQt6
import PySide6
from pathlib import Path
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]
if ImageQt.qt_is_installed:
from PIL.ImageQt import QPixmap
@ -32,11 +19,16 @@ if ImageQt.qt_is_installed:
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
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): # type: ignore[misc]
class Example(QWidget):
def __init__(self) -> None:
super().__init__()
@ -47,9 +39,9 @@ if ImageQt.qt_is_installed:
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)

View File

@ -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

View File

@ -2,8 +2,6 @@ from __future__ import annotations
import shutil
from io import BytesIO
from pathlib import Path
from typing import IO, Callable
import pytest
@ -11,6 +9,12 @@ from PIL import GifImagePlugin, Image, JpegImagePlugin
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"

View File

@ -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

View File

@ -29,10 +29,6 @@ Pillow for enterprise is available via the Tidelift Subscription. `Learn more <h
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-mingw.yml
:alt: GitHub Actions build status (Test MinGW)
.. image:: https://github.com/python-pillow/Pillow/workflows/Test%20Cygwin/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/test-cygwin.yml
:alt: GitHub Actions build status (Test Cygwin)
.. image:: https://github.com/python-pillow/Pillow/workflows/Wheels/badge.svg
:target: https://github.com/python-pillow/Pillow/actions/workflows/wheels.yml
:alt: GitHub Actions build status (Wheels)

View File

@ -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 |
+----------------------------------+----------------------------+---------------------+
@ -37,27 +37,25 @@ These platforms are built and tested for every change.
+----------------------------------+----------------------------+---------------------+
| Gentoo | 3.12 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 13 Ventura | 3.9 | x86-64 |
| macOS 13 Ventura | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| macOS 14 Sonoma | 3.10, 3.11, 3.12, 3.13, | arm64 |
| | 3.14, PyPy3 | |
| macOS 14 Sonoma | 3.11, 3.12, 3.13, 3.14 | arm64 |
| | PyPy3 | |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 22.04 LTS (Jammy) | 3.10 | x86-64 |
+----------------------------------+----------------------------+---------------------+
| Ubuntu Linux 24.04 LTS (Noble) | 3.9, 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.9 | x86 |
| Windows Server 2022 | 3.10 | x86 |
| +----------------------------+---------------------+
| | 3.10, 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 |
| +----------------------------+---------------------+
| | 3.9 (Cygwin) | x86-64 |
+----------------------------------+----------------------------+---------------------+

View File

@ -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
------------------------

View File

@ -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
^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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",
@ -76,9 +75,6 @@ optional-dependencies.tests = [
"trove-classifiers>=2024.10.12",
]
optional-dependencies.typing = [
"typing-extensions; python_version<'3.10'",
]
optional-dependencies.xmp = [
"defusedxml",
]
@ -189,8 +185,8 @@ 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
]
lint.per-file-ignores."Tests/oss-fuzz/fuzz_font.py" = [
"I002",
@ -216,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"

View File

@ -31,7 +31,7 @@ import os
import subprocess
from enum import IntEnum
from functools import cached_property
from typing import IO, Any, Literal, NamedTuple, Union, cast
from typing import Any, NamedTuple, cast
from . import (
Image,
@ -49,6 +49,8 @@ from ._util import DeferredError
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO, Literal
from . import _imaging
from ._typing import Buffer
@ -535,7 +537,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(

View File

@ -21,10 +21,14 @@ See the GIMP distribution for more information.)
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

View File

@ -34,20 +34,23 @@ from __future__ import annotations
import math
import struct
from collections.abc import Sequence
from types import ModuleType
from typing import Any, AnyStr, Callable, Union, cast
from typing import cast
from . import Image, ImageColor
from ._typing import Coords
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
TYPE_CHECKING = False
if TYPE_CHECKING:
from . import ImageDraw2, ImageFont
_Ink = Union[float, tuple[int, ...], str]
_Ink = float | tuple[int, ...] | str
"""
A simple 2D drawing interface for PIL images.

View File

@ -19,11 +19,14 @@ from __future__ import annotations
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

View File

@ -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"""

View File

@ -19,23 +19,18 @@ from __future__ import annotations
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 @@ for version, qt_module in qt_versions:
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

View File

@ -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:
"""

View File

@ -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 i16le as l16
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

View File

@ -8,7 +8,15 @@ import os
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 @@ class PdfArray(list[Any]):
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":

View File

@ -47,22 +47,24 @@ import math
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__)

View File

@ -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

View File

@ -1,4 +1,5 @@
from typing import Any, Callable
from collections.abc import Callable
from typing import Any
from . import ImageFont, _imaging

View File

@ -3,7 +3,7 @@ from __future__ import annotations
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 @@ if sys.version_info >= (3, 12):
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"]

View File

@ -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]:

View File

@ -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}