Added type hints

This commit is contained in:
Andrew Murray 2024-06-24 21:04:33 +10:00
parent ad4c23bddd
commit f2302ab716
10 changed files with 79 additions and 53 deletions

View File

@ -105,6 +105,7 @@ def test_load_image_series() -> None:
img_list = SpiderImagePlugin.loadImageSeries(file_list) img_list = SpiderImagePlugin.loadImageSeries(file_list)
# Assert # Assert
assert img_list is not None
assert len(img_list) == 1 assert len(img_list) == 1
assert isinstance(img_list[0], Image.Image) assert isinstance(img_list[0], Image.Image)
assert img_list[0].size == (128, 128) assert img_list[0].size == (128, 128)

View File

@ -209,7 +209,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
# eof # eof
return -1, 0 return -1, 0
@ -222,7 +222,7 @@ class MockPyEncoder(ImageFile.PyEncoder):
super().__init__(mode, *args) super().__init__(mode, *args)
def encode(self, buffer): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
return 1, 1, b"" return 1, 1, b""
def cleanup(self) -> None: def cleanup(self) -> None:

View File

@ -101,7 +101,7 @@ def test_blur_accuracy(test_images: dict[str, ImageFile.ImageFile]) -> None:
assert i.im.getpixel((x, y))[c] >= 250 assert i.im.getpixel((x, y))[c] >= 250
# Fuzzy match. # Fuzzy match.
def gp(x, y): def gp(x: int, y: int) -> tuple[int, ...]:
return i.im.getpixel((x, y)) return i.im.getpixel((x, y))
assert 236 <= gp(7, 4)[0] <= 239 assert 236 <= gp(7, 4)[0] <= 239

View File

@ -88,13 +88,13 @@ def test_file(tmp_path: Path) -> None:
palette.save(f) palette.save(f)
p = ImagePalette.load(f) lut = ImagePalette.load(f)
# load returns raw palette information # load returns raw palette information
assert len(p[0]) == 768 assert len(lut[0]) == 768
assert p[1] == "RGB" assert lut[1] == "RGB"
p = ImagePalette.raw(p[1], p[0]) p = ImagePalette.raw(lut[1], lut[0])
assert isinstance(p, ImagePalette.ImagePalette) assert isinstance(p, ImagePalette.ImagePalette)
assert p.palette == palette.tobytes() assert p.palette == palette.tobytes()

View File

@ -430,6 +430,7 @@ class BLPEncoder(ImageFile.PyEncoder):
def _write_palette(self) -> bytes: def _write_palette(self) -> bytes:
data = b"" data = b""
assert self.im is not None
palette = self.im.getpalette("RGBA", "RGBA") palette = self.im.getpalette("RGBA", "RGBA")
for i in range(len(palette) // 4): for i in range(len(palette) // 4):
r, g, b, a = palette[i * 4 : (i + 1) * 4] r, g, b, a = palette[i * 4 : (i + 1) * 4]
@ -444,6 +445,7 @@ class BLPEncoder(ImageFile.PyEncoder):
offset = 20 + 16 * 4 * 2 + len(palette_data) offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15)) data = struct.pack("<16I", offset, *((0,) * 15))
assert self.im is not None
w, h = self.im.size w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15)) data += struct.pack("<16I", w * h, *((0,) * 15))

View File

@ -24,7 +24,10 @@
""" """
from __future__ import annotations from __future__ import annotations
from typing import BinaryIO
from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
from ._typing import StrOrBytesPath
class Pen: class Pen:
@ -45,7 +48,9 @@ class Brush:
class Font: class Font:
"""Stores a TrueType font and color""" """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 # FIXME: add support for bitmap fonts
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
self.font = ImageFont.truetype(file, size) self.font = ImageFont.truetype(file, size)

View File

@ -65,7 +65,7 @@ Dict of known error codes returned from :meth:`.PyDecoder.decode`,
# Helpers # Helpers
def _get_oserror(error, *, encoder): def _get_oserror(error: int, *, encoder: bool) -> OSError:
try: try:
msg = Image.core.getcodecstatus(error) msg = Image.core.getcodecstatus(error)
except AttributeError: except AttributeError:
@ -76,7 +76,7 @@ def _get_oserror(error, *, encoder):
return OSError(msg) return OSError(msg)
def raise_oserror(error): def raise_oserror(error: int) -> OSError:
deprecate( deprecate(
"raise_oserror", "raise_oserror",
12, 12,
@ -154,11 +154,12 @@ class ImageFile(Image.Image):
self.fp.close() self.fp.close()
raise raise
def get_format_mimetype(self): def get_format_mimetype(self) -> str | None:
if self.custom_mimetype: if self.custom_mimetype:
return self.custom_mimetype return self.custom_mimetype
if self.format is not None: if self.format is not None:
return Image.MIME.get(self.format.upper()) return Image.MIME.get(self.format.upper())
return None
def __setstate__(self, state): def __setstate__(self, state):
self.tile = [] self.tile = []
@ -365,7 +366,7 @@ class StubImageFile(ImageFile):
certain format, but relies on external code to load the file. certain format, but relies on external code to load the file.
""" """
def _open(self): def _open(self) -> None:
msg = "StubImageFile subclass must implement _open" msg = "StubImageFile subclass must implement _open"
raise NotImplementedError(msg) raise NotImplementedError(msg)
@ -381,7 +382,7 @@ class StubImageFile(ImageFile):
self.__dict__ = image.__dict__ self.__dict__ = image.__dict__
return image.load() return image.load()
def _load(self): def _load(self) -> StubHandler | None:
"""(Hook) Find actual image loader.""" """(Hook) Find actual image loader."""
msg = "StubImageFile subclass must implement _load" msg = "StubImageFile subclass must implement _load"
raise NotImplementedError(msg) raise NotImplementedError(msg)
@ -621,7 +622,7 @@ class PyCodecState:
self.xoff = 0 self.xoff = 0
self.yoff = 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 return self.xoff, self.yoff, self.xoff + self.xsize, self.yoff + self.ysize
@ -661,7 +662,7 @@ class PyCodec:
""" """
self.fd = 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 Called from ImageFile to set the core output image for the codec
@ -710,10 +711,10 @@ class PyDecoder(PyCodec):
_pulls_fd = False _pulls_fd = False
@property @property
def pulls_fd(self): def pulls_fd(self) -> bool:
return self._pulls_fd return self._pulls_fd
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
""" """
Override to perform the decoding process. Override to perform the decoding process.
@ -738,6 +739,7 @@ class PyDecoder(PyCodec):
if not rawmode: if not rawmode:
rawmode = self.mode rawmode = self.mode
d = Image._getdecoder(self.mode, "raw", rawmode) d = Image._getdecoder(self.mode, "raw", rawmode)
assert self.im is not None
d.setimage(self.im, self.state.extents()) d.setimage(self.im, self.state.extents())
s = d.decode(data) s = d.decode(data)
@ -760,7 +762,7 @@ class PyEncoder(PyCodec):
_pushes_fd = False _pushes_fd = False
@property @property
def pushes_fd(self): def pushes_fd(self) -> bool:
return self._pushes_fd return self._pushes_fd
def encode(self, bufsize: int) -> tuple[int, int, bytes]: def encode(self, bufsize: int) -> tuple[int, int, bytes]:
@ -775,7 +777,7 @@ class PyEncoder(PyCodec):
msg = "unavailable in base encoder" msg = "unavailable in base encoder"
raise NotImplementedError(msg) 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, If ``pushes_fd`` is ``True``, then this method will be used,
and ``encode()`` will only be called once. and ``encode()`` will only be called once.
@ -787,6 +789,7 @@ class PyEncoder(PyCodec):
return 0, -8 # bad configuration return 0, -8 # bad configuration
bytes_consumed, errcode, data = self.encode(0) bytes_consumed, errcode, data = self.encode(0)
if data: if data:
assert self.fd is not None
self.fd.write(data) self.fd.write(data)
return bytes_consumed, errcode return bytes_consumed, errcode

View File

@ -38,23 +38,27 @@ class ImagePalette:
Defaults to an empty palette. 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.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.palette = palette or bytearray()
self.dirty: int | None = None self.dirty: int | None = None
@property @property
def palette(self): def palette(self) -> Sequence[int] | bytes | bytearray:
return self._palette return self._palette
@palette.setter @palette.setter
def palette(self, palette): def palette(self, palette: Sequence[int] | bytes | bytearray) -> None:
self._colors = None self._colors: dict[tuple[int, ...], int] | None = None
self._palette = palette self._palette = palette
@property @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: if self._colors is None:
mode_len = len(self.mode) mode_len = len(self.mode)
self._colors = {} self._colors = {}
@ -66,9 +70,7 @@ class ImagePalette:
return self._colors return self._colors
@colors.setter @colors.setter
def colors( def colors(self, colors: dict[tuple[int, ...], int]) -> None:
self, colors: dict[tuple[int, int, int] | tuple[int, int, int, int], int]
) -> None:
self._colors = colors self._colors = colors
def copy(self) -> ImagePalette: def copy(self) -> ImagePalette:
@ -82,7 +84,7 @@ class ImagePalette:
return new 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 Get palette contents in format suitable for the low-level
``im.putpalette`` primitive. ``im.putpalette`` primitive.
@ -137,7 +139,7 @@ class ImagePalette:
def getcolor( def getcolor(
self, self,
color: tuple[int, int, int] | tuple[int, int, int, int], color: tuple[int, ...],
image: Image.Image | None = None, image: Image.Image | None = None,
) -> int: ) -> int:
"""Given an rgb tuple, allocate palette entry. """Given an rgb tuple, allocate palette entry.
@ -162,12 +164,13 @@ class ImagePalette:
except KeyError as e: except KeyError as e:
# allocate new color slot # allocate new color slot
index = self._new_color_index(image, e) index = self._new_color_index(image, e)
assert isinstance(self._palette, bytearray)
self.colors[color] = index self.colors[color] = index
if index * 3 < len(self.palette): if index * 3 < len(self.palette):
self._palette = ( self._palette = (
self.palette[: index * 3] self._palette[: index * 3]
+ bytes(color) + bytes(color)
+ self.palette[index * 3 + 3 :] + self._palette[index * 3 + 3 :]
) )
else: else:
self._palette += bytes(color) self._palette += bytes(color)
@ -204,7 +207,7 @@ class ImagePalette:
# Internal # Internal
def raw(rawmode, data) -> ImagePalette: def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette() palette = ImagePalette()
palette.rawmode = rawmode palette.rawmode = rawmode
palette.palette = data palette.palette = data
@ -216,9 +219,9 @@ def raw(rawmode, data) -> ImagePalette:
# Factories # Factories
def make_linear_lut(black, white): def make_linear_lut(black: int, white: float) -> list[int]:
if black == 0: 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" msg = "unavailable when black is non-zero"
raise NotImplementedError(msg) # FIXME raise NotImplementedError(msg) # FIXME
@ -251,15 +254,22 @@ def wedge(mode: str = "RGB") -> ImagePalette:
return ImagePalette(mode, [i // len(mode) for i in palette]) 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 # FIXME: supports GIMP gradients only
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
for paletteHandler in [ paletteHandlers: list[
type[
GimpPaletteFile.GimpPaletteFile
| GimpGradientFile.GimpGradientFile
| PaletteFile.PaletteFile
]
] = [
GimpPaletteFile.GimpPaletteFile, GimpPaletteFile.GimpPaletteFile,
GimpGradientFile.GimpGradientFile, GimpGradientFile.GimpGradientFile,
PaletteFile.PaletteFile, PaletteFile.PaletteFile,
]: ]
for paletteHandler in paletteHandlers:
try: try:
fp.seek(0) fp.seek(0)
lut = paletteHandler(fp).getpalette() lut = paletteHandler(fp).getpalette()

View File

@ -69,19 +69,22 @@ class Dib:
defines the size of the image. defines the size of the image.
""" """
def __init__(self, image, size=None): def __init__(
if hasattr(image, "mode") and hasattr(image, "size"): 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 mode = image.mode
size = image.size size = image.size
else:
mode = image
image = None
if mode not in ["1", "L", "P", "RGB"]: if mode not in ["1", "L", "P", "RGB"]:
mode = Image.getmodebase(mode) mode = Image.getmodebase(mode)
self.image = Image.core.display(mode, size) self.image = Image.core.display(mode, size)
self.mode = mode self.mode = mode
self.size = size self.size = size
if image: if image:
assert not isinstance(image, str)
self.paste(image) self.paste(image)
def expose(self, handle): def expose(self, handle):

View File

@ -37,12 +37,12 @@ from __future__ import annotations
import os import os
import struct import struct
import sys import sys
from typing import IO, TYPE_CHECKING from typing import IO, TYPE_CHECKING, Any, Tuple, cast
from . import Image, ImageFile from . import Image, ImageFile
def isInt(f): def isInt(f: Any) -> int:
try: try:
i = int(f) i = int(f)
if f - i == 0: if f - i == 0:
@ -62,7 +62,7 @@ iforms = [1, 3, -11, -12, -21, -22]
# otherwise returns 0 # 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 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 # header values 1,2,5,12,13,22,23 should be integers
for i in [1, 2, 5, 12, 13, 22, 23]: for i in [1, 2, 5, 12, 13, 22, 23]:
@ -82,7 +82,7 @@ def isSpiderHeader(t):
return labbyt return labbyt
def isSpiderImage(filename): def isSpiderImage(filename: str) -> int:
with open(filename, "rb") as fp: with open(filename, "rb") as fp:
f = fp.read(92) # read 23 * 4 bytes f = fp.read(92) # read 23 * 4 bytes
t = struct.unpack(">23f", f) # try big-endian first t = struct.unpack(">23f", f) # try big-endian first
@ -184,13 +184,15 @@ class SpiderImageFile(ImageFile.ImageFile):
self._open() self._open()
# returns a byte image after rescaling to 0..255 # returns a byte image after rescaling to 0..255
def convert2byte(self, depth=255): def convert2byte(self, depth: int = 255) -> Image.Image:
(minimum, maximum) = self.getextrema() extrema = self.getextrema()
m = 1 assert isinstance(extrema[0], float)
minimum, maximum = cast(Tuple[float, float], extrema)
m: float = 1
if maximum != minimum: if maximum != minimum:
m = depth / (maximum - minimum) m = depth / (maximum - minimum)
b = -m * 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: if TYPE_CHECKING:
from . import ImageTk from . import ImageTk
@ -207,10 +209,10 @@ class SpiderImageFile(ImageFile.ImageFile):
# given a list of filenames, return a list of images # 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""" """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage"""
if filelist is None or len(filelist) < 1: if filelist is None or len(filelist) < 1:
return return None
imglist = [] imglist = []
for img in filelist: for img in filelist: