Added type hints

This commit is contained in:
Andrew Murray 2024-02-12 09:28:53 +11:00
parent 5d6f22da12
commit 3f6422b512
15 changed files with 124 additions and 73 deletions

View File

@ -244,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image:
return Image.open(BytesIO(data))
def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes:
def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes:
out = BytesIO()
im.save(out, string_format, **options)
return out.getvalue()

View File

@ -4,6 +4,7 @@ import os
import subprocess
import sys
import sysconfig
from types import ModuleType
import pytest
@ -23,6 +24,7 @@ else:
except ImportError:
cffi = None
numpy: ModuleType | None
try:
import numpy
except ImportError:
@ -71,9 +73,10 @@ class TestImagePutPixel(AccessTest):
pix1 = im1.load()
pix2 = im2.load()
for x, y in ((0, "0"), ("0", 0)):
with pytest.raises(TypeError):
pix1[x, y]
with pytest.raises(TypeError):
pix1[0, "0"]
with pytest.raises(TypeError):
pix1["0", 0]
for y in range(im1.size[1]):
for x in range(im1.size[0]):
@ -123,12 +126,13 @@ class TestImagePutPixel(AccessTest):
im = hopper()
pix = im.load()
assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel(AccessTest):
@staticmethod
def color(mode):
def color(mode: str) -> int | tuple[int, ...]:
bands = Image.getmodebands(mode)
if bands == 1:
return 1
@ -138,12 +142,13 @@ class TestImageGetPixel(AccessTest):
return (16, 32, 49)
return tuple(range(1, bands + 1))
def check(self, mode, expected_color=None) -> None:
def check(self, mode: str, expected_color_int: int | None = None) -> None:
if self._need_cffi_access and mode.startswith("BGR;"):
pytest.skip("Support not added to deprecated module for BGR;* modes")
if not expected_color:
expected_color = self.color(mode)
expected_color = (
expected_color_int if expected_color_int is not None else self.color(mode)
)
# check putpixel
im = Image.new(mode, (1, 1), None)
@ -222,7 +227,7 @@ class TestImageGetPixel(AccessTest):
"YCbCr",
),
)
def test_basic(self, mode) -> None:
def test_basic(self, mode: str) -> None:
self.check(mode)
def test_list(self) -> None:
@ -231,14 +236,14 @@ 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, expected_color) -> None:
def test_signedness(self, mode: str, expected_color: int) -> None:
# see https://github.com/python-pillow/Pillow/issues/452
# pixelaccess is using signed int* instead of uint*
self.check(mode, expected_color)
@pytest.mark.parametrize("mode", ("P", "PA"))
@pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255)))
def test_p_putpixel_rgb_rgba(self, mode, color) -> None:
def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None:
im = Image.new(mode, (1, 1))
im.putpixel((0, 0), color)
@ -262,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel):
class TestCffi(AccessTest):
_need_cffi_access = True
def _test_get_access(self, im) -> None:
def _test_get_access(self, im: Image.Image) -> None:
"""Do we get the same thing as the old pixel access
Using private interfaces, forcing a capi access and
@ -299,7 +304,7 @@ class TestCffi(AccessTest):
# im = Image.new('I;32B', (10, 10), 2**10)
# self._test_get_access(im)
def _test_set_access(self, im, color) -> None:
def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None:
"""Are we writing the correct bits into the image?
Using private interfaces, forcing a capi access and
@ -359,7 +364,7 @@ class TestCffi(AccessTest):
assert px[i, 0] == 0
@pytest.mark.parametrize("mode", ("P", "PA"))
def test_p_putpixel_rgb_rgba(self, mode) -> None:
def test_p_putpixel_rgb_rgba(self, mode: str) -> None:
for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)):
im = Image.new(mode, (1, 1))
with pytest.warns(DeprecationWarning):
@ -377,7 +382,7 @@ class TestImagePutPixelError(AccessTest):
INVALID_TYPES = ["foo", 1.0, None]
@pytest.mark.parametrize("mode", IMAGE_MODES1)
def test_putpixel_type_error1(self, mode) -> None:
def test_putpixel_type_error1(self, mode: str) -> None:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(TypeError, match="color must be int or tuple"):
@ -400,14 +405,16 @@ class TestImagePutPixelError(AccessTest):
),
),
)
def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None:
def test_putpixel_invalid_number_of_bands(
self, mode: str, band_numbers: tuple[int, ...], match: str
) -> None:
im = hopper(mode)
for band_number in band_numbers:
with pytest.raises(TypeError, match=match):
im.putpixel((0, 0), (0,) * band_number)
@pytest.mark.parametrize("mode", IMAGE_MODES2)
def test_putpixel_type_error2(self, mode) -> None:
def test_putpixel_type_error2(self, mode: str) -> None:
im = hopper(mode)
for v in self.INVALID_TYPES:
with pytest.raises(
@ -416,7 +423,7 @@ class TestImagePutPixelError(AccessTest):
im.putpixel((0, 0), v)
@pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2)
def test_putpixel_overflow_error(self, mode) -> None:
def test_putpixel_overflow_error(self, mode: str) -> None:
im = hopper(mode)
with pytest.raises(OverflowError):
im.putpixel((0, 0), 2**80)
@ -428,7 +435,7 @@ class TestEmbeddable:
def test_embeddable(self) -> None:
import ctypes
from setuptools.command.build_ext import new_compiler
from setuptools.command import build_ext
with open("embed_pil.c", "w", encoding="utf-8") as fh:
fh.write(
@ -457,7 +464,7 @@ int main(int argc, char* argv[])
% sys.prefix.replace("\\", "\\\\")
)
compiler = new_compiler()
compiler = getattr(build_ext, "new_compiler")()
compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY"))
libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var(
@ -471,7 +478,7 @@ int main(int argc, char* argv[])
env["PATH"] = sys.prefix + ";" + env["PATH"]
# do not display the Windows Error Reporting dialog
ctypes.windll.kernel32.SetErrorMode(0x0002)
getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002)
process = subprocess.Popen(["embed_pil.exe"], env=env)
process.communicate()

View File

@ -1,5 +1,7 @@
from __future__ import annotations
from typing import Any
import pytest
from packaging.version import parse as parse_version
@ -13,7 +15,7 @@ im = hopper().resize((128, 100))
def test_toarray() -> None:
def test(mode):
def test(mode: str) -> tuple[tuple[int, ...], str, int]:
ai = numpy.array(im.convert(mode))
return ai.shape, ai.dtype.str, ai.nbytes
@ -50,14 +52,14 @@ def test_fromarray() -> None:
class Wrapper:
"""Class with API matching Image.fromarray"""
def __init__(self, img, arr_params) -> None:
def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None:
self.img = img
self.__array_interface__ = arr_params
def tobytes(self):
def tobytes(self) -> bytes:
return self.img.tobytes()
def test(mode):
def test(mode: str) -> tuple[str, tuple[int, int], bool]:
i = im.convert(mode)
a = numpy.array(i)
# Make wrapper instance for image, new array interface

View File

@ -7,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring
pytestmark = skip_unless_feature("jpg")
def draft_roundtrip(in_mode, in_size, req_mode, req_size):
def draft_roundtrip(
in_mode: str,
in_size: tuple[int, int],
req_mode: str | None,
req_size: tuple[int, int] | None,
) -> Image.Image:
im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG")
im = fromstring(data)

View File

@ -4,7 +4,7 @@ from .helper import hopper
def test_entropy() -> None:
def entropy(mode):
def entropy(mode: str) -> float:
return hopper(mode).entropy()
assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0

View File

@ -36,7 +36,7 @@ from .helper import assert_image_equal, hopper
),
)
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity(filter_to_apply, mode) -> None:
def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None:
im = hopper(mode)
if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter):
out = im.filter(filter_to_apply)
@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None:
@pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK"))
def test_sanity_error(mode) -> None:
def test_sanity_error(mode: str) -> None:
with pytest.raises(TypeError):
im = hopper(mode)
im.filter("hello")
@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None:
# crashes on small images
@pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3)))
def test_crash(size) -> None:
def test_crash(size: tuple[int, int]) -> None:
im = Image.new("RGB", size)
im.filter(ImageFilter.SMOOTH)
@ -67,7 +67,10 @@ def test_crash(size) -> None:
("RGB", ((4, 0, 0), (0, 0, 0))),
),
)
def test_modefilter(mode, expected) -> None:
def test_modefilter(
mode: str,
expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]],
) -> None:
im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9)))
# image is:
@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None:
("F", (0.0, 4.0, 8.0)),
),
)
def test_rankfilter(mode, expected) -> None:
def test_rankfilter(
mode: str,
expected: (
tuple[float, float, float]
| tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]]
),
) -> None:
im = Image.new(mode, (3, 3), None)
im.putdata(list(range(9)))
# image is:
@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None:
@pytest.mark.parametrize(
"filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter)
)
def test_rankfilter_error(filter) -> None:
def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None:
with pytest.raises(ValueError):
im = Image.new("P", (3, 3), None)
im.putdata(list(range(9)))
@ -137,7 +146,7 @@ def test_kernel_not_enough_coefficients() -> None:
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_3x3(mode) -> None:
def test_consistency_3x3(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss"
reference_name += "_I.png" if mode == "I" else ".bmp"
@ -163,7 +172,7 @@ def test_consistency_3x3(mode) -> None:
@pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK"))
def test_consistency_5x5(mode) -> None:
def test_consistency_5x5(mode: str) -> None:
with Image.open("Tests/images/hopper.bmp") as source:
reference_name = "hopper_emboss_more"
reference_name += "_I.png" if mode == "I" else ".bmp"
@ -199,7 +208,7 @@ def test_consistency_5x5(mode) -> None:
(2, -2),
),
)
def test_invalid_box_blur_filter(radius) -> None:
def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None:
with pytest.raises(ValueError):
ImageFilter.BoxBlur(radius)

View File

@ -6,7 +6,7 @@ from .helper import hopper
def test_extrema() -> None:
def extrema(mode):
def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]:
return hopper(mode).getextrema()
assert extrema("1") == (0, 255)

View File

@ -6,7 +6,7 @@ from .helper import hopper
def test_palette() -> None:
def palette(mode):
def palette(mode: str) -> list[int] | None:
p = hopper(mode).getpalette()
if p:
return p[:10]

View File

@ -46,7 +46,7 @@ class TestImagingPaste:
self.assert_9points_image(im, expected)
@CachedProperty
def mask_1(self):
def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size))
px = mask.load()
for y in range(mask.height):
@ -55,11 +55,11 @@ class TestImagingPaste:
return mask
@CachedProperty
def mask_L(self):
def mask_L(self) -> Image.Image:
return self.gradient_L.transpose(Image.Transpose.ROTATE_270)
@CachedProperty
def gradient_L(self):
def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
for y in range(gradient.height):
@ -68,7 +68,7 @@ class TestImagingPaste:
return gradient
@CachedProperty
def gradient_RGB(self):
def gradient_RGB(self) -> Image.Image:
return Image.merge(
"RGB",
[
@ -79,7 +79,7 @@ class TestImagingPaste:
)
@CachedProperty
def gradient_LA(self):
def gradient_LA(self) -> Image.Image:
return Image.merge(
"LA",
[
@ -89,7 +89,7 @@ class TestImagingPaste:
)
@CachedProperty
def gradient_RGBA(self):
def gradient_RGBA(self) -> Image.Image:
return Image.merge(
"RGBA",
[
@ -101,7 +101,7 @@ class TestImagingPaste:
)
@CachedProperty
def gradient_RGBa(self):
def gradient_RGBa(self) -> Image.Image:
return Image.merge(
"RGBa",
[

View File

@ -31,7 +31,7 @@ def test_sanity() -> None:
def test_long_integers() -> None:
# see bug-200802-systemerror
def put(value):
def put(value: int) -> tuple[int, int, int, int]:
im = Image.new("RGBA", (1, 1))
im.putdata([value])
return im.getpixel((0, 0))
@ -58,7 +58,7 @@ def test_mode_with_L_with_float() -> None:
@pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B"))
def test_mode_i(mode) -> None:
def test_mode_i(mode: str) -> None:
src = hopper("L")
data = list(src.getdata())
im = Image.new(mode, src.size, 0)
@ -79,7 +79,7 @@ def test_mode_F() -> None:
@pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
def test_mode_BGR(mode) -> None:
def test_mode_BGR(mode: str) -> None:
data = [(16, 32, 49), (32, 32, 98)]
im = Image.new(mode, (1, 2))
im.putdata(data)

View File

@ -8,7 +8,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper
def test_putpalette() -> None:
def palette(mode):
def palette(mode: str) -> str | tuple[str, list[int]]:
im = hopper(mode).copy()
im.putpalette(list(range(256)) * 3)
p = im.getpalette()
@ -81,7 +81,7 @@ def test_putpalette_with_alpha_values() -> None:
("RGBAX", (1, 2, 3, 4, 0)),
),
)
def test_rgba_palette(mode, palette) -> None:
def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None:
im = Image.new("P", (1, 1))
im.putpalette(palette, mode)
assert im.getpalette() == [1, 2, 3]

View File

@ -231,11 +231,13 @@ class TestImagingCoreResampleAccuracy:
class TestCoreResampleConsistency:
def make_case(self, mode: str, fill: tuple[int, int, int] | float):
def make_case(
self, mode: str, fill: tuple[int, int, int] | float
) -> tuple[Image.Image, tuple[int, ...]]:
im = Image.new(mode, (512, 9), fill)
return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0]
def run_case(self, case) -> None:
def run_case(self, case: tuple[Image.Image, Image.Image]) -> None:
channel, color = case
px = channel.load()
for x in range(channel.size[0]):
@ -353,7 +355,7 @@ class TestCoreResampleAlphaCorrect:
class TestCoreResamplePasses:
@contextmanager
def count(self, diff):
def count(self, diff: int) -> Generator[None, None, None]:
count = Image.core.get_stats()["new_count"]
yield
assert Image.core.get_stats()["new_count"] - count == diff

View File

@ -12,7 +12,13 @@ from .helper import (
)
def rotate(im, mode, angle, center=None, translate=None) -> None:
def rotate(
im: Image.Image,
mode: str,
angle: int,
center: tuple[int, int] | None = None,
translate: tuple[int, int] | None = None,
) -> None:
out = im.rotate(angle, center=center, translate=translate)
assert out.mode == mode
assert out.size == im.size # default rotate clips output
@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None) -> None:
@pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F"))
def test_mode(mode) -> None:
def test_mode(mode: str) -> None:
im = hopper(mode)
rotate(im, mode, 45)
@pytest.mark.parametrize("angle", (0, 90, 180, 270))
def test_angle(angle) -> None:
def test_angle(angle: int) -> None:
with Image.open("Tests/images/test-card.png") as im:
rotate(im, im.mode, angle)
@ -42,7 +48,7 @@ def test_angle(angle) -> None:
@pytest.mark.parametrize("angle", (0, 45, 90, 180, 270))
def test_zero(angle) -> None:
def test_zero(angle: int) -> None:
im = Image.new("RGB", (0, 0))
rotate(im, im.mode, angle)

View File

@ -111,7 +111,7 @@ def test_load_first_unless_jpeg() -> None:
with Image.open("Tests/images/hopper.jpg") as im:
draft = im.draft
def im_draft(mode, size):
def im_draft(mode: str, size: tuple[int, int]):
result = draft(mode, size)
assert result is not None

View File

@ -1,6 +1,7 @@
from __future__ import annotations
import math
from typing import Callable
import pytest
@ -91,7 +92,7 @@ class TestImageTransform:
("LA", (76, 0)),
),
)
def test_fill(self, mode, expected_pixel) -> None:
def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None:
im = hopper(mode)
(w, h) = im.size
transformed = im.transform(
@ -142,7 +143,9 @@ class TestImageTransform:
assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2)))
assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h)))
def _test_alpha_premult(self, op) -> None:
def _test_alpha_premult(
self, op: Callable[[Image.Image, tuple[int, int]], Image.Image]
) -> None:
# create image with half white, half black,
# with the black half transparent.
# do op,
@ -159,13 +162,13 @@ class TestImageTransform:
assert 40 * 10 == hist[-1]
def test_alpha_premult_resize(self) -> None:
def op(im, sz):
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
return im.resize(sz, Image.Resampling.BILINEAR)
self._test_alpha_premult(op)
def test_alpha_premult_transform(self) -> None:
def op(im, sz):
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR
@ -173,7 +176,9 @@ class TestImageTransform:
self._test_alpha_premult(op)
def _test_nearest(self, op, mode) -> None:
def _test_nearest(
self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str
) -> None:
# create white image with half transparent,
# do op,
# the image should remain white with half transparent
@ -196,15 +201,15 @@ class TestImageTransform:
)
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_resize(self, mode) -> None:
def op(im, sz):
def test_nearest_resize(self, mode: str) -> None:
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
return im.resize(sz, Image.Resampling.NEAREST)
self._test_nearest(op, mode)
@pytest.mark.parametrize("mode", ("RGBA", "LA"))
def test_nearest_transform(self, mode) -> None:
def op(im, sz):
def test_nearest_transform(self, mode: str) -> None:
def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image:
(w, h) = im.size
return im.transform(
sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST
@ -227,7 +232,9 @@ class TestImageTransform:
# Running by default, but I'd totally understand not doing it in
# the future
pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)]
pattern: list[Image.Image] | None = [
Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)
]
# Yeah. Watch some JIT optimize this out.
pattern = None # noqa: F841
@ -240,7 +247,7 @@ class TestImageTransform:
im.transform((100, 100), None)
@pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown"))
def test_unknown_resampling_filter(self, resample) -> None:
def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None:
with hopper() as im:
(w, h) = im.size
with pytest.raises(ValueError):
@ -250,7 +257,7 @@ class TestImageTransform:
class TestImageTransformAffine:
transform = Image.Transform.AFFINE
def _test_image(self):
def _test_image(self) -> Image.Image:
im = hopper("RGB")
return im.crop((10, 20, im.width - 10, im.height - 20))
@ -263,7 +270,7 @@ class TestImageTransformAffine:
(270, Image.Transpose.ROTATE_270),
),
)
def test_rotate(self, deg, transpose) -> None:
def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None:
im = self._test_image()
angle = -math.radians(deg)
@ -313,7 +320,13 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1),
),
)
def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None:
def test_resize(
self,
scale: float,
epsilon_scale: float,
resample: Image.Resampling,
epsilon: int,
) -> None:
im = self._test_image()
size_up = int(round(im.width * scale)), int(round(im.height * scale))
@ -342,7 +355,14 @@ class TestImageTransformAffine:
(Image.Resampling.BICUBIC, 1),
),
)
def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None:
def test_translate(
self,
x: float,
y: float,
epsilon_scale: float,
resample: Image.Resampling,
epsilon: float,
) -> None:
im = self._test_image()
size_up = int(round(im.width + x)), int(round(im.height + y))