Skip to content

Commit

Permalink
Merge branch 'main' into type_hint
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Jun 3, 2024
2 parents 4639a71 + 219add0 commit 12559ff
Show file tree
Hide file tree
Showing 38 changed files with 291 additions and 167 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/wheels-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ elif [ "${AUDITWHEEL_POLICY::9}" == "musllinux" ]; then
else
yum install -y fribidi
fi
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ]; then
if [ "${AUDITWHEEL_POLICY::9}" != "musllinux" ] && !([[ "$OSTYPE" == "darwin"* ]] && [[ $(python3 --version) == *"3.13."* ]]); then
python3 -m pip install numpy
fi

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ jobs:
- cp310
- cp311
- cp312
- cp313
spec:
- manylinux2014
- manylinux_2_28
Expand Down Expand Up @@ -80,6 +81,7 @@ jobs:
CIBW_ARCHS: "aarch64"
# Likewise, select only one Python version per job to speed this up.
CIBW_BUILD: "${{ matrix.python-version }}-${{ matrix.spec == 'musllinux' && 'musllinux' || 'manylinux' }}*"
CIBW_PRERELEASE_PYTHONS: True
# Extra options for manylinux.
CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.spec }}
CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: ${{ matrix.spec }}
Expand Down Expand Up @@ -133,6 +135,7 @@ jobs:
CIBW_BUILD: ${{ matrix.build }}
CIBW_MANYLINUX_PYPY_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux }}
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: cp38-macosx_arm64
MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macosx_deployment_target }}
Expand Down Expand Up @@ -204,6 +207,7 @@ jobs:
CIBW_ARCHS: ${{ matrix.cibw_arch }}
CIBW_BEFORE_ALL: "{package}\\winbuild\\build\\build_dep_all.cmd"
CIBW_CACHE_PATH: "C:\\cibw"
CIBW_PRERELEASE_PYTHONS: True
CIBW_SKIP: pp38-*
CIBW_TEST_SKIP: "*-win_arm64"
CIBW_TEST_COMMAND: 'docker run --rm
Expand Down
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ Changelog (Pillow)
10.4.0 (unreleased)
-------------------

- Added ImageDraw circle() #8085
[void4, hugovk, radarhere]

- Add mypy target to Makefile #8077
[Yay295]

- Added more modes to Image.MODES #7984
[radarhere]

- Deprecate BGR;15, BGR;16 and BGR;24 modes #7978
[radarhere, hugovk]

Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,8 @@ lint-fix:
python3 -m black .
python3 -c "import ruff" > /dev/null 2>&1 || python3 -m pip install ruff
python3 -m ruff --fix .

.PHONY: mypy
mypy:
python3 -c "import tox" > /dev/null 2>&1 || python3 -m pip install tox
python3 -m tox -e mypy
25 changes: 11 additions & 14 deletions Tests/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,13 @@ def skip_unless_feature(feature: str) -> pytest.MarkDecorator:
def skip_unless_feature_version(
feature: str, required: str, reason: str | None = None
) -> pytest.MarkDecorator:
if not features.check(feature):
version = features.version(feature)
if version is None:
return pytest.mark.skip(f"{feature} not available")
if reason is None:
reason = f"{feature} is older than {required}"
version_required = parse_version(required)
version_available = parse_version(features.version(feature))
version_available = parse_version(version)
return pytest.mark.skipif(version_available < version_required, reason=reason)


Expand All @@ -189,12 +190,13 @@ def mark_if_feature_version(
version_blacklist: str,
reason: str | None = None,
) -> pytest.MarkDecorator:
if not features.check(feature):
version = features.version(feature)
if version is None:
return pytest.mark.pil_noop_mark()
if reason is None:
reason = f"{feature} is {version_blacklist}"
version_required = parse_version(version_blacklist)
version_available = parse_version(features.version(feature))
version_available = parse_version(version)
if (
version_available.major == version_required.major
and version_available.minor == version_required.minor
Expand All @@ -220,16 +222,11 @@ def _get_mem_usage(self) -> float:
from resource import RUSAGE_SELF, getrusage

mem = getrusage(RUSAGE_SELF).ru_maxrss
if sys.platform == "darwin":
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized (in bytes).
return mem / 1024 # Kb
# linux
# man 2 getrusage
# ru_maxrss (since Linux 2.6.32)
# This is the maximum resident set size used (in kilobytes).
return mem # Kb
# man 2 getrusage:
# ru_maxrss
# This is the maximum resident set size utilized
# in bytes on macOS, in kilobytes on Linux
return mem / 1024 if sys.platform == "darwin" else mem

def _test_leak(self, core: Callable[[], None]) -> None:
start_mem = self._get_mem_usage()
Expand Down
5 changes: 3 additions & 2 deletions Tests/oss-fuzz/test_fuzzers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@

if sys.platform.startswith("win32"):
pytest.skip("Fuzzer is linux only", allow_module_level=True)
if features.check("libjpeg_turbo"):
version = packaging.version.parse(features.version("libjpeg_turbo"))
libjpeg_turbo_version = features.version("libjpeg_turbo")
if libjpeg_turbo_version is not None:
version = packaging.version.parse(libjpeg_turbo_version)
if version.major == 2 and version.minor == 0:
pytestmark = pytest.mark.valgrind_known_error(
reason="Known failing with libjpeg_turbo 2.0"
Expand Down
10 changes: 7 additions & 3 deletions Tests/test_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_version() -> None:
# Check the correctness of the convenience function
# and the format of version numbers

def test(name: str, function: Callable[[str], bool]) -> None:
def test(name: str, function: Callable[[str], str | None]) -> None:
version = features.version(name)
if not features.check(name):
assert version is None
Expand Down Expand Up @@ -67,12 +67,16 @@ def test_webp_anim() -> None:

@skip_unless_feature("libjpeg_turbo")
def test_libjpeg_turbo_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libjpeg_turbo"))
version = features.version("libjpeg_turbo")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)


@skip_unless_feature("libimagequant")
def test_libimagequant_version() -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version("libimagequant"))
version = features.version("libimagequant")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)


@pytest.mark.parametrize("feature", features.modules)
Expand Down
5 changes: 3 additions & 2 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -1252,10 +1252,11 @@ def test_palette_save_L(tmp_path: Path) -> None:

im = hopper("P")
im_l = Image.frombytes("L", im.size, im.tobytes())
palette = bytes(im.getpalette())
palette = im.getpalette()
assert palette is not None

out = str(tmp_path / "temp.gif")
im_l.save(out, palette=palette)
im_l.save(out, palette=bytes(palette))

with Image.open(out) as reloaded:
assert_image_equal(reloaded.convert("RGB"), im.convert("RGB"))
Expand Down
8 changes: 5 additions & 3 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Im

def test_sanity(self) -> None:
# internal version number
assert re.search(r"\d+\.\d+$", features.version_codec("jpg"))
version = features.version_codec("jpg")
assert version is not None
assert re.search(r"\d+\.\d+$", version)

with Image.open(TEST_FILE) as im:
im.load()
Expand Down Expand Up @@ -152,7 +154,7 @@ def test_cmyk(self) -> None:
assert k > 0.9

def test_rgb(self) -> None:
def getchannels(im: Image.Image) -> tuple[int, int, int]:
def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]:
return tuple(v[0] for v in im.layer)

im = hopper()
Expand Down Expand Up @@ -441,7 +443,7 @@ def test_smooth(self) -> None:
assert_image(im1, im2.mode, im2.size)

def test_subsampling(self) -> None:
def getsampling(im: Image.Image):
def getsampling(im: JpegImagePlugin.JpegImageFile):
layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]

Expand Down
4 changes: 3 additions & 1 deletion Tests/test_file_jpeg2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def roundtrip(im: Image.Image, **options: Any) -> Image.Image:

def test_sanity() -> None:
# Internal version number
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("jpg_2000"))
version = features.version_codec("jpg_2000")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)

with Image.open("Tests/images/test-card-lossless.jp2") as im:
px = im.load()
Expand Down
7 changes: 5 additions & 2 deletions Tests/test_file_libtiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> No

class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None:
assert re.search(r"\d+\.\d+\.\d+$", features.version_codec("libtiff"))
version = features.version_codec("libtiff")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)

def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path"""
Expand Down Expand Up @@ -666,7 +668,8 @@ def save_bytesio(compression: str | None = None) -> None:
pilim.save(buffer_io, format="tiff", compression=compression)
buffer_io.seek(0)

assert_image_similar_tofile(pilim, buffer_io, 0)
with Image.open(buffer_io) as saved_im:
assert_image_similar(pilim, saved_im, 0)

save_bytesio()
save_bytesio("raw")
Expand Down
6 changes: 3 additions & 3 deletions Tests/test_file_png.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ def get_chunks(self, filename: str) -> list[bytes]:

def test_sanity(self, tmp_path: Path) -> None:
# internal version number
assert re.search(
r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", features.version_codec("zlib")
)
version = features.version_codec("zlib")
assert version is not None
assert re.search(r"\d+(\.\d+){1,3}(\.zlib\-ng)?$", version)

test_file = str(tmp_path / "temp.png")

Expand Down
4 changes: 3 additions & 1 deletion Tests/test_file_webp.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ def setup_method(self) -> None:
def test_version(self) -> None:
_webp.WebPDecoderVersion()
_webp.WebPDecoderBuggyAlpha()
assert re.search(r"\d+\.\d+\.\d+$", features.version_module("webp"))
version = features.version_module("webp")
assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version)

def test_read_rgb(self) -> None:
"""
Expand Down
10 changes: 6 additions & 4 deletions Tests/test_file_webp_animated.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ def test_write_animation_L(tmp_path: Path) -> None:
assert_image_similar(im, orig.convert("RGBA"), 32.9)

if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
version = features.version_module("webp")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
orig.seek(orig.n_frames - 1)
im.seek(im.n_frames - 1)
Expand All @@ -78,8 +79,9 @@ def check(temp_file) -> None:

# Compare second frame to original
if is_big_endian():
webp = parse_version(features.version_module("webp"))
if webp < parse_version("1.2.2"):
version = features.version_module("webp")
assert version is not None
if parse_version(version) < parse_version("1.2.2"):
pytest.skip("Fails with libwebp earlier than 1.2.2")
im.seek(1)
im.load()
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_font_leaks.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class TestTTypeFontLeak(PillowLeakTestCase):
iterations = 10
mem_limit = 4096 # k

def _test_font(self, font: ImageFont.FreeTypeFont) -> None:
def _test_font(self, font: ImageFont.FreeTypeFont | ImageFont.ImageFont) -> None:
im = Image.new("RGB", (255, 255), "white")
draw = ImageDraw.ImageDraw(im)
self._test_leak(
Expand Down
24 changes: 18 additions & 6 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .helper import (
assert_image_equal,
assert_image_equal_tofile,
assert_image_similar,
assert_image_similar_tofile,
assert_not_all_same,
hopper,
Expand Down Expand Up @@ -99,10 +100,18 @@ def test_open_formats(self) -> None:
JPGFILE = "Tests/images/hopper.jpg"

with pytest.raises(TypeError):
with Image.open(PNGFILE, formats=123):
with Image.open(PNGFILE, formats=123): # type: ignore[arg-type]
pass

for formats in [["JPEG"], ("JPEG",), ["jpeg"], ["Jpeg"], ["jPeG"], ["JpEg"]]:
format_list: list[list[str] | tuple[str, ...]] = [
["JPEG"],
("JPEG",),
["jpeg"],
["Jpeg"],
["jPeG"],
["JpEg"],
]
for formats in format_list:
with pytest.raises(UnidentifiedImageError):
with Image.open(PNGFILE, formats=formats):
pass
Expand Down Expand Up @@ -138,7 +147,7 @@ def test_invalid_image(self) -> None:

def test_bad_mode(self) -> None:
with pytest.raises(ValueError):
with Image.open("filename", "bad mode"):
with Image.open("filename", "bad mode"): # type: ignore[arg-type]
pass

def test_stringio(self) -> None:
Expand Down Expand Up @@ -185,7 +194,8 @@ def test_tempfile(self) -> None:
with tempfile.TemporaryFile() as fp:
im.save(fp, "JPEG")
fp.seek(0)
assert_image_similar_tofile(im, fp, 20)
with Image.open(fp) as reloaded:
assert_image_similar(im, reloaded, 20)

def test_unknown_extension(self, tmp_path: Path) -> None:
im = hopper()
Expand Down Expand Up @@ -497,9 +507,11 @@ def test_effect_spread_zero(self) -> None:
def test_check_size(self) -> None:
# Checking that the _check_size function throws value errors when we want it to
with pytest.raises(ValueError):
Image.new("RGB", 0) # not a tuple
# not a tuple
Image.new("RGB", 0) # type: ignore[arg-type]
with pytest.raises(ValueError):
Image.new("RGB", (0,)) # Tuple too short
# tuple too short
Image.new("RGB", (0,)) # type: ignore[arg-type]
with pytest.raises(ValueError):
Image.new("RGB", (-1, -1)) # w,h < 0

Expand Down
2 changes: 1 addition & 1 deletion Tests/test_image_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ def test(mode: str) -> tuple[str, tuple[int, int], bool]:
assert test("RGBX") == ("RGBA", (128, 100), True)

# Test mode is None with no "typestr" in the array interface
wrapped = Wrapper(hopper("L"), {"shape": (100, 128)})
with pytest.raises(TypeError):
wrapped = Wrapper(test("L"), {"shape": (100, 128)})
Image.fromarray(wrapped)


Expand Down
Loading

0 comments on commit 12559ff

Please sign in to comment.