diff --git a/Tests/test_arrow.py b/Tests/test_arrow.py index f236360b4..e5b48f2b6 100644 --- a/Tests/test_arrow.py +++ b/Tests/test_arrow.py @@ -12,62 +12,6 @@ from .helper import ( hopper, ) -pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") - -TEST_IMAGE_SIZE = (10, 10) - - -def _test_img_equals_pyarray(img: Image.Image, arr: Any, mask) -> None: - assert img.height * img.width == len(arr) - px = img.load() - assert px is not None - for x in range(0, img.size[0], int(img.size[0] / 10)): - for y in range(0, img.size[1], int(img.size[1] / 10)): - if mask: - for ix, elt in enumerate(mask): - assert px[x, y][ix] == arr[y * img.width + x].as_py()[elt] - else: - assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) - - -# really hard to get a non-nullable list type -fl_uint8_4_type = pyarrow.field( - "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) -).type - - -@pytest.mark.parametrize( - "mode, dtype, mask", - ( - ("L", pyarrow.uint8(), None), - ("I", pyarrow.int32(), None), - ("F", pyarrow.float32(), None), - ("LA", fl_uint8_4_type, [0, 3]), - ("RGB", fl_uint8_4_type, [0, 1, 2]), - ("RGBA", fl_uint8_4_type, None), - ("RGBX", fl_uint8_4_type, None), - ("CMYK", fl_uint8_4_type, None), - ("YCbCr", fl_uint8_4_type, [0, 1, 2]), - ("HSV", fl_uint8_4_type, [0, 1, 2]), - ), -) -def test_to_array(mode: str, dtype: Any, mask: Any) -> None: - img = hopper(mode) - - # Resize to non-square - img = img.crop((3, 0, 124, 127)) - assert img.size == (121, 127) - - arr = pyarrow.array(img) - _test_img_equals_pyarray(img, arr, mask) - assert arr.type == dtype - - reloaded = Image.fromarrow(arr, mode, img.size) - - assert reloaded - - assert_image_equal(img, reloaded) - @pytest.mark.parametrize( "mode, dest_modes", @@ -99,43 +43,6 @@ def test_invalid_array_size(): Image.fromarrow(img, "RGB", (10, 10)) -def test_lifetime(): - # valgrind shouldn't error out here. - # arrays should be accessible after the image is deleted. - - img = hopper("L") - - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) - - del img - - assert arr_1.sum().as_py() > 0 - del arr_1 - - assert arr_2.sum().as_py() > 0 - del arr_2 - - -def test_lifetime2(): - # valgrind shouldn't error out here. - # img should remain after the arrays are collected. - - img = hopper("L") - - arr_1 = pyarrow.array(img) - arr_2 = pyarrow.array(img) - - assert arr_1.sum().as_py() > 0 - del arr_1 - - assert arr_2.sum().as_py() > 0 - del arr_2 - - img2 = img.copy() - px = img2.load() - assert isinstance(px[0, 0], int) - def test_release_schema(): # these should not error out, valgrind should be clean diff --git a/Tests/test_pyarrow.py b/Tests/test_pyarrow.py new file mode 100644 index 000000000..8616d88ad --- /dev/null +++ b/Tests/test_pyarrow.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +from typing import Any # undone + +import pytest + +from PIL import Image + +from .helper import ( + assert_deep_equal, + assert_image_equal, + hopper, +) + +pyarrow = pytest.importorskip("pyarrow", reason="PyArrow not installed") + +TEST_IMAGE_SIZE = (10, 10) + + +def _test_img_equals_pyarray(img: Image.Image, arr: Any, mask) -> None: + assert img.height * img.width == len(arr) + px = img.load() + assert px is not None + for x in range(0, img.size[0], int(img.size[0] / 10)): + for y in range(0, img.size[1], int(img.size[1] / 10)): + if mask: + for ix, elt in enumerate(mask): + assert px[x, y][ix] == arr[y * img.width + x].as_py()[elt] + else: + assert_deep_equal(px[x, y], arr[y * img.width + x].as_py()) + + +# really hard to get a non-nullable list type +fl_uint8_4_type = pyarrow.field( + "_", pyarrow.list_(pyarrow.field("_", pyarrow.uint8()).with_nullable(False), 4) +).type + + +@pytest.mark.parametrize( + "mode, dtype, mask", + ( + ("L", pyarrow.uint8(), None), + ("I", pyarrow.int32(), None), + ("F", pyarrow.float32(), None), + ("LA", fl_uint8_4_type, [0, 3]), + ("RGB", fl_uint8_4_type, [0, 1, 2]), + ("RGBA", fl_uint8_4_type, None), + ("RGBX", fl_uint8_4_type, None), + ("CMYK", fl_uint8_4_type, None), + ("YCbCr", fl_uint8_4_type, [0, 1, 2]), + ("HSV", fl_uint8_4_type, [0, 1, 2]), + ), +) +def test_to_array(mode: str, dtype: Any, mask: Any) -> None: + img = hopper(mode) + + # Resize to non-square + img = img.crop((3, 0, 124, 127)) + assert img.size == (121, 127) + + arr = pyarrow.array(img) + _test_img_equals_pyarray(img, arr, mask) + assert arr.type == dtype + + reloaded = Image.fromarrow(arr, mode, img.size) + + assert reloaded + + assert_image_equal(img, reloaded) + + + +def test_lifetime(): + # valgrind shouldn't error out here. + # arrays should be accessible after the image is deleted. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + del img + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + +def test_lifetime2(): + # valgrind shouldn't error out here. + # img should remain after the arrays are collected. + + img = hopper("L") + + arr_1 = pyarrow.array(img) + arr_2 = pyarrow.array(img) + + assert arr_1.sum().as_py() > 0 + del arr_1 + + assert arr_2.sum().as_py() > 0 + del arr_2 + + img2 = img.copy() + px = img2.load() + assert isinstance(px[0, 0], int) diff --git a/pyproject.toml b/pyproject.toml index 9cf3344f3..a7e5ecde4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,13 +61,17 @@ optional-dependencies.tests = [ "markdown2", "olefile", "packaging", - "pyarrow", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers>=2024.10.12", ] + +optional-dependencies.test-arrow = [ + "pyarrow", +] + optional-dependencies.typing = [ "typing-extensions; python_version<'3.10'", ]