From f2302ab716e9ee4e45bca3dee2c83ce5f56283b8 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 24 Jun 2024 21:04:33 +1000 Subject: [PATCH] Added type hints --- Tests/test_file_spider.py | 1 + Tests/test_imagefile.py | 4 +-- Tests/test_imageops_usm.py | 2 +- Tests/test_imagepalette.py | 8 +++--- src/PIL/BlpImagePlugin.py | 2 ++ src/PIL/ImageDraw2.py | 7 +++++- src/PIL/ImageFile.py | 25 ++++++++++--------- src/PIL/ImagePalette.py | 48 ++++++++++++++++++++++-------------- src/PIL/ImageWin.py | 13 ++++++---- src/PIL/SpiderImagePlugin.py | 22 +++++++++-------- 10 files changed, 79 insertions(+), 53 deletions(-) diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 9b82a962aee..66c88e9d8eb 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -105,6 +105,7 @@ def test_load_image_series() -> None: img_list = SpiderImagePlugin.loadImageSeries(file_list) # Assert + assert img_list is not None assert len(img_list) == 1 assert isinstance(img_list[0], Image.Image) assert img_list[0].size == (128, 128) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index a269b846a25..68b28ef07e2 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -209,7 +209,7 @@ def __init__(self, mode: str, *args: Any) -> None: super().__init__(mode, *args) - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: # eof return -1, 0 @@ -222,7 +222,7 @@ def __init__(self, mode: str, *args: Any) -> None: super().__init__(mode, *args) - def encode(self, buffer): + def encode(self, bufsize: int) -> tuple[int, int, bytes]: return 1, 1, b"" def cleanup(self) -> None: diff --git a/Tests/test_imageops_usm.py b/Tests/test_imageops_usm.py index dbdd5b317d5..c8e2c0467de 100644 --- a/Tests/test_imageops_usm.py +++ b/Tests/test_imageops_usm.py @@ -101,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None: assert i.im.getpixel((x, y))[c] >= 250 # Fuzzy match. - def gp(x, y): + def gp(x: int, y: int) -> tuple[int, ...]: return i.im.getpixel((x, y)) assert 236 <= gp(7, 4)[0] <= 239 diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 17f68710a48..6cf0079dda7 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None: palette.save(f) - p = ImagePalette.load(f) + lut = ImagePalette.load(f) # load returns raw palette information - assert len(p[0]) == 768 - assert p[1] == "RGB" + assert len(lut[0]) == 768 + assert lut[1] == "RGB" - p = ImagePalette.raw(p[1], p[0]) + p = ImagePalette.raw(lut[1], lut[0]) assert isinstance(p, ImagePalette.ImagePalette) assert p.palette == palette.tobytes() diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 59246c6e2e1..b9cefafdd07 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -430,6 +430,7 @@ class BLPEncoder(ImageFile.PyEncoder): def _write_palette(self) -> bytes: data = b"" + assert self.im is not None palette = self.im.getpalette("RGBA", "RGBA") for i in range(len(palette) // 4): r, g, b, a = palette[i * 4 : (i + 1) * 4] @@ -444,6 +445,7 @@ def encode(self, bufsize: int) -> tuple[int, int, bytes]: offset = 20 + 16 * 4 * 2 + len(palette_data) data = struct.pack("<16I", offset, *((0,) * 15)) + assert self.im is not None w, h = self.im.size data += struct.pack("<16I", w * h, *((0,) * 15)) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 600e140e1f6..e89a78be4b5 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -24,7 +24,10 @@ """ from __future__ import annotations +from typing import BinaryIO + from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath +from ._typing import StrOrBytesPath class Pen: @@ -45,7 +48,9 @@ def __init__(self, color: str, opacity: int = 255) -> None: class Font: """Stores a TrueType font and color""" - def __init__(self, color, file, size=12): + def __init__( + self, color: str, file: StrOrBytesPath | BinaryIO, size: float = 12 + ) -> None: # FIXME: add support for bitmap fonts self.color = ImageColor.getrgb(color) self.font = ImageFont.truetype(file, size) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 5d67409ea45..69e7ee54811 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -65,7 +65,7 @@ # Helpers -def _get_oserror(error, *, encoder): +def _get_oserror(error: int, *, encoder: bool) -> OSError: try: msg = Image.core.getcodecstatus(error) except AttributeError: @@ -76,7 +76,7 @@ def _get_oserror(error, *, encoder): return OSError(msg) -def raise_oserror(error): +def raise_oserror(error: int) -> OSError: deprecate( "raise_oserror", 12, @@ -154,11 +154,12 @@ def __init__(self, fp=None, filename=None): self.fp.close() raise - def get_format_mimetype(self): + def get_format_mimetype(self) -> str | None: if self.custom_mimetype: return self.custom_mimetype if self.format is not None: return Image.MIME.get(self.format.upper()) + return None def __setstate__(self, state): self.tile = [] @@ -365,7 +366,7 @@ class StubImageFile(ImageFile): certain format, but relies on external code to load the file. """ - def _open(self): + def _open(self) -> None: msg = "StubImageFile subclass must implement _open" raise NotImplementedError(msg) @@ -381,7 +382,7 @@ def load(self): self.__dict__ = image.__dict__ return image.load() - def _load(self): + def _load(self) -> StubHandler | None: """(Hook) Find actual image loader.""" msg = "StubImageFile subclass must implement _load" raise NotImplementedError(msg) @@ -621,7 +622,7 @@ def __init__(self) -> None: self.xoff = 0 self.yoff = 0 - def extents(self): + def extents(self) -> tuple[int, int, int, int]: return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize @@ -661,7 +662,7 @@ def setfd(self, fd): """ self.fd = fd - def setimage(self, im, extents=None): + def setimage(self, im, extents: tuple[int, int, int, int] | None = None) -> None: """ Called from ImageFile to set the core output image for the codec @@ -710,10 +711,10 @@ class PyDecoder(PyCodec): _pulls_fd = False @property - def pulls_fd(self): + def pulls_fd(self) -> bool: return self._pulls_fd - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: """ Override to perform the decoding process. @@ -738,6 +739,7 @@ def set_as_raw(self, data: bytes, rawmode=None) -> None: if not rawmode: rawmode = self.mode d = Image._getdecoder(self.mode, "raw", rawmode) + assert self.im is not None d.setimage(self.im, self.state.extents()) s = d.decode(data) @@ -760,7 +762,7 @@ class PyEncoder(PyCodec): _pushes_fd = False @property - def pushes_fd(self): + def pushes_fd(self) -> bool: return self._pushes_fd def encode(self, bufsize: int) -> tuple[int, int, bytes]: @@ -775,7 +777,7 @@ def encode(self, bufsize: int) -> tuple[int, int, bytes]: msg = "unavailable in base encoder" raise NotImplementedError(msg) - def encode_to_pyfd(self): + def encode_to_pyfd(self) -> tuple[int, int]: """ If ``pushes_fd`` is ``True``, then this method will be used, and ``encode()`` will only be called once. @@ -787,6 +789,7 @@ def encode_to_pyfd(self): return 0, -8 # bad configuration bytes_consumed, errcode, data = self.encode(0) if data: + assert self.fd is not None self.fd.write(data) return bytes_consumed, errcode diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 6473c4577b0..ed38285dc28 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -38,23 +38,27 @@ class ImagePalette: Defaults to an empty palette. """ - def __init__(self, mode: str = "RGB", palette: Sequence[int] | None = None) -> None: + def __init__( + self, + mode: str = "RGB", + palette: Sequence[int] | bytes | bytearray | None = None, + ) -> None: self.mode = mode - self.rawmode = None # if set, palette contains raw data + self.rawmode: str | None = None # if set, palette contains raw data self.palette = palette or bytearray() self.dirty: int | None = None @property - def palette(self): + def palette(self) -> Sequence[int] | bytes | bytearray: return self._palette @palette.setter - def palette(self, palette): - self._colors = None + def palette(self, palette: Sequence[int] | bytes | bytearray) -> None: + self._colors: dict[tuple[int, ...], int] | None = None self._palette = palette @property - def colors(self) -> dict[tuple[int, int, int] | tuple[int, int, int, int], int]: + def colors(self) -> dict[tuple[int, ...], int]: if self._colors is None: mode_len = len(self.mode) self._colors = {} @@ -66,9 +70,7 @@ def colors(self) -> dict[tuple[int, int, int] | tuple[int, int, int, int], int]: return self._colors @colors.setter - def colors( - self, colors: dict[tuple[int, int, int] | tuple[int, int, int, int], int] - ) -> None: + def colors(self, colors: dict[tuple[int, ...], int]) -> None: self._colors = colors def copy(self) -> ImagePalette: @@ -82,7 +84,7 @@ def copy(self) -> ImagePalette: return new - def getdata(self) -> tuple[str, bytes]: + def getdata(self) -> tuple[str, Sequence[int] | bytes | bytearray]: """ Get palette contents in format suitable for the low-level ``im.putpalette`` primitive. @@ -137,7 +139,7 @@ def _new_color_index( def getcolor( self, - color: tuple[int, int, int] | tuple[int, int, int, int], + color: tuple[int, ...], image: Image.Image | None = None, ) -> int: """Given an rgb tuple, allocate palette entry. @@ -162,12 +164,13 @@ def getcolor( except KeyError as e: # allocate new color slot index = self._new_color_index(image, e) + assert isinstance(self._palette, bytearray) self.colors[color] = index if index * 3 < len(self.palette): self._palette = ( - self.palette[: index * 3] + self._palette[: index * 3] + bytes(color) - + self.palette[index * 3 + 3 :] + + self._palette[index * 3 + 3 :] ) else: self._palette += bytes(color) @@ -204,7 +207,7 @@ def save(self, fp: str | IO[str]) -> None: # Internal -def raw(rawmode, data) -> ImagePalette: +def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data @@ -216,9 +219,9 @@ def raw(rawmode, data) -> ImagePalette: # Factories -def make_linear_lut(black, white): +def make_linear_lut(black: int, white: float) -> list[int]: if black == 0: - return [white * i // 255 for i in range(256)] + return [int(white * i // 255) for i in range(256)] msg = "unavailable when black is non-zero" raise NotImplementedError(msg) # FIXME @@ -251,15 +254,22 @@ def wedge(mode: str = "RGB") -> ImagePalette: return ImagePalette(mode, [i // len(mode) for i in palette]) -def load(filename): +def load(filename: str) -> tuple[bytes, str]: # FIXME: supports GIMP gradients only with open(filename, "rb") as fp: - for paletteHandler in [ + paletteHandlers: list[ + type[ + GimpPaletteFile.GimpPaletteFile + | GimpGradientFile.GimpGradientFile + | PaletteFile.PaletteFile + ] + ] = [ GimpPaletteFile.GimpPaletteFile, GimpGradientFile.GimpGradientFile, PaletteFile.PaletteFile, - ]: + ] + for paletteHandler in paletteHandlers: try: fp.seek(0) lut = paletteHandler(fp).getpalette() diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 6c29e2590cf..978c5a9d176 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -69,19 +69,22 @@ class Dib: defines the size of the image. """ - def __init__(self, image, size=None): - if hasattr(image, "mode") and hasattr(image, "size"): + def __init__( + self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None + ) -> None: + if isinstance(image, str): + mode = image + image = "" + else: mode = image.mode size = image.size - else: - mode = image - image = None if mode not in ["1", "L", "P", "RGB"]: mode = Image.getmodebase(mode) self.image = Image.core.display(mode, size) self.mode = mode self.size = size if image: + assert not isinstance(image, str) self.paste(image) def expose(self, handle): diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index a6cc00019da..f5a09c3ef61 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -37,12 +37,12 @@ import os import struct import sys -from typing import IO, TYPE_CHECKING +from typing import IO, TYPE_CHECKING, Any, Tuple, cast from . import Image, ImageFile -def isInt(f): +def isInt(f: Any) -> int: try: i = int(f) if f - i == 0: @@ -62,7 +62,7 @@ def isInt(f): # otherwise returns 0 -def isSpiderHeader(t): +def isSpiderHeader(t: tuple[float, ...]) -> int: h = (99,) + t # add 1 value so can use spider header index start=1 # header values 1,2,5,12,13,22,23 should be integers for i in [1, 2, 5, 12, 13, 22, 23]: @@ -82,7 +82,7 @@ def isSpiderHeader(t): return labbyt -def isSpiderImage(filename): +def isSpiderImage(filename: str) -> int: with open(filename, "rb") as fp: f = fp.read(92) # read 23 * 4 bytes t = struct.unpack(">23f", f) # try big-endian first @@ -184,13 +184,15 @@ def seek(self, frame: int) -> None: self._open() # returns a byte image after rescaling to 0..255 - def convert2byte(self, depth=255): - (minimum, maximum) = self.getextrema() - m = 1 + def convert2byte(self, depth: int = 255) -> Image.Image: + extrema = self.getextrema() + assert isinstance(extrema[0], float) + minimum, maximum = cast(Tuple[float, float], extrema) + m: float = 1 if maximum != minimum: m = depth / (maximum - minimum) b = -m * minimum - return self.point(lambda i, m=m, b=b: i * m + b).convert("L") + return self.point(lambda i: i * m + b).convert("L") if TYPE_CHECKING: from . import ImageTk @@ -207,10 +209,10 @@ def tkPhotoImage(self) -> ImageTk.PhotoImage: # given a list of filenames, return a list of images -def loadImageSeries(filelist=None): +def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: - return + return None imglist = [] for img in filelist: