Added type hints

This commit is contained in:
Andrew Murray 2024-07-02 20:10:47 +10:00
parent 8c2a823e77
commit 267c0b37b1
18 changed files with 107 additions and 31 deletions

View File

@ -321,6 +321,7 @@ class TestColorLut3DCoreAPI:
-1, 2, 2, 2, 2, 2,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)
@ -341,6 +342,7 @@ class TestColorLut3DCoreAPI:
-3, 5, 5, 5, 5, 5,
])).load()
# fmt: on
assert transformed is not None
assert transformed[0, 0] == (0, 0, 255)
assert transformed[50, 50] == (0, 0, 255)
assert transformed[255, 0] == (0, 255, 255)

View File

@ -76,6 +76,7 @@ def test_pil184() -> None:
def test_1px_width(tmp_path: Path) -> None:
im = Image.new("L", (1, 256))
px = im.load()
assert px is not None
for y in range(256):
px[0, y] = y
_roundtrip(tmp_path, im)
@ -84,6 +85,7 @@ def test_1px_width(tmp_path: Path) -> None:
def test_large_count(tmp_path: Path) -> None:
im = Image.new("L", (256, 1))
px = im.load()
assert px is not None
for x in range(256):
px[x, 0] = x // 67 * 67
_roundtrip(tmp_path, im)
@ -101,6 +103,7 @@ def _test_buffer_overflow(tmp_path: Path, im: Image.Image, size: int = 1024) ->
def test_break_in_count_overflow(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -110,6 +113,7 @@ def test_break_in_count_overflow(tmp_path: Path) -> None:
def test_break_one_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -119,6 +123,7 @@ def test_break_one_in_loop(tmp_path: Path) -> None:
def test_break_many_in_loop(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(4):
for x in range(256):
px[x, y] = x % 128
@ -130,6 +135,7 @@ def test_break_many_in_loop(tmp_path: Path) -> None:
def test_break_one_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -140,6 +146,7 @@ def test_break_one_at_end(tmp_path: Path) -> None:
def test_break_many_at_end(tmp_path: Path) -> None:
im = Image.new("L", (256, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(256):
px[x, y] = x % 128
@ -152,6 +159,7 @@ def test_break_many_at_end(tmp_path: Path) -> None:
def test_break_padding(tmp_path: Path) -> None:
im = Image.new("L", (257, 5))
px = im.load()
assert px is not None
for y in range(5):
for x in range(257):
px[x, y] = x % 128

View File

@ -575,6 +575,7 @@ class TestImage:
for mode in ("I", "F", "L"):
im = Image.new(mode, (100, 100), (5,))
px = im.load()
assert px is not None
assert px[0, 0] == 5
def test_linear_gradient_wrong_mode(self) -> None:

View File

@ -47,6 +47,8 @@ class TestImagePutPixel:
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
with pytest.raises(TypeError):
pix1[0, "0"]
with pytest.raises(TypeError):
@ -89,6 +91,8 @@ class TestImagePutPixel:
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(-1, -im1.size[1] - 1, -1):
for x in range(-1, -im1.size[0] - 1, -1):
pix2[x, y] = pix1[x, y]
@ -98,10 +102,11 @@ class TestImagePutPixel:
@pytest.mark.skipif(numpy is None, reason="NumPy not installed")
def test_numpy(self) -> None:
im = hopper()
pix = im.load()
px = im.load()
assert px is not None
assert numpy is not None
assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
assert px[numpy.int32(1), numpy.int32(2)] == (18, 20, 59)
class TestImageGetPixel:

View File

@ -222,8 +222,10 @@ def test_l_macro_rounding(convert_mode: str) -> None:
converted_im = im.convert(convert_mode)
px = converted_im.load()
assert px is not None
converted_color = px[0, 0]
if convert_mode == "LA":
assert converted_color is not None
converted_color = converted_color[0]
assert converted_color == 1

View File

@ -12,9 +12,10 @@ from .helper import hopper
def test_sanity() -> None:
im = hopper()
pix = im.load()
px = im.load()
assert pix[0, 0] == (20, 20, 70)
assert px is not None
assert px[0, 0] == (20, 20, 70)
def test_close() -> None:

View File

@ -14,6 +14,7 @@ class TestImagingPaste:
self, im: Image.Image, expected: list[tuple[int, int, int, int]]
) -> None:
px = im.load()
assert px is not None
actual = [
px[0, 0],
px[self.size // 2, 0],
@ -48,6 +49,7 @@ class TestImagingPaste:
def mask_1(self) -> Image.Image:
mask = Image.new("1", (self.size, self.size))
px = mask.load()
assert px is not None
for y in range(mask.height):
for x in range(mask.width):
px[y, x] = (x + y) % 2
@ -61,6 +63,7 @@ class TestImagingPaste:
def gradient_L(self) -> Image.Image:
gradient = Image.new("L", (self.size, self.size))
px = gradient.load()
assert px is not None
for y in range(gradient.height):
for x in range(gradient.width):
px[y, x] = (x + y) % 255

View File

@ -80,6 +80,7 @@ def test_quantize_no_dither2() -> None:
assert tuple(quantized.palette.palette) == data
px = quantized.load()
assert px is not None
for x in range(9):
assert px[x, 0] == (0 if x < 5 else 1)
@ -118,10 +119,12 @@ def test_colors() -> None:
def test_transparent_colors_equal() -> None:
im = Image.new("RGBA", (1, 2), (0, 0, 0, 0))
px = im.load()
assert px is not None
px[0, 1] = (255, 255, 255, 0)
converted = im.quantize()
converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted_px[0, 1]
@ -139,6 +142,7 @@ def test_palette(method: Image.Quantize, color: tuple[int, ...]) -> None:
converted = im.quantize(method=method)
converted_px = converted.load()
assert converted_px is not None
assert converted_px[0, 0] == converted.palette.colors[color]

View File

@ -74,6 +74,7 @@ class TestImagingCoreResampleAccuracy:
data = data.replace(" ", "")
sample = Image.new("L", size)
s_px = sample.load()
assert s_px is not None
w, h = size[0] // 2, size[1] // 2
for y in range(h):
for x in range(w):
@ -87,6 +88,8 @@ class TestImagingCoreResampleAccuracy:
def check_case(self, case: Image.Image, sample: Image.Image) -> None:
s_px = sample.load()
c_px = case.load()
assert s_px is not None
assert c_px is not None
for y in range(case.size[1]):
for x in range(case.size[0]):
if c_px[x, y] != s_px[x, y]:
@ -98,6 +101,7 @@ class TestImagingCoreResampleAccuracy:
def serialize_image(self, image: Image.Image) -> str:
s_px = image.load()
assert s_px is not None
return "\n".join(
" ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0]))
for y in range(image.size[1])
@ -235,11 +239,14 @@ class TestCoreResampleConsistency:
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]
px = im.load()
assert px is not None
return im.resize((9, 512), Image.Resampling.LANCZOS), px[0, 0]
def run_case(self, case: tuple[Image.Image, int | tuple[int, ...]]) -> None:
channel, color = case
px = channel.load()
assert px is not None
for x in range(channel.size[0]):
for y in range(channel.size[1]):
if px[x, y] != color:
@ -271,6 +278,7 @@ class TestCoreResampleAlphaCorrect:
def make_levels_case(self, mode: str) -> Image.Image:
i = Image.new(mode, (256, 16))
px = i.load()
assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
pix = [x] * len(mode)
@ -280,6 +288,7 @@ class TestCoreResampleAlphaCorrect:
def run_levels_case(self, i: Image.Image) -> None:
px = i.load()
assert px is not None
for y in range(i.size[1]):
used_colors = {px[x, y][0] for x in range(i.size[0])}
assert 256 == len(used_colors), (
@ -310,6 +319,7 @@ class TestCoreResampleAlphaCorrect:
) -> Image.Image:
i = Image.new(mode, (64, 64), dirty_pixel)
px = i.load()
assert px is not None
xdiv4 = i.size[0] // 4
ydiv4 = i.size[1] // 4
for y in range(ydiv4 * 2):
@ -319,6 +329,7 @@ class TestCoreResampleAlphaCorrect:
def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None:
px = i.load()
assert px is not None
for y in range(i.size[1]):
for x in range(i.size[0]):
if px[x, y][-1] != 0 and px[x, y][:-1] != clean_pixel:
@ -406,6 +417,7 @@ class TestCoreResampleCoefficients:
draw.rectangle((0, 0, i.size[0] // 2 - 1, 0), test_color)
px = i.resize((5, i.size[1]), Image.Resampling.BICUBIC).load()
assert px is not None
if px[2, 0] != test_color // 2:
assert test_color // 2 == px[2, 0]

View File

@ -165,10 +165,14 @@ def test_pad() -> None:
def test_pad_round() -> None:
im = Image.new("1", (1, 1), 1)
new_im = ImageOps.pad(im, (4, 1))
assert new_im.load()[2, 0] == 1
px = new_im.load()
assert px is not None
assert px[2, 0] == 1
new_im = ImageOps.pad(im, (1, 4))
assert new_im.load()[0, 2] == 1
px = new_im.load()
assert px is not None
assert px[0, 2] == 1
@pytest.mark.parametrize("mode", ("P", "PA"))
@ -223,6 +227,7 @@ def test_expand_palette(border: int | tuple[int, int, int, int]) -> None:
else:
left, top, right, bottom = border
px = im_expanded.convert("RGB").load()
assert px is not None
for x in range(im_expanded.width):
for b in range(top):
assert px[x, b] == (255, 0, 0)

View File

@ -70,6 +70,11 @@ def test_photoimage(mode: str) -> None:
reloaded = ImageTk.getimage(im_tk)
assert_image_equal(reloaded, im.convert("RGBA"))
with pytest.raises(ValueError):
ImageTk.PhotoImage()
with pytest.raises(ValueError):
ImageTk.PhotoImage(mode)
def test_photoimage_apply_transparency() -> None:
with Image.open("Tests/images/pil123p.png") as im:

View File

@ -16,6 +16,8 @@ def verify(im1: Image.Image) -> None:
assert im1.size == im2.size
pix1 = im1.load()
pix2 = im2.load()
assert pix1 is not None
assert pix2 is not None
for y in range(im1.size[1]):
for x in range(im1.size[0]):
xy = x, y

View File

@ -109,6 +109,7 @@ def _test_img_equals_nparray(img: Image.Image, np_img: _typing.NumpyArray) -> No
np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size
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)):
assert_deep_equal(px[x, y], np_img[y, x])
@ -141,6 +142,7 @@ def test_save_tiff_uint16() -> None:
img = Image.fromarray(a)
img_px = img.load()
assert img_px is not None
assert img_px[0, 0] == pixel_value

View File

@ -53,7 +53,7 @@ class FpxImageFile(ImageFile.ImageFile):
format = "FPX"
format_description = "FlashPix"
def _open(self):
def _open(self) -> None:
#
# read the OLE directory and see if this is a likely
# to be a FlashPix file
@ -64,7 +64,8 @@ class FpxImageFile(ImageFile.ImageFile):
msg = "not an FPX file; invalid OLE file"
raise SyntaxError(msg) from e
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
root = self.ole.root
if not root or root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
msg = "not an FPX file; bad root CLSID"
raise SyntaxError(msg)
@ -99,8 +100,7 @@ class FpxImageFile(ImageFile.ImageFile):
s = prop[0x2000002 | id]
bands = i32(s, 4)
if bands > 4:
if not isinstance(s, bytes) or (bands := i32(s, 4)) > 4:
msg = "Invalid number of bands"
raise OSError(msg)

View File

@ -221,7 +221,7 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# Registries
if TYPE_CHECKING:
from . import ImageFile
from . import ImageFile, ImagePalette
ID: list[str] = []
OPEN: dict[
str,
@ -1167,7 +1167,7 @@ class Image:
colors: int = 256,
method: int | None = None,
kmeans: int = 0,
palette=None,
palette: Image | None = None,
dither: Dither = Dither.FLOYDSTEINBERG,
) -> Image:
"""
@ -1239,8 +1239,8 @@ class Image:
from . import ImagePalette
mode = im.im.getpalettemode()
palette = im.im.getpalette(mode, mode)[: colors * len(mode)]
im.palette = ImagePalette.ImagePalette(mode, palette)
palette_data = im.im.getpalette(mode, mode)[: colors * len(mode)]
im.palette = ImagePalette.ImagePalette(mode, palette_data)
return im
@ -1395,7 +1395,9 @@ class Image:
self.load()
return self.im.getbbox(alpha_only)
def getcolors(self, maxcolors: int = 256):
def getcolors(
self, maxcolors: int = 256
) -> list[tuple[int, int]] | list[tuple[int, float]] | None:
"""
Returns a list of colors used in this image.
@ -1456,7 +1458,7 @@ class Image:
return tuple(self.im.getband(i).getextrema() for i in range(self.im.bands))
return self.im.getextrema()
def getxmp(self):
def getxmp(self) -> dict[str, Any]:
"""
Returns a dictionary containing the XMP tags.
Requires defusedxml to be installed.
@ -2017,7 +2019,11 @@ class Image:
self.im.putdata(data, scale, offset)
def putpalette(self, data, rawmode="RGB") -> None:
def putpalette(
self,
data: ImagePalette.ImagePalette | bytes | Sequence[int],
rawmode: str = "RGB",
) -> None:
"""
Attaches a palette to this image. The image must be a "P", "PA", "L"
or "LA" image.
@ -2093,7 +2099,9 @@ class Image:
value = (palette_index, alpha) if self.mode == "PA" else palette_index
return self.im.putpixel(xy, value)
def remap_palette(self, dest_map, source_palette=None):
def remap_palette(
self, dest_map: list[int], source_palette: bytes | bytearray | None = None
) -> Image:
"""
Rewrites the image to reorder the palette.
@ -3532,7 +3540,7 @@ def composite(image1: Image, image2: Image, mask: Image) -> Image:
return image
def eval(image, *args):
def eval(image: Image, *args: Callable[[int], float]) -> Image:
"""
Applies the function (which should take one argument) to each pixel
in the given image. If the image has more than one band, the same

View File

@ -361,7 +361,9 @@ def pad(
else:
out = Image.new(image.mode, size, color)
if resized.palette:
out.putpalette(resized.getpalette())
palette = resized.getpalette()
if palette is not None:
out.putpalette(palette)
if resized.width != size[0]:
x = round((size[0] - resized.width) * max(0, min(centering[0], 1)))
out.paste(resized, (x, 0))

View File

@ -28,8 +28,9 @@ from __future__ import annotations
import tkinter
from io import BytesIO
from typing import Any
from . import Image
from . import Image, ImageFile
# --------------------------------------------------------------------
# Check for Tkinter interface hooks
@ -49,14 +50,15 @@ def _pilbitmap_check() -> int:
return _pilbitmap_ok
def _get_image_from_kw(kw):
def _get_image_from_kw(kw: dict[str, Any]) -> ImageFile.ImageFile | None:
source = None
if "file" in kw:
source = kw.pop("file")
elif "data" in kw:
source = BytesIO(kw.pop("data"))
if source:
return Image.open(source)
if not source:
return None
return Image.open(source)
def _pyimagingtkcall(command, photo, id):
@ -96,12 +98,27 @@ class PhotoImage:
image file).
"""
def __init__(self, image=None, size=None, **kw):
def __init__(
self,
image: Image.Image | str | None = None,
size: tuple[int, int] | None = None,
**kw: Any,
) -> None:
# Tk compatibility: file or data
if image is None:
image = _get_image_from_kw(kw)
if hasattr(image, "mode") and hasattr(image, "size"):
if image is None:
msg = "Image is required"
raise ValueError(msg)
elif isinstance(image, str):
mode = image
image = None
if size is None:
msg = "If first argument is mode, size is required"
raise ValueError(msg)
else:
# got an image instead of a mode
mode = image.mode
if mode == "P":
@ -114,9 +131,6 @@ class PhotoImage:
mode = "RGB" # default
size = image.size
kw["width"], kw["height"] = size
else:
mode = image
image = None
if mode not in ["1", "L", "RGB", "RGBA"]:
mode = Image.getmodebase(mode)

View File

@ -70,7 +70,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.__fp = self.fp
self.seek(0)
def seek(self, frame):
def seek(self, frame: int) -> None:
if not self._seek_check(frame):
return
try: