Skip to content

Commit

Permalink
Merge branch 'main' into progress
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere authored Jul 16, 2024
2 parents 26e2a9f + f19e07b commit 8b4b7ce
Show file tree
Hide file tree
Showing 75 changed files with 2,363 additions and 516 deletions.
10 changes: 8 additions & 2 deletions .ci/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,18 @@ python3 -m pip install -U pytest-timeout
python3 -m pip install pyroma

if [[ $(uname) != CYGWIN* ]]; then
python3 -m pip install numpy
# TODO Update condition when NumPy supports free-threading
if [[ "$PYTHON_GIL" == "0" ]]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
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
python3 -m pip install pyqt6
# 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
Expand Down
30 changes: 20 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,24 @@ jobs:
"3.9",
]
include:
- python-version: "3.11"
PYTHONOPTIMIZE: 1
REVERSE: "--reverse"
- python-version: "3.10"
PYTHONOPTIMIZE: 2
- { python-version: "3.11", PYTHONOPTIMIZE: 1, REVERSE: "--reverse" }
- { python-version: "3.10", PYTHONOPTIMIZE: 2 }
# Free-threaded
- { os: "ubuntu-latest", python-version: "3.13-dev", disable-gil: true }
# M1 only available for 3.10+
- os: "macos-13"
python-version: "3.9"
- { os: "macos-13", python-version: "3.9" }
exclude:
- os: "macos-14"
python-version: "3.9"
- { os: "macos-14", python-version: "3.9" }

runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }} ${{ matrix.disable-gil && 'free-threaded' || '' }}

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
if: "${{ !matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
allow-prereleases: true
Expand All @@ -78,6 +76,18 @@ jobs:
".ci/*.sh"
"pyproject.toml"
- name: Set up Python ${{ matrix.python-version }} (free-threaded)
uses: deadsnakes/action@v3.1.0
if: "${{ matrix.disable-gil }}"
with:
python-version: ${{ matrix.python-version }}
nogil: ${{ matrix.disable-gil }}

- name: Set PYTHON_GIL
if: "${{ matrix.disable-gil }}"
run: |
echo "PYTHON_GIL=0" >> $GITHUB_ENV
- name: Build system information
run: python3 .github/workflows/system-info.py

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/wheels-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else
yum install -y fribidi
fi

if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
# TODO Update condition when NumPy supports free-threading
if [ $(python3 -c "import sysconfig;print(sysconfig.get_config_var('Py_GIL_DISABLED'))") == "1" ]; then
python3 -m pip install numpy --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
else
python3 -m pip install numpy
fi
fi

if [ ! -d "test-images-main" ]; then
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,8 @@ jobs:
python-version:
- pp39
- pp310
- cp39
- cp310
- cp311
- cp312
- cp313
- cp3{9,10,11}
- cp3{12,13}
spec:
- manylinux2014
- manylinux_2_28
Expand Down Expand Up @@ -132,6 +129,7 @@ jobs:
env:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BUILD: ${{ matrix.build }}
CIBW_FREE_THREADED_SUPPORT: True
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
Expand Down Expand Up @@ -204,6 +202,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_FREE_THREADED_SUPPORT: True
CIBW_PRERELEASE_PYTHONS: True
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
Expand Down
15 changes: 15 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
Changelog (Pillow)
==================

11.0.0 (unreleased)
-------------------

- Drop support for Python 3.8 #8183
[hugovk, radarhere]

- Add support for Python 3.13 #8181
[hugovk, radarhere]

- Fix incompatibility with NumPy 1.20 #8187
[neutrinoceros, radarhere]

- Remove PSFile, PyAccess and USE_CFFI_ACCESS #8182
[hugovk, radarhere]

10.4.0 (2024-07-01)
-------------------

Expand Down
4 changes: 1 addition & 3 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ def convert_to_comparable(
return new_a, new_b


def assert_deep_equal(
a: Sequence[Any], b: Sequence[Any], msg: str | None = None
) -> None:
def assert_deep_equal(a: Any, b: Any, msg: str | None = None) -> None:
try:
assert len(a) == len(b), msg or f"got length {len(a)}, expected {len(b)}"
except Exception:
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ def test_palette_434(tmp_path: Path) -> None:

def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image:
out = str(tmp_path / "temp.gif")
im.copy().save(out, **kwargs)
im.copy().save(out, "GIF", **kwargs)
reloaded = Image.open(out)

return reloaded
Expand Down
27 changes: 21 additions & 6 deletions Tests/test_file_libtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,22 @@ def test_g4_tiff_bytesio(self, tmp_path: Path) -> None:
def test_g4_non_disk_file_object(self, tmp_path: Path) -> None:
"""Testing loading from non-disk non-BytesIO file object"""
test_file = "Tests/images/hopper_g4_500.tif"
s = io.BytesIO()
with open(test_file, "rb") as f:
s.write(f.read())
s.seek(0)
r = io.BufferedReader(s)
data = f.read()

class NonBytesIO(io.RawIOBase):
def read(self, size: int = -1) -> bytes:
nonlocal data
if size == -1:
size = len(data)
result = data[:size]
data = data[size:]
return result

def readable(self) -> bool:
return True

r = io.BufferedReader(NonBytesIO())
with Image.open(r) as im:
assert im.size == (500, 500)
self._assert_noerr(tmp_path, im)
Expand Down Expand Up @@ -1048,7 +1059,11 @@ def test_open_missing_samplesperpixel(self) -> None:
],
)
def test_wrong_bits_per_sample(
self, file_name: str, mode: str, size: tuple[int, int], tile
self,
file_name: str,
mode: str,
size: tuple[int, int],
tile: list[tuple[str, tuple[int, int, int, int], int, tuple[Any, ...]]],
) -> None:
with Image.open("Tests/images/" + file_name) as im:
assert im.mode == mode
Expand Down Expand Up @@ -1135,7 +1150,7 @@ def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None:
arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"}
if argument:
arguments["strip_size"] = 2**18
im.save(out, **arguments)
im.save(out, "TIFF", **arguments)

with Image.open(out) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
Expand Down
16 changes: 12 additions & 4 deletions Tests/test_file_mpo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

import warnings
from io import BytesIO
from typing import Any, cast
from typing import Any

import pytest

from PIL import Image, MpoImagePlugin
from PIL import Image, ImageFile, MpoImagePlugin

from .helper import (
assert_image_equal,
Expand All @@ -20,11 +20,11 @@
pytestmark = skip_unless_feature("jpg")


def roundtrip(im: Image.Image, **options: Any) -> MpoImagePlugin.MpoImageFile:
def roundtrip(im: Image.Image, **options: Any) -> ImageFile.ImageFile:
out = BytesIO()
im.save(out, "MPO", **options)
out.seek(0)
return cast(MpoImagePlugin.MpoImageFile, Image.open(out))
return Image.open(out)


@pytest.mark.parametrize("test_file", test_files)
Expand Down Expand Up @@ -226,6 +226,12 @@ def test_eoferror() -> None:
im.seek(n_frames - 1)


def test_adopt_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
with pytest.raises(ValueError):
MpoImagePlugin.MpoImageFile.adopt(im)


def test_ultra_hdr() -> None:
with Image.open("Tests/images/ultrahdr.jpg") as im:
assert im.format == "JPEG"
Expand Down Expand Up @@ -275,6 +281,8 @@ def test_save_all() -> None:
im_reloaded = roundtrip(im, save_all=True, append_images=[im2])

assert_image_equal(im, im_reloaded)
assert isinstance(im_reloaded, MpoImagePlugin.MpoImageFile)
assert im_reloaded.mpinfo is not None
assert im_reloaded.mpinfo[45056] == b"0100"

im_reloaded.seek(1)
Expand Down
3 changes: 2 additions & 1 deletion Tests/test_file_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def test_dpi(params: dict[str, int | tuple[int, int]], tmp_path: Path) -> None:
im = hopper()

outfile = str(tmp_path / "temp.pdf")
im.save(outfile, **params)
im.save(outfile, "PDF", **params)

with open(outfile, "rb") as fp:
contents = fp.read()
Expand Down Expand Up @@ -271,6 +271,7 @@ def test_pdf_append_fails_on_nonexistent_file() -> None:


def check_pdf_pages_consistency(pdf: PdfParser.PdfParser) -> None:
assert pdf.pages_ref is not None
pages_info = pdf.read_indirect(pdf.pages_ref)
assert b"Parent" not in pages_info
assert b"Kids" in pages_info
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@

def chunk(cid: bytes, *data: bytes) -> bytes:
test_file = BytesIO()
PngImagePlugin.putchunk(*(test_file, cid) + data)
PngImagePlugin.putchunk(test_file, cid, *data)
return test_file.getvalue()


Expand Down
22 changes: 13 additions & 9 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def test_closed_file(self) -> None:

def test_seek_after_close(self) -> None:
im = Image.open("Tests/images/multipage.tiff")
assert isinstance(im, TiffImagePlugin.TiffImageFile)
im.close()

with pytest.raises(ValueError):
Expand Down Expand Up @@ -424,13 +425,13 @@ def test_load_string(self) -> None:
def test_load_float(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdabcd"
ret = ifd.load_float(data, False)
ret = getattr(ifd, "load_float")(data, False)
assert ret == (1.6777999408082104e22, 1.6777999408082104e22)

def test_load_double(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2()
data = b"abcdefghabcdefgh"
ret = ifd.load_double(data, False)
ret = getattr(ifd, "load_double")(data, False)
assert ret == (8.540883223036124e194, 8.540883223036124e194)

def test_ifd_tag_type(self) -> None:
Expand Down Expand Up @@ -599,7 +600,7 @@ def test_gray_semibyte_per_pixel(self) -> None:
def test_with_underscores(self, tmp_path: Path) -> None:
kwargs = {"resolution_unit": "inch", "x_resolution": 72, "y_resolution": 36}
filename = str(tmp_path / "temp.tif")
hopper("RGB").save(filename, **kwargs)
hopper("RGB").save(filename, "TIFF", **kwargs)
with Image.open(filename) as im:
# legacy interface
assert im.tag[X_RESOLUTION][0][0] == 72
Expand All @@ -624,14 +625,17 @@ def test_roundtrip_tiff_uint16(self, tmp_path: Path) -> None:
def test_iptc(self, tmp_path: Path) -> None:
# Do not preserve IPTC_NAA_CHUNK by default if type is LONG
outfile = str(tmp_path / "temp.tif")
im = hopper()
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)
with Image.open("Tests/images/hopper.tif") as im:
im.load()
assert isinstance(im, TiffImagePlugin.TiffImageFile)
ifd = TiffImagePlugin.ImageFileDirectory_v2()
ifd[33723] = 1
ifd.tagtype[33723] = 4
im.tag_v2 = ifd
im.save(outfile)

with Image.open(outfile) as im:
assert isinstance(im, TiffImagePlugin.TiffImageFile)
assert 33723 not in im.tag_v2

def test_rowsperstrip(self, tmp_path: Path) -> None:
Expand Down
5 changes: 4 additions & 1 deletion Tests/test_file_webp_animated.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections.abc import Generator
from pathlib import Path

import pytest
Expand Down Expand Up @@ -96,7 +97,9 @@ def check(temp_file: str) -> None:
check(temp_file1)

# Tests appending using a generator
def im_generator(ims):
def im_generator(
ims: list[Image.Image],
) -> Generator[Image.Image, None, None]:
yield from ims

temp_file2 = str(tmp_path / "temp_generator.webp")
Expand Down
11 changes: 7 additions & 4 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,9 @@ def test_alpha_composite(self) -> None:
img = Image.alpha_composite(dst, src)

# Assert
img_colors = sorted(img.getcolors())
assert img_colors == expected_colors
img_colors = img.getcolors()
assert img_colors is not None
assert sorted(img_colors) == expected_colors

def test_alpha_inplace(self) -> None:
src = Image.new("RGBA", (128, 128), "blue")
Expand Down Expand Up @@ -670,7 +671,9 @@ def test_remap_palette_transparency(self) -> None:

im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
assert len(im_remapped.getpalette()) == 6
palette = im_remapped.getpalette()
assert palette is not None
assert len(palette) == 6

# Test unused transparency
im.info["transparency"] = 2
Expand Down Expand Up @@ -701,7 +704,7 @@ def _make_new(
else:
assert new_image.palette is None

_make_new(im, im_p, ImagePalette.ImagePalette(list(range(256)) * 3))
_make_new(im, im_p, ImagePalette.ImagePalette("RGB"))
_make_new(im_p, im, None)
_make_new(im, blank_p, ImagePalette.ImagePalette())
_make_new(im, blank_pa, ImagePalette.ImagePalette())
Expand Down
Loading

0 comments on commit 8b4b7ce

Please sign in to comment.