From 139245a3db00cf4cc6de4ed726eba4561dcd5cec Mon Sep 17 00:00:00 2001 From: Yay295 Date: Wed, 27 Mar 2024 10:29:19 -0500 Subject: [PATCH 01/18] use namedtuple for image mode info --- Tests/test_image.py | 67 ++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 941ec40d9..779785c53 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,7 +8,7 @@ import sys import tempfile import warnings from pathlib import Path -from typing import IO +from typing import IO, NamedTuple import pytest @@ -33,34 +33,39 @@ from .helper import ( skip_unless_feature, ) -# name, pixel size + +class ImageModeInfo(NamedTuple): + name: str + pixel_size: int + + image_modes = ( - ("1", 1), - ("L", 1), - ("LA", 4), - ("La", 4), - ("P", 1), - ("PA", 4), - ("F", 4), - ("I", 4), - ("I;16", 2), - ("I;16L", 2), - ("I;16B", 2), - ("I;16N", 2), - ("RGB", 4), - ("RGBA", 4), - ("RGBa", 4), - ("RGBX", 4), - ("BGR;15", 2), - ("BGR;16", 2), - ("BGR;24", 3), - ("CMYK", 4), - ("YCbCr", 4), - ("HSV", 4), - ("LAB", 4), + ImageModeInfo("1", 1), + ImageModeInfo("L", 1), + ImageModeInfo("LA", 4), + ImageModeInfo("La", 4), + ImageModeInfo("P", 1), + ImageModeInfo("PA", 4), + ImageModeInfo("F", 4), + ImageModeInfo("I", 4), + ImageModeInfo("I;16", 2), + ImageModeInfo("I;16L", 2), + ImageModeInfo("I;16B", 2), + ImageModeInfo("I;16N", 2), + ImageModeInfo("RGB", 4), + ImageModeInfo("RGBA", 4), + ImageModeInfo("RGBa", 4), + ImageModeInfo("RGBX", 4), + ImageModeInfo("BGR;15", 2), + ImageModeInfo("BGR;16", 2), + ImageModeInfo("BGR;24", 3), + ImageModeInfo("CMYK", 4), + ImageModeInfo("YCbCr", 4), + ImageModeInfo("HSV", 4), + ImageModeInfo("LAB", 4), ) -image_mode_names = [name for name, _ in image_modes] +image_mode_names = [mode.name for mode in image_modes] class TestImage: @@ -1062,13 +1067,13 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize(("mode", "pixelsize"), image_modes) - def test_getdata_putdata(self, mode: str, pixelsize: int) -> None: - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixelsize)) + @pytest.mark.parametrize("mode", image_modes) + def test_getdata_putdata(self, mode: ImageModeInfo) -> None: + im = Image.new(mode.name, (2, 2)) + source_bytes = bytes(range(im.width * im.height * mode.pixel_size)) im.frombytes(source_bytes) - reloaded = Image.new(mode, im.size) + reloaded = Image.new(mode.name, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5a4b771fb00389a43dbf32a4c13d73c9b39e3f5d Mon Sep 17 00:00:00 2001 From: Yay295 Date: Mon, 2 Jan 2023 16:55:31 -0600 Subject: [PATCH 02/18] move image mode info variables to helper.py --- Tests/helper.py | 36 +++++++++++++++++++++++++++++++++++- Tests/test_image.py | 39 ++++----------------------------------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index c1399e89b..32ea99fdd 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,7 +13,7 @@ import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, Sequence +from typing import Any, Callable, NamedTuple, Sequence import pytest from packaging.version import parse as parse_version @@ -29,6 +29,40 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" +class ImageModeInfo(NamedTuple): + name: str + pixel_size: int + + +image_modes = ( + ImageModeInfo("1", 1), + ImageModeInfo("L", 1), + ImageModeInfo("LA", 4), + ImageModeInfo("La", 4), + ImageModeInfo("P", 1), + ImageModeInfo("PA", 4), + ImageModeInfo("F", 4), + ImageModeInfo("I", 4), + ImageModeInfo("I;16", 2), + ImageModeInfo("I;16L", 2), + ImageModeInfo("I;16B", 2), + ImageModeInfo("I;16N", 2), + ImageModeInfo("RGB", 4), + ImageModeInfo("RGBA", 4), + ImageModeInfo("RGBa", 4), + ImageModeInfo("RGBX", 4), + ImageModeInfo("BGR;15", 2), + ImageModeInfo("BGR;16", 2), + ImageModeInfo("BGR;24", 3), + ImageModeInfo("CMYK", 4), + ImageModeInfo("YCbCr", 4), + ImageModeInfo("HSV", 4), + ImageModeInfo("LAB", 4), +) + +image_mode_names = [mode.name for mode in image_modes] + + def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": # local img.show for errors. diff --git a/Tests/test_image.py b/Tests/test_image.py index 779785c53..5654307f1 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -8,7 +8,7 @@ import sys import tempfile import warnings from pathlib import Path -from typing import IO, NamedTuple +from typing import IO import pytest @@ -23,51 +23,20 @@ from PIL import ( ) from .helper import ( + ImageModeInfo, assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, assert_not_all_same, hopper, + image_mode_names, + image_modes, is_win32, mark_if_feature_version, skip_unless_feature, ) -class ImageModeInfo(NamedTuple): - name: str - pixel_size: int - - -image_modes = ( - ImageModeInfo("1", 1), - ImageModeInfo("L", 1), - ImageModeInfo("LA", 4), - ImageModeInfo("La", 4), - ImageModeInfo("P", 1), - ImageModeInfo("PA", 4), - ImageModeInfo("F", 4), - ImageModeInfo("I", 4), - ImageModeInfo("I;16", 2), - ImageModeInfo("I;16L", 2), - ImageModeInfo("I;16B", 2), - ImageModeInfo("I;16N", 2), - ImageModeInfo("RGB", 4), - ImageModeInfo("RGBA", 4), - ImageModeInfo("RGBa", 4), - ImageModeInfo("RGBX", 4), - ImageModeInfo("BGR;15", 2), - ImageModeInfo("BGR;16", 2), - ImageModeInfo("BGR;24", 3), - ImageModeInfo("CMYK", 4), - ImageModeInfo("YCbCr", 4), - ImageModeInfo("HSV", 4), - ImageModeInfo("LAB", 4), -) - -image_mode_names = [mode.name for mode in image_modes] - - class TestImage: @pytest.mark.parametrize("mode", image_mode_names) def test_image_modes_success(self, mode: str) -> None: From 0fed6a5fbcbe40aff7540693a438d18d825c839e Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sun, 31 Mar 2024 23:29:01 -0500 Subject: [PATCH 03/18] use common image mode list for TestImageGetPixel tests --- Tests/test_image_access.py | 38 ++++++++------------------------------ 1 file changed, 8 insertions(+), 30 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 8c42da57a..96afc87a4 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, is_win32 +from .helper import assert_image_equal, hopper, image_mode_names, is_win32 # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -138,8 +138,8 @@ class TestImageGetPixel(AccessTest): if bands == 1: return 1 if mode in ("BGR;15", "BGR;16"): - # These modes have less than 8 bits per band - # So (1, 2, 3) cannot be roundtripped + # These modes have less than 8 bits per band, + # so (1, 2, 3) cannot be roundtripped. return (16, 32, 49) return tuple(range(1, bands + 1)) @@ -168,16 +168,15 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None - error = ValueError if self._need_cffi_access else IndexError with pytest.raises(error): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): @@ -198,36 +197,15 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # Check 0 + # check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # Check 0 negative index + # check negative index with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize( - "mode", - ( - "1", - "L", - "LA", - "I", - "I;16", - "I;16B", - "F", - "P", - "PA", - "BGR;15", - "BGR;16", - "BGR;24", - "RGB", - "RGBA", - "RGBX", - "CMYK", - "YCbCr", - ), - ) + @pytest.mark.parametrize("mode", image_mode_names) def test_basic(self, mode: str) -> None: self.check(mode) From da7198c98740e9607dac0bf97ae073cf21510634 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 30 Mar 2024 14:32:45 -0500 Subject: [PATCH 04/18] fix ImagingAccess for I;16N on big-endian --- src/libImaging/Access.c | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/libImaging/Access.c b/src/libImaging/Access.c index 091c84e18..04618df09 100644 --- a/src/libImaging/Access.c +++ b/src/libImaging/Access.c @@ -81,12 +81,6 @@ get_pixel_16B(Imaging im, int x, int y, void *color) { #endif } -static void -get_pixel_16(Imaging im, int x, int y, void *color) { - UINT8 *in = (UINT8 *)&im->image[y][x + x]; - memcpy(color, in, sizeof(UINT16)); -} - static void get_pixel_BGR15(Imaging im, int x, int y, void *color) { UINT8 *in = (UINT8 *)&im->image8[y][x * 2]; @@ -207,7 +201,11 @@ ImagingAccessInit() { ADD("I;16", get_pixel_16L, put_pixel_16L); ADD("I;16L", get_pixel_16L, put_pixel_16L); ADD("I;16B", get_pixel_16B, put_pixel_16B); - ADD("I;16N", get_pixel_16, put_pixel_16L); +#ifdef WORDS_BIGENDIAN + ADD("I;16N", get_pixel_16B, put_pixel_16B); +#else + ADD("I;16N", get_pixel_16L, put_pixel_16L); +#endif ADD("I;32L", get_pixel_32L, put_pixel_32L); ADD("I;32B", get_pixel_32B, put_pixel_32B); ADD("F", get_pixel_32, put_pixel_32); From 5dabc6cf14c78d397d2b31d81d6a7fe9e10dbd3b Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 30 Mar 2024 15:10:04 -0500 Subject: [PATCH 05/18] fix I;16N lib pack test --- Tests/test_lib_pack.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index 6a0e704b8..f34ff7d02 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -216,7 +216,10 @@ class TestLibPack: ) def test_I16(self) -> None: - self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + if sys.byteorder == "little": + self.assert_pack("I;16N", "I;16N", 2, 0x0201, 0x0403, 0x0605) + else: + self.assert_pack("I;16N", "I;16N", 2, 0x0102, 0x0304, 0x0506) def test_F_float(self) -> None: self.assert_pack("F", "F;32F", 4, 1.539989614439558e-36, 4.063216068939723e-34) From fe79ae5653e42e5b5c42ff5428eeef36e4bbe7a6 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 6 Apr 2024 10:48:38 -0500 Subject: [PATCH 06/18] get pixel size by counting bytes in 1x1 image --- Tests/helper.py | 57 ++++++++++++++++++++------------------------- Tests/test_image.py | 19 +++++++++------ 2 files changed, 37 insertions(+), 39 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 32ea99fdd..f0bb8af00 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -13,7 +13,7 @@ import sysconfig import tempfile from functools import lru_cache from io import BytesIO -from typing import Any, Callable, NamedTuple, Sequence +from typing import Any, Callable, Sequence import pytest from packaging.version import parse as parse_version @@ -29,39 +29,32 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -class ImageModeInfo(NamedTuple): - name: str - pixel_size: int - - -image_modes = ( - ImageModeInfo("1", 1), - ImageModeInfo("L", 1), - ImageModeInfo("LA", 4), - ImageModeInfo("La", 4), - ImageModeInfo("P", 1), - ImageModeInfo("PA", 4), - ImageModeInfo("F", 4), - ImageModeInfo("I", 4), - ImageModeInfo("I;16", 2), - ImageModeInfo("I;16L", 2), - ImageModeInfo("I;16B", 2), - ImageModeInfo("I;16N", 2), - ImageModeInfo("RGB", 4), - ImageModeInfo("RGBA", 4), - ImageModeInfo("RGBa", 4), - ImageModeInfo("RGBX", 4), - ImageModeInfo("BGR;15", 2), - ImageModeInfo("BGR;16", 2), - ImageModeInfo("BGR;24", 3), - ImageModeInfo("CMYK", 4), - ImageModeInfo("YCbCr", 4), - ImageModeInfo("HSV", 4), - ImageModeInfo("LAB", 4), +image_mode_names = ( + "1", + "L", + "LA", + "La", + "P", + "PA", + "F", + "I", + "I;16", + "I;16L", + "I;16B", + "I;16N", + "RGB", + "RGBA", + "RGBa", + "RGBX", + "BGR;15", + "BGR;16", + "BGR;24", + "CMYK", + "YCbCr", + "HSV", + "LAB", ) -image_mode_names = [mode.name for mode in image_modes] - def upload(a: Image.Image, b: Image.Image) -> str | None: if uploader == "show": diff --git a/Tests/test_image.py b/Tests/test_image.py index 5654307f1..8b20de0a9 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -23,14 +23,12 @@ from PIL import ( ) from .helper import ( - ImageModeInfo, assert_image_equal, assert_image_equal_tofile, assert_image_similar_tofile, assert_not_all_same, hopper, image_mode_names, - image_modes, is_win32, mark_if_feature_version, skip_unless_feature, @@ -1036,13 +1034,20 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_modes) - def test_getdata_putdata(self, mode: ImageModeInfo) -> None: - im = Image.new(mode.name, (2, 2)) - source_bytes = bytes(range(im.width * im.height * mode.pixel_size)) + @pytest.mark.parametrize("mode", image_mode_names) + def test_getdata_putdata(self, mode: str) -> None: + # create an image with 1 pixel to get its pixel size + im = Image.new(mode, (1, 1)) + pixel_size = len(im.tobytes()) + + # create a new image with incrementing byte values + im = Image.new(mode, (2, 2)) + source_bytes = bytes(range(im.width * im.height * pixel_size)) im.frombytes(source_bytes) - reloaded = Image.new(mode.name, im.size) + # copy the data from the previous image to a new image + # and check that they are the same + reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5573ec74901945c58e700b2becb34ac800607bbe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 22:54:47 +1000 Subject: [PATCH 07/18] use hopper() for test_getdata_putdata() --- Tests/test_image.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 8b20de0a9..090b80f87 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1036,17 +1036,7 @@ class TestImageBytes: @pytest.mark.parametrize("mode", image_mode_names) def test_getdata_putdata(self, mode: str) -> None: - # create an image with 1 pixel to get its pixel size - im = Image.new(mode, (1, 1)) - pixel_size = len(im.tobytes()) - - # create a new image with incrementing byte values - im = Image.new(mode, (2, 2)) - source_bytes = bytes(range(im.width * im.height * pixel_size)) - im.frombytes(source_bytes) - - # copy the data from the previous image to a new image - # and check that they are the same + im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) assert_image_equal(im, reloaded) From 5c960d6abc25aa94044a0831743e061383d3f486 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 7 Apr 2024 23:01:51 +1000 Subject: [PATCH 08/18] rename "image_mode_names" to "modes" --- Tests/helper.py | 2 +- Tests/test_image.py | 10 +++++----- Tests/test_image_access.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index f0bb8af00..5d206f644 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -29,7 +29,7 @@ elif "GITHUB_ACTIONS" in os.environ: uploader = "github_actions" -image_mode_names = ( +modes = ( "1", "L", "LA", diff --git a/Tests/test_image.py b/Tests/test_image.py index 090b80f87..9985f2346 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,15 +28,15 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, - image_mode_names, is_win32, mark_if_feature_version, + modes, skip_unless_feature, ) class TestImage: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_image_modes_success(self, mode: str) -> None: Image.new(mode, (1, 1)) @@ -1017,7 +1017,7 @@ class TestImage: class TestImageBytes: - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_constructor(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1025,7 +1025,7 @@ class TestImageBytes: reloaded = Image.frombytes(mode, im.size, source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_roundtrip_bytes_method(self, mode: str) -> None: im = hopper(mode) source_bytes = im.tobytes() @@ -1034,7 +1034,7 @@ class TestImageBytes: reloaded.frombytes(source_bytes) assert reloaded.tobytes() == source_bytes - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: im = hopper(mode) reloaded = Image.new(mode, im.size) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 96afc87a4..50afb2a23 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -10,7 +10,7 @@ import pytest from PIL import Image -from .helper import assert_image_equal, hopper, image_mode_names, is_win32 +from .helper import assert_image_equal, hopper, is_win32, modes # CFFI imports pycparser which doesn't support PYTHONOPTIMIZE=2 # https://github.com/eliben/pycparser/pull/198#issuecomment-317001670 @@ -205,7 +205,7 @@ class TestImageGetPixel(AccessTest): with pytest.raises(error): im.getpixel((-1, -1)) - @pytest.mark.parametrize("mode", image_mode_names) + @pytest.mark.parametrize("mode", modes) def test_basic(self, mode: str) -> None: self.check(mode) From 98510570e6a54398c7975f1128b3de91fd8aad1f Mon Sep 17 00:00:00 2001 From: Yay295 Date: Sat, 20 Apr 2024 09:19:20 -0500 Subject: [PATCH 09/18] ignore BGR;15/16 test failure on big-endian --- Tests/test_image.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Tests/test_image.py b/Tests/test_image.py index 9985f2346..4a056df40 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -28,6 +28,7 @@ from .helper import ( assert_image_similar_tofile, assert_not_all_same, hopper, + is_big_endian, is_win32, mark_if_feature_version, modes, @@ -1036,6 +1037,8 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: + if is_big_endian and mode in ("BGR;15", "BGR;16"): + pytest.xfail(f"Known failure of {mode} on big-endian") im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) From d4a4b59ee39dcf2e8dddafebfecccaaf709ec8bb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:43:48 +0300 Subject: [PATCH 10/18] Sphinx extension to add dates to release notes Co-authored-by: Jason R. Coombs --- docs/conf.py | 1 + docs/dater.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 docs/dater.py diff --git a/docs/conf.py b/docs/conf.py index 392cf317e..f12b30e65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -28,6 +28,7 @@ needs_sphinx = "7.3" # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ + "dater", "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", diff --git a/docs/dater.py b/docs/dater.py new file mode 100644 index 000000000..d9e583547 --- /dev/null +++ b/docs/dater.py @@ -0,0 +1,52 @@ +""" +Sphinx extension to add timestamps to release notes based on Git versions. + +Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. +""" + +from __future__ import annotations + +import datetime as dt +import os +import re +import subprocess +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from sphinx.application import Sphinx + +DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") +VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") + + +def get_date_for(git_version: str) -> dt.datetime | None: + cmd = ["git", "log", "-1", "--format=%ai", git_version] + try: + with open(os.devnull, "w", encoding="utf-8") as devnull: + out = subprocess.check_output( + cmd, stderr=devnull, text=True, encoding="utf-8" + ) + ts = out.strip() + return dt.datetime.fromisoformat(ts) + except subprocess.CalledProcessError: + return None + + +def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: + if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): + old_title = m.group(1) + + if tag_datetime := get_date_for(old_title): + new_title = f"{old_title} ({tag_datetime:%Y-%m-%d})" + else: + new_title = f"{old_title} (unreleased)" + + new_underline = "-" * len(new_title) + + result = source[0].replace(m.group(0), f"{new_title}\n{new_underline}\n", 1) + source[0] = result + + +def setup(app: Sphinx) -> dict[str, bool]: + app.connect("source-read", add_date) + return {"parallel_read_safe": True} From 35003343386f3d2d40f179e0c18f96e0b58ce102 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Apr 2024 14:58:44 +0300 Subject: [PATCH 11/18] Fetch tags on Read the Docs --- .readthedocs.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 0c8f935d5..b83ba05b1 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,6 +6,10 @@ build: os: ubuntu-22.04 tools: python: "3" + jobs: + post_checkout: + - git remote add upstream https://github.com/python-pillow/Pillow.git # For forks + - git fetch upstream --tags python: install: From ccf1efb3efb371818455289b8c04aa21f18d4c4d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 24 Apr 2024 23:06:06 +1000 Subject: [PATCH 12/18] Use subprocess.DEVNULL --- docs/dater.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dater.py b/docs/dater.py index d9e583547..04855956b 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -7,7 +7,6 @@ Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. from __future__ import annotations import datetime as dt -import os import re import subprocess from typing import TYPE_CHECKING @@ -22,11 +21,10 @@ VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") def get_date_for(git_version: str) -> dt.datetime | None: cmd = ["git", "log", "-1", "--format=%ai", git_version] try: - with open(os.devnull, "w", encoding="utf-8") as devnull: - out = subprocess.check_output( - cmd, stderr=devnull, text=True, encoding="utf-8" - ) - ts = out.strip() + out = subprocess.check_output( + cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" + ) + ts = out.strip() return dt.datetime.fromisoformat(ts) except subprocess.CalledProcessError: return None From 03bcf03567ae934feaa737ee9128c19136cb7803 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 08:41:15 +1000 Subject: [PATCH 13/18] Removed Fedora 38 and added Fedora 40 --- .github/workflows/test-docker.yml | 2 +- docs/installation/platform-support.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-docker.yml b/.github/workflows/test-docker.yml index 70426d7b5..8f4a4d090 100644 --- a/.github/workflows/test-docker.yml +++ b/.github/workflows/test-docker.yml @@ -47,8 +47,8 @@ jobs: debian-11-bullseye-amd64, debian-12-bookworm-x86, debian-12-bookworm-amd64, - fedora-38-amd64, fedora-39-amd64, + fedora-40-amd64, gentoo, ubuntu-20.04-focal-amd64, ubuntu-22.04-jammy-amd64, diff --git a/docs/installation/platform-support.rst b/docs/installation/platform-support.rst index af205a4e8..c08a53a43 100644 --- a/docs/installation/platform-support.rst +++ b/docs/installation/platform-support.rst @@ -31,10 +31,10 @@ These platforms are built and tested for every change. +----------------------------------+----------------------------+---------------------+ | Debian 12 Bookworm | 3.11 | x86, x86-64 | +----------------------------------+----------------------------+---------------------+ -| Fedora 38 | 3.11 | x86-64 | -+----------------------------------+----------------------------+---------------------+ | Fedora 39 | 3.12 | x86-64 | +----------------------------------+----------------------------+---------------------+ +| Fedora 40 | 3.12 | x86-64 | ++----------------------------------+----------------------------+---------------------+ | Gentoo | 3.9 | x86-64 | +----------------------------------+----------------------------+---------------------+ | macOS 12 Monterey | 3.8, 3.9 | x86-64 | From 35ffbdc9cde567ae629ae01ddce3116a35f3483d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Apr 2024 01:38:43 +0000 Subject: [PATCH 14/18] Update dependency mypy to v1.10.0 --- .ci/requirements-mypy.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 6b0535fc1..a0dcb92d2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -1 +1 @@ -mypy==1.9.0 +mypy==1.10.0 From 5faebadd56ab3ec66139d123fcee54d3ff89ad30 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 11:06:04 +1000 Subject: [PATCH 15/18] BGR;16 does not fail on big-endian --- Tests/test_image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 4a056df40..8e7e40c05 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -1037,8 +1037,8 @@ class TestImageBytes: @pytest.mark.parametrize("mode", modes) def test_getdata_putdata(self, mode: str) -> None: - if is_big_endian and mode in ("BGR;15", "BGR;16"): - pytest.xfail(f"Known failure of {mode} on big-endian") + if is_big_endian and mode == "BGR;15": + pytest.xfail("Known failure of BGR;15 on big-endian") im = hopper(mode) reloaded = Image.new(mode, im.size) reloaded.putdata(im.getdata()) From bc35bf0c9e2b3c0a5702a21daa02d5982932f8d7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 13:10:45 +1000 Subject: [PATCH 16/18] Use split instead of datetime --- docs/dater.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docs/dater.py b/docs/dater.py index 04855956b..f9fb0c1da 100644 --- a/docs/dater.py +++ b/docs/dater.py @@ -6,7 +6,6 @@ Based on https://github.com/jaraco/rst.linker, with thanks to Jason R. Coombs. from __future__ import annotations -import datetime as dt import re import subprocess from typing import TYPE_CHECKING @@ -18,24 +17,23 @@ DOC_NAME_REGEX = re.compile(r"releasenotes/\d+\.\d+\.\d+") VERSION_TITLE_REGEX = re.compile(r"^(\d+\.\d+\.\d+)\n-+\n") -def get_date_for(git_version: str) -> dt.datetime | None: +def get_date_for(git_version: str) -> str | None: cmd = ["git", "log", "-1", "--format=%ai", git_version] try: out = subprocess.check_output( cmd, stderr=subprocess.DEVNULL, text=True, encoding="utf-8" ) - ts = out.strip() - return dt.datetime.fromisoformat(ts) except subprocess.CalledProcessError: return None + return out.split()[0] def add_date(app: Sphinx, doc_name: str, source: list[str]) -> None: if DOC_NAME_REGEX.match(doc_name) and (m := VERSION_TITLE_REGEX.match(source[0])): old_title = m.group(1) - if tag_datetime := get_date_for(old_title): - new_title = f"{old_title} ({tag_datetime:%Y-%m-%d})" + if tag_date := get_date_for(old_title): + new_title = f"{old_title} ({tag_date})" else: new_title = f"{old_title} (unreleased)" From bbd5a87e6046bd0c0eb9ad105102a0d019277eb5 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 25 Apr 2024 16:16:33 +1000 Subject: [PATCH 17/18] Combined conditions --- src/_imagingcms.c | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/_imagingcms.c b/src/_imagingcms.c index f18d55a57..63d78f84d 100644 --- a/src/_imagingcms.c +++ b/src/_imagingcms.c @@ -213,34 +213,37 @@ cms_transform_dealloc(CmsTransformObject *self) { static cmsUInt32Number findLCMStype(char *PILmode) { - if (strcmp(PILmode, "RGB") == 0) { + if ( + strcmp(PILmode, "RGB") == 0 || + strcmp(PILmode, "RGBA") == 0 || + strcmp(PILmode, "RGBX") == 0 + ) { return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBX") == 0) { - return TYPE_RGBA_8; - } else if (strcmp(PILmode, "RGBA;16B") == 0) { + } + if (strcmp(PILmode, "RGBA;16B") == 0) { return TYPE_RGBA_16; - } else if (strcmp(PILmode, "CMYK") == 0) { + } + if (strcmp(PILmode, "CMYK") == 0) { return TYPE_CMYK_8; - } else if (strcmp(PILmode, "L") == 0) { - return TYPE_GRAY_8; - } else if (strcmp(PILmode, "L;16") == 0) { + } + if (strcmp(PILmode, "L;16") == 0) { return TYPE_GRAY_16; - } else if (strcmp(PILmode, "L;16B") == 0) { + } + if (strcmp(PILmode, "L;16B") == 0) { return TYPE_GRAY_16_SE; - } else if (strcmp(PILmode, "YCCA") == 0) { + } + if ( + strcmp(PILmode, "YCCA") == 0 || + strcmp(PILmode, "YCC") == 0 + ) { return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "YCC") == 0) { - return TYPE_YCbCr_8; - } else if (strcmp(PILmode, "LAB") == 0) { + } + if (strcmp(PILmode, "LAB") == 0) { // LabX equivalent like ALab, but not reversed -- no #define in lcms2 return (COLORSPACE_SH(PT_LabV2) | CHANNELS_SH(3) | BYTES_SH(1) | EXTRA_SH(1)); } - else { - /* take a wild guess... */ - return TYPE_GRAY_8; - } + /* presume "L" by default */ + return TYPE_GRAY_8; } #define Cms_Min(a, b) ((a) < (b) ? (a) : (b)) From a4080a72494042183cff6fbadeba3026fb6fa711 Mon Sep 17 00:00:00 2001 From: Yay295 Date: Thu, 25 Apr 2024 08:51:33 -0500 Subject: [PATCH 18/18] clean up comments in test_image_access.py --- Tests/test_image_access.py | 40 +++++++++++++------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 50afb2a23..2cd2e0c34 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -33,7 +33,7 @@ except ImportError: class AccessTest: - # initial value + # Initial value _init_cffi_access = Image.USE_CFFI_ACCESS _need_cffi_access = False @@ -151,7 +151,7 @@ class TestImageGetPixel(AccessTest): self.color(mode) if expected_color_int is None else expected_color_int ) - # check putpixel + # Check putpixel im = Image.new(mode, (1, 1), None) im.putpixel((0, 0), expected_color) actual_color = im.getpixel((0, 0)) @@ -160,7 +160,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check putpixel negative index + # Check putpixel negative index im.putpixel((-1, -1), expected_color) actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( @@ -168,7 +168,7 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check 0x0 image with None initial color + # Check 0x0 image with None initial color im = Image.new(mode, (0, 0), None) assert im.load() is not None error = ValueError if self._need_cffi_access else IndexError @@ -176,13 +176,13 @@ class TestImageGetPixel(AccessTest): im.putpixel((0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # check negative index + # Check negative index with pytest.raises(error): im.putpixel((-1, -1), expected_color) with pytest.raises(error): im.getpixel((-1, -1)) - # check initial color + # Check initial color im = Image.new(mode, (1, 1), expected_color) actual_color = im.getpixel((0, 0)) assert actual_color == expected_color, ( @@ -190,18 +190,18 @@ class TestImageGetPixel(AccessTest): f"expected {expected_color} got {actual_color}" ) - # check initial color negative index + # Check initial color negative index actual_color = im.getpixel((-1, -1)) assert actual_color == expected_color, ( f"initial color failed with negative index for mode {mode}, " f"expected {expected_color} got {actual_color}" ) - # check 0x0 image with initial color + # Check 0x0 image with initial color im = Image.new(mode, (0, 0), expected_color) with pytest.raises(error): im.getpixel((0, 0)) - # check negative index + # Check negative index with pytest.raises(error): im.getpixel((-1, -1)) @@ -216,7 +216,7 @@ class TestImageGetPixel(AccessTest): @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) def test_signedness(self, mode: str, expected_color: int) -> None: - # see https://github.com/python-pillow/Pillow/issues/452 + # See https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @@ -276,13 +276,6 @@ class TestCffi(AccessTest): im = Image.new(mode, (10, 10), 40000) self._test_get_access(im) - # These don't actually appear to be modes that I can actually make, - # as unpack sets them directly into the I mode. - # im = Image.new('I;32L', (10, 10), -2**10) - # self._test_get_access(im) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_get_access(im) - def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? @@ -314,23 +307,18 @@ class TestCffi(AccessTest): self._test_set_access(hopper("LA"), (128, 128)) self._test_set_access(hopper("1"), 255) self._test_set_access(hopper("P"), 128) - # self._test_set_access(i, (128, 128)) #PA -- undone how to make + self._test_set_access(hopper("PA"), (128, 128)) self._test_set_access(hopper("F"), 1024.0) for mode in ("I;16", "I;16L", "I;16B", "I;16N", "I"): im = Image.new(mode, (10, 10), 40000) self._test_set_access(im, 45000) - # im = Image.new('I;32L', (10, 10), -(2**10)) - # self._test_set_access(im, -(2**13)+1) - # im = Image.new('I;32B', (10, 10), 2**10) - # self._test_set_access(im, 2**13-1) - @pytest.mark.filterwarnings("ignore::DeprecationWarning") def test_not_implemented(self) -> None: assert PyAccess.new(hopper("BGR;15")) is None - # ref https://github.com/python-pillow/Pillow/pull/2009 + # Ref https://github.com/python-pillow/Pillow/pull/2009 def test_reference_counting(self) -> None: size = 10 @@ -339,7 +327,7 @@ class TestCffi(AccessTest): with pytest.warns(DeprecationWarning): px = Image.new("L", (size, 1), 0).load() for i in range(size): - # pixels can contain garbage if image is released + # Pixels can contain garbage if image is released assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) @@ -456,7 +444,7 @@ int main(int argc, char* argv[]) env = os.environ.copy() env["PATH"] = sys.prefix + ";" + env["PATH"] - # do not display the Windows Error Reporting dialog + # Do not display the Windows Error Reporting dialog getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env)