Skip to content

Commit 35460fe

Browse files
authored
Merge branch 'main' into stdlib-src
2 parents 43993e9 + 6c06fa2 commit 35460fe

12 files changed

Lines changed: 150 additions & 164 deletions

Tests/benchmarks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,8 @@ def test_quantize_to_palette(
868868
size: tuple[int, int],
869869
) -> None:
870870
if isinstance(source_type, pathlib.Path):
871-
im = Image.open(source_type).convert("RGB").resize(size)
871+
with Image.open(source_type) as source_im:
872+
im = source_im.convert("RGB").resize(size)
872873
elif source_type == "synthetic":
873874
im = make_pillow_image("RGB", size)
874875
if palette_type == "exact":

Tests/helper.py

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,6 @@ def assert_not_all_same(items: Sequence[Any], msg: str | None = None) -> None:
154154
assert items.count(items[0]) != len(items), msg
155155

156156

157-
def assert_tuple_approx_equal(
158-
actuals: Sequence[int], targets: tuple[int, ...], threshold: int, msg: str
159-
) -> None:
160-
"""Tests if actuals has values within threshold from targets"""
161-
for i, target in enumerate(targets):
162-
if not (target - threshold <= actuals[i] <= target + threshold):
163-
pytest.fail(msg + ": " + repr(actuals) + " != " + repr(targets))
164-
165-
166157
def timeout_unless_slower_valgrind(timeout: float) -> pytest.MarkDecorator:
167158
if "PILLOW_VALGRIND_TEST" in os.environ:
168159
return pytest.mark.pil_noop_mark()
@@ -344,13 +335,4 @@ def is_win32() -> bool:
344335

345336

346337
def is_pypy() -> bool:
347-
return hasattr(sys, "pypy_translation_info")
348-
349-
350-
class CachedProperty:
351-
def __init__(self, func: Callable[[Any], Any]) -> None:
352-
self.func = func
353-
354-
def __get__(self, instance: Any, cls: type[Any] | None = None) -> Any:
355-
result = instance.__dict__[self.func.__name__] = self.func(instance)
356-
return result
338+
return sys.implementation.name == "pypy"

Tests/test_file_apng.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,30 @@ def test_apng_mode() -> None:
299299
assert im_rgba.getpixel((64, 32)) == (0, 0, 255, 128)
300300

301301

302+
def test_apng_save_different_palettes(tmp_path: Path) -> None:
303+
# APNG does not support multiple palettes
304+
test_file = tmp_path / "temp.png"
305+
red = Image.new("P", (1, 1), (255, 0, 0))
306+
green = Image.new("P", (1, 1), (0, 255, 0))
307+
red.save(test_file, save_all=True, append_images=[green])
308+
309+
with Image.open(test_file) as reloaded:
310+
# So the frames must be converted to another mode
311+
assert reloaded.mode == "RGB"
312+
assert reloaded.getpixel((0, 0)) == (255, 0, 0)
313+
314+
reloaded.seek(1)
315+
assert reloaded.mode == "RGB"
316+
assert reloaded.getpixel((0, 0)) == (0, 255, 0)
317+
318+
# Test that RGBA palettes result in an RGBA image
319+
green = Image.new("L", (1, 1))
320+
green.putpalette([], "RGBA")
321+
red.save(test_file, save_all=True, append_images=[green])
322+
with Image.open(test_file) as reloaded:
323+
assert reloaded.mode == "RGBA"
324+
325+
302326
def test_apng_chunk_errors() -> None:
303327
with Image.open("Tests/images/apng/chunk_no_actl.png") as im:
304328
assert isinstance(im, PngImagePlugin.PngImageFile)

Tests/test_image.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,23 @@ def test_remap_palette(self) -> None:
674674
with pytest.raises(ValueError):
675675
im_hopper.remap_palette([])
676676

677+
@pytest.mark.parametrize("palette_mode", ("RGB", "RGBA"))
678+
def test_remap_palette_source_palette(self, palette_mode: str) -> None:
679+
# When source_palette is provided, mode is inferred from length.
680+
# 768 entries should be detected as a 256-entry RGB palette
681+
# 1024 entries should be detected as a 256-entry RGBA palette
682+
im = Image.new("P", (1, 1))
683+
source_palette = bytes(
684+
entry for entry in range(256) for channel in range(len(palette_mode))
685+
)
686+
im_remapped = im.remap_palette(list(range(256)), source_palette)
687+
688+
palette = im_remapped.palette
689+
assert palette is not None
690+
assert len(palette.colors) == 256
691+
assert palette.mode == palette_mode
692+
assert palette.palette == source_palette
693+
677694
def test_remap_palette_transparency(self) -> None:
678695
im = Image.new("P", (1, 2), (0, 0, 0))
679696
im.putpixel((0, 1), (255, 0, 0))

Tests/test_image_paste.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

3+
from functools import cached_property
4+
35
import pytest
46

57
from PIL import Image
68

7-
from .helper import CachedProperty, assert_image_equal
9+
from .helper import assert_image_equal
810

911

1012
class TestImagingPaste:
@@ -45,7 +47,7 @@ def assert_9points_paste(
4547
im.paste(im2, mask)
4648
self.assert_9points_image(im, expected)
4749

48-
@CachedProperty
50+
@cached_property
4951
def mask_1(self) -> Image.Image:
5052
mask = Image.new("1", (self.size, self.size))
5153
px = mask.load()
@@ -55,11 +57,11 @@ def mask_1(self) -> Image.Image:
5557
px[y, x] = (x + y) % 2
5658
return mask
5759

58-
@CachedProperty
60+
@cached_property
5961
def mask_L(self) -> Image.Image:
6062
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
6163

62-
@CachedProperty
64+
@cached_property
6365
def gradient_L(self) -> Image.Image:
6466
gradient = Image.new("L", (self.size, self.size))
6567
px = gradient.load()
@@ -69,7 +71,7 @@ def gradient_L(self) -> Image.Image:
6971
px[y, x] = (x + y) % 255
7072
return gradient
7173

72-
@CachedProperty
74+
@cached_property
7375
def gradient_RGB(self) -> Image.Image:
7476
return Image.merge(
7577
"RGB",
@@ -80,7 +82,7 @@ def gradient_RGB(self) -> Image.Image:
8082
],
8183
)
8284

83-
@CachedProperty
85+
@cached_property
8486
def gradient_LA(self) -> Image.Image:
8587
return Image.merge(
8688
"LA",
@@ -90,7 +92,7 @@ def gradient_LA(self) -> Image.Image:
9092
],
9193
)
9294

93-
@CachedProperty
95+
@cached_property
9496
def gradient_RGBA(self) -> Image.Image:
9597
return Image.merge(
9698
"RGBA",
@@ -102,7 +104,7 @@ def gradient_RGBA(self) -> Image.Image:
102104
],
103105
)
104106

105-
@CachedProperty
107+
@cached_property
106108
def gradient_RGBa(self) -> Image.Image:
107109
return Image.merge(
108110
"RGBa",

Tests/test_imageops.py

Lines changed: 39 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
assert_image_equal,
99
assert_image_similar,
1010
assert_image_similar_tofile,
11-
assert_tuple_approx_equal,
1211
hopper,
1312
)
1413

@@ -291,33 +290,15 @@ def test_colorize_2color() -> None:
291290
im_test = ImageOps.colorize(im_l, "red", "green")
292291

293292
# Test output image (2-color)
294-
left = (0, 1)
295-
middle = (127, 1)
296-
right = (255, 1)
297-
value = im_test.getpixel(left)
298-
assert isinstance(value, tuple)
299-
assert_tuple_approx_equal(
300-
value,
301-
(255, 0, 0),
302-
threshold=1,
303-
msg="black test pixel incorrect",
304-
)
305-
value = im_test.getpixel(middle)
306-
assert isinstance(value, tuple)
307-
assert_tuple_approx_equal(
308-
value,
309-
(127, 63, 0),
310-
threshold=1,
311-
msg="mid test pixel incorrect",
312-
)
313-
value = im_test.getpixel(right)
314-
assert isinstance(value, tuple)
315-
assert_tuple_approx_equal(
316-
value,
317-
(0, 127, 0),
318-
threshold=1,
319-
msg="white test pixel incorrect",
320-
)
293+
assert im_test.getpixel((0, 1)) == pytest.approx(
294+
(255, 0, 0), abs=1
295+
), "black test pixel incorrect"
296+
assert im_test.getpixel((127, 1)) == pytest.approx(
297+
(127, 63, 0), abs=1
298+
), "mid test pixel incorrect"
299+
assert im_test.getpixel((255, 1)) == pytest.approx(
300+
(0, 127, 0), abs=1
301+
), "white test pixel incorrect"
321302

322303

323304
def test_colorize_2color_offset() -> None:
@@ -333,33 +314,15 @@ def test_colorize_2color_offset() -> None:
333314
)
334315

335316
# Test output image (2-color) with offsets
336-
left = (25, 1)
337-
middle = (75, 1)
338-
right = (125, 1)
339-
value = im_test.getpixel(left)
340-
assert isinstance(value, tuple)
341-
assert_tuple_approx_equal(
342-
value,
343-
(255, 0, 0),
344-
threshold=1,
345-
msg="black test pixel incorrect",
346-
)
347-
value = im_test.getpixel(middle)
348-
assert isinstance(value, tuple)
349-
assert_tuple_approx_equal(
350-
value,
351-
(127, 63, 0),
352-
threshold=1,
353-
msg="mid test pixel incorrect",
354-
)
355-
value = im_test.getpixel(right)
356-
assert isinstance(value, tuple)
357-
assert_tuple_approx_equal(
358-
value,
359-
(0, 127, 0),
360-
threshold=1,
361-
msg="white test pixel incorrect",
362-
)
317+
assert im_test.getpixel((25, 1)) == pytest.approx(
318+
(255, 0, 0), abs=1
319+
), "black test pixel incorrect"
320+
assert im_test.getpixel((75, 1)) == pytest.approx(
321+
(127, 63, 0), abs=1
322+
), "mid test pixel incorrect"
323+
assert im_test.getpixel((125, 1)) == pytest.approx(
324+
(0, 127, 0), abs=1
325+
), "white test pixel incorrect"
363326

364327

365328
def test_colorize_3color_offset() -> None:
@@ -381,46 +344,21 @@ def test_colorize_3color_offset() -> None:
381344
)
382345

383346
# Test output image (3-color) with offsets
384-
left = (25, 1)
385-
left_middle = (75, 1)
386-
middle = (100, 1)
387-
right_middle = (150, 1)
388-
right = (225, 1)
389-
value = im_test.getpixel(left)
390-
assert isinstance(value, tuple)
391-
assert_tuple_approx_equal(
392-
value,
393-
(255, 0, 0),
394-
threshold=1,
395-
msg="black test pixel incorrect",
396-
)
397-
value = im_test.getpixel(left_middle)
398-
assert isinstance(value, tuple)
399-
assert_tuple_approx_equal(
400-
value,
401-
(127, 0, 127),
402-
threshold=1,
403-
msg="low-mid test pixel incorrect",
404-
)
405-
value = im_test.getpixel(middle)
406-
assert isinstance(value, tuple)
407-
assert_tuple_approx_equal(value, (0, 0, 255), threshold=1, msg="mid incorrect")
408-
value = im_test.getpixel(right_middle)
409-
assert isinstance(value, tuple)
410-
assert_tuple_approx_equal(
411-
value,
412-
(0, 63, 127),
413-
threshold=1,
414-
msg="high-mid test pixel incorrect",
415-
)
416-
value = im_test.getpixel(right)
417-
assert isinstance(value, tuple)
418-
assert_tuple_approx_equal(
419-
value,
420-
(0, 127, 0),
421-
threshold=1,
422-
msg="white test pixel incorrect",
423-
)
347+
assert im_test.getpixel((25, 1)) == pytest.approx(
348+
(255, 0, 0), abs=1
349+
), "black test pixel incorrect"
350+
assert im_test.getpixel((75, 1)) == pytest.approx(
351+
(127, 0, 127), abs=1
352+
), "low-mid test pixel incorrect"
353+
assert im_test.getpixel((100, 1)) == pytest.approx(
354+
(0, 0, 255), abs=1
355+
), "mid incorrect"
356+
assert im_test.getpixel((150, 1)) == pytest.approx(
357+
(0, 63, 127), abs=1
358+
), "high-mid test pixel incorrect"
359+
assert im_test.getpixel((225, 1)) == pytest.approx(
360+
(0, 127, 0), abs=1
361+
), "white test pixel incorrect"
424362

425363

426364
def test_colorize_invalid_mode() -> None:
@@ -594,18 +532,12 @@ def test_autocontrast_mask_real_input() -> None:
594532
result_nomask = ImageOps.autocontrast(img)
595533

596534
assert result_nomask != result
597-
assert_tuple_approx_equal(
598-
ImageStat.Stat(result, mask=rect_mask).median,
599-
(195, 202, 184),
600-
threshold=2,
601-
msg="autocontrast with mask pixel incorrect",
602-
)
603-
assert_tuple_approx_equal(
604-
ImageStat.Stat(result_nomask).median,
605-
(119, 106, 79),
606-
threshold=2,
607-
msg="autocontrast without mask pixel incorrect",
608-
)
535+
assert ImageStat.Stat(result, mask=rect_mask).median == pytest.approx(
536+
(195, 202, 184), abs=2
537+
), "autocontrast with mask pixel incorrect"
538+
assert ImageStat.Stat(result_nomask).median == pytest.approx(
539+
(119, 106, 79), abs=2
540+
), "autocontrast without mask pixel incorrect"
609541

610542

611543
def test_autocontrast_preserve_tone() -> None:

Tests/test_tiff_ifdrational.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ def test_sanity() -> None:
4141

4242
def test_float() -> None:
4343
# float() must agree with ==, int() and repr(), which all use the
44-
# normalized fraction
44+
# normalized fraction
4545
r = IFDRational(1.5, 3)
4646
assert r == 0.5
4747
assert float(r) == 0.5
4848

49-
# Integer numerator and 0/0 (nan) cases
49+
# Integer numerator and 0/0 (nan) cases
5050
assert float(IFDRational(4, 2)) == 2.0
5151
assert math.isnan(float(IFDRational(0, 0)))
5252

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ lint.isort.required-imports = [
187187
max_supported_python = "3.15"
188188

189189
[tool.mypy]
190+
exclude = [
191+
"^Tests/images/msp/",
192+
"^Tests/images/picins/",
193+
"^Tests/images/sunraster/",
194+
]
190195
follow_imports = "silent"
191196
python_version = "3.11"
192197
disallow_any_generics = true
@@ -197,6 +202,7 @@ warn_unreachable = true
197202
enable_error_code = "ignore-without-code"
198203
extra_checks = true
199204
pretty = true
205+
num_workers = 4
200206

201207
[tool.pytest]
202208
addopts = [ "-ra", "--color=auto" ]

0 commit comments

Comments
 (0)