From 873770978197484b284a267d247d1b832afc2a24 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 13:32:59 +1000 Subject: [PATCH 1/4] Added return type to ImageFile.load() --- src/PIL/Image.py | 8 +++----- src/PIL/ImageFile.py | 19 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ebf4f46c4..a6eefff56 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -218,9 +218,10 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: + import mmap from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette, TiffImagePlugin + from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -612,7 +613,7 @@ class Image: logger.debug("Error closing: %s", msg) if getattr(self, "map", None): - self.map = None + self.map: mmap.mmap | None = None # Instead of simply setting to None, we're setting up a # deferred error that will better explain that the core image @@ -1336,9 +1337,6 @@ class Image: self.load() return self._new(self.im.expand(xmargin, ymargin)) - if TYPE_CHECKING: - from . import ImageFilter - def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image: """ Filters this image using the given filter. For a list of diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 2a8846c1a..829082e94 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -174,7 +174,7 @@ class ImageFile(Image.Image): self.fp.close() self.fp = None - def load(self): + def load(self) -> Image.core.PixelAccess | None: """Load image data based on tile list""" if self.tile is None: @@ -185,7 +185,7 @@ class ImageFile(Image.Image): if not self.tile: return pixel - self.map = None + self.map: mmap.mmap | None = None use_mmap = self.filename and len(self.tile) == 1 # As of pypy 2.1.0, memory mapping was failing here. use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") @@ -193,17 +193,17 @@ class ImageFile(Image.Image): readonly = 0 # look for read/seek overrides - try: + if hasattr(self, "load_read"): read = self.load_read # don't use mmap if there are custom read/seek functions use_mmap = False - except AttributeError: + else: read = self.fp.read - try: + if hasattr(self, "load_seek"): seek = self.load_seek use_mmap = False - except AttributeError: + else: seek = self.fp.seek if use_mmap: @@ -243,11 +243,8 @@ class ImageFile(Image.Image): # sort tiles in file order self.tile.sort(key=_tilesort) - try: - # FIXME: This is a hack to handle TIFF's JpegTables tag. - prefix = self.tile_prefix - except AttributeError: - prefix = b"" + # FIXME: This is a hack to handle TIFF's JpegTables tag. + prefix = getattr(self, "tile_prefix", b"") # Remove consecutive duplicates that only differ by their offset self.tile = [ From 497080f63b62c9acbb427c7e554f222de70fe3a9 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Aug 2024 15:20:34 +1000 Subject: [PATCH 2/4] Added type hint to ImageFile._save tile parameter --- Tests/test_imagefile.py | 30 ++++++++++++++++++++++++------ src/PIL/BlpImagePlugin.py | 2 +- src/PIL/BmpImagePlugin.py | 4 +++- src/PIL/EpsImagePlugin.py | 2 +- src/PIL/GifImagePlugin.py | 8 ++++++-- src/PIL/IcoImagePlugin.py | 4 +++- src/PIL/ImImagePlugin.py | 4 +++- src/PIL/ImageFile.py | 4 ++-- src/PIL/Jpeg2KImagePlugin.py | 2 +- src/PIL/JpegImagePlugin.py | 4 +++- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PalmImagePlugin.py | 4 +++- src/PIL/PcxImagePlugin.py | 4 +++- src/PIL/PdfImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 8 ++++---- src/PIL/PpmImagePlugin.py | 4 +++- src/PIL/SpiderImagePlugin.py | 4 +++- src/PIL/TgaImagePlugin.py | 8 ++++++-- src/PIL/XbmImagePlugin.py | 2 +- 19 files changed, 72 insertions(+), 30 deletions(-) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index fe7d44785..95e91db83 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -317,7 +317,13 @@ class TestPyEncoder(CodecsTest): fp = BytesIO() ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] + im, + fp, + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB" + ) + ], ) assert MockPyEncoder.last @@ -333,7 +339,7 @@ class TestPyEncoder(CodecsTest): im.tile = [("MOCK", None, 32, None)] fp = BytesIO() - ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) + ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")]) assert MockPyEncoder.last assert MockPyEncoder.last.state.xoff == 0 @@ -350,7 +356,9 @@ class TestPyEncoder(CodecsTest): MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")], ) last: MockPyEncoder | None = MockPyEncoder.last assert last @@ -358,7 +366,9 @@ class TestPyEncoder(CodecsTest): with pytest.raises(ValueError): ImageFile._save( - im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")] + im, + fp, + [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")], ) def test_oversize(self) -> None: @@ -371,14 +381,22 @@ class TestPyEncoder(CodecsTest): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB" + ) + ], ) with pytest.raises(ValueError): ImageFile._save( im, fp, - [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")], + [ + ImageFile._Tile( + "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB" + ) + ], ) def test_encode(self) -> None: diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 6d71049a9..569f2c9bf 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(struct.pack(" None: if bits != 32: and_mask = Image.new("1", size) ImageFile._save( - and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))] + and_mask, + image_io, + [ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))], ) else: frame.save(image_io, "png") diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 2fb7ecd52..b94165089 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: palette += im_palette[colors * i : colors * (i + 1)] palette += b"\x00" * (256 - colors) fp.write(palette) # 768 bytes - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))] + ) # diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 829082e94..73554fa53 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -93,7 +93,7 @@ def _tilesort(t: _Tile) -> int: class _Tile(NamedTuple): codec_name: str - extents: tuple[int, int, int, int] + extents: tuple[int, int, int, int] | None offset: int args: tuple[Any, ...] | str | None @@ -522,7 +522,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile: list[_Tile], bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index ef9107f00..02c2e48cf 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -419,7 +419,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: plt, ) - ImageFile._save(im, fp, [("jpeg2k", (0, 0) + im.size, 0, kind)]) + ImageFile._save(im, fp, [ImageFile._Tile("jpeg2k", (0, 0) + im.size, 0, kind)]) # ------------------------------------------------------------ diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index fc897e2b9..bd4539be4 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -826,7 +826,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # Ensure that our buffer is big enough. Same with the icc_profile block. bufsize = max(bufsize, len(exif) + 5, len(extra) + 1) - ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) + ImageFile._save( + im, fp, [ImageFile._Tile("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize + ) def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 0a75c868b..40e5fa435 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -188,7 +188,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(o16(h)) # image body - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) + ImageFile._save(im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 32, ("1", 0, 1))]) # diff --git a/src/PIL/PalmImagePlugin.py b/src/PIL/PalmImagePlugin.py index 1735070f8..62bf5f542 100644 --- a/src/PIL/PalmImagePlugin.py +++ b/src/PIL/PalmImagePlugin.py @@ -213,7 +213,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ) # now convert data to raw form - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, rowbytes, 1))] + ) if hasattr(fp, "flush"): fp.flush() diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py index dd42003b5..4fb04715b 100644 --- a/src/PIL/PcxImagePlugin.py +++ b/src/PIL/PcxImagePlugin.py @@ -198,7 +198,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: assert fp.tell() == 128 - ImageFile._save(im, fp, [("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))]) + ImageFile._save( + im, fp, [ImageFile._Tile("pcx", (0, 0) + im.size, 0, (rawmode, bits * planes))] + ) if im.mode == "P": # colour palette diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index 7fc1108bb..e9c20ddc1 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -138,7 +138,7 @@ def _write_image( op = io.BytesIO() if decode_filter == "ASCIIHexDecode": - ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) + ImageFile._save(im, op, [ImageFile._Tile("hex", (0, 0) + im.size, 0, im.mode)]) elif decode_filter == "CCITTFaxDecode": im.save( op, diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 910fa9755..fc20b18a8 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -1226,7 +1226,7 @@ def _write_multiple_frames( ImageFile._save( im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im.size, 0, rawmode)], ) seq_num = 0 @@ -1263,14 +1263,14 @@ def _write_multiple_frames( ImageFile._save( im_frame, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, cast(IO[bytes], fdat_chunks), - [("zip", (0, 0) + im_frame.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num return None @@ -1471,7 +1471,7 @@ def _save( ImageFile._save( single_im, cast(IO[bytes], _idat(fp, chunk)), - [("zip", (0, 0) + single_im.size, 0, rawmode)], + [ImageFile._Tile("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 16c9ccbba..7bdaa9fe7 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -353,7 +353,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: elif head == b"Pf": fp.write(b"-1.0\n") row_order = -1 if im.mode == "F" else 1 - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))] + ) # diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index a07101e54..7045ab566 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -278,7 +278,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.writelines(hdr) rawmode = "F;32NF" # 32-bit native floating point - ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) + ImageFile._save( + im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))] + ) def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py index 39104aece..a43aae1ec 100644 --- a/src/PIL/TgaImagePlugin.py +++ b/src/PIL/TgaImagePlugin.py @@ -238,11 +238,15 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if rle: ImageFile._save( - im, fp, [("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))] + im, + fp, + [ImageFile._Tile("tga_rle", (0, 0) + im.size, 0, (rawmode, orientation))], ) else: ImageFile._save( - im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))] + im, + fp, + [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, orientation))], ) # write targa version 2 footer diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py index 6d11bbfcf..6c2e32804 100644 --- a/src/PIL/XbmImagePlugin.py +++ b/src/PIL/XbmImagePlugin.py @@ -85,7 +85,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: fp.write(b"static char im_bits[] = {\n") - ImageFile._save(im, fp, [("xbm", (0, 0) + im.size, 0, None)]) + ImageFile._save(im, fp, [ImageFile._Tile("xbm", (0, 0) + im.size, 0, None)]) fp.write(b"};\n") From 8afb7ddb4ed265caf67213f4fd5416cd29ae24a1 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 08:08:43 +1000 Subject: [PATCH 3/4] Added type hints --- .ci/requirements-mypy.txt | 1 + docs/example/DdsImagePlugin.py | 29 +++++++++++++++------------ src/PIL/TiffImagePlugin.py | 36 ++++++++++++++++++++-------------- tox.ini | 2 +- winbuild/build_prepare.py | 12 ++++++------ 5 files changed, 45 insertions(+), 35 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 23792281b..47fc64399 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -5,6 +5,7 @@ ipython numpy packaging pytest +sphinx types-defusedxml types-olefile types-setuptools diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 2a2a0ba29..caa852b1f 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -14,6 +14,7 @@ from __future__ import annotations import struct from io import BytesIO +from typing import IO from PIL import Image, ImageFile @@ -94,26 +95,26 @@ DXT3_FOURCC = 0x33545844 DXT5_FOURCC = 0x35545844 -def _decode565(bits): +def _decode565(bits: int) -> tuple[int, int, int]: a = ((bits >> 11) & 0x1F) << 3 b = ((bits >> 5) & 0x3F) << 2 c = (bits & 0x1F) << 3 return a, b, c -def _c2a(a, b): +def _c2a(a: int, b: int) -> int: return (2 * a + b) // 3 -def _c2b(a, b): +def _c2b(a: int, b: int) -> int: return (a + b) // 2 -def _c3(a, b): +def _c3(a: int, b: int) -> int: return (2 * b + a) // 3 -def _dxt1(data, width, height): +def _dxt1(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -151,7 +152,7 @@ def _dxt1(data, width, height): return bytes(ret) -def _dxtc_alpha(a0, a1, ac0, ac1, ai): +def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int: if ai <= 12: ac = (ac0 >> ai) & 7 elif ai == 15: @@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai): return alpha -def _dxt5(data, width, height): +def _dxt5(data: IO[bytes], width: int, height: int) -> bytes: # TODO implement this function as pixel format in decode.c ret = bytearray(4 * width * height) @@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile): format = "DDS" format_description = "DirectDraw Surface" - def _open(self): + def _open(self) -> None: if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) @@ -242,19 +243,20 @@ class DdsImageFile(ImageFile.ImageFile): elif fourcc == b"DXT5": self.decoder = "DXT5" else: - msg = f"Unimplemented pixel format {fourcc}" + msg = f"Unimplemented pixel format {repr(fourcc)}" raise NotImplementedError(msg) self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))] - def load_seek(self, pos): + def load_seek(self, pos: int) -> None: pass class DXT1Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -266,7 +268,8 @@ class DXT1Decoder(ImageFile.PyDecoder): class DXT5Decoder(ImageFile.PyDecoder): _pulls_fd = True - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: + assert self.fd is not None try: self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize)) except struct.error as e: @@ -279,7 +282,7 @@ Image.register_decoder("DXT1", DXT1Decoder) Image.register_decoder("DXT5", DXT5Decoder) -def _accept(prefix): +def _accept(prefix: bytes) -> bool: return prefix[:4] == b"DDS " diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index deae199d5..2ce8a6c2a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -456,8 +456,11 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -def _register_loader(idx: int, size: int): - def decorator(func): +_LoaderFunc = Callable[["ImageFileDirectory_v2", bytes, bool], Any] + + +def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc]: + def decorator(func: _LoaderFunc) -> _LoaderFunc: from .TiffTags import TYPES if func.__name__.startswith("load_"): @@ -482,12 +485,13 @@ def _register_basic(idx_fmt_name: tuple[int, str, str]) -> None: idx, fmt, name = idx_fmt_name TYPES[idx] = name size = struct.calcsize(f"={fmt}") - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) + + def basic_handler( + self: ImageFileDirectory_v2, data: bytes, legacy_api: bool = True + ) -> tuple[Any, ...]: + return self._unpack(f"{len(data) // size}{fmt}", data) + + _load_dispatch[idx] = size, basic_handler # noqa: F821 _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 b"".join(self._pack(fmt, value) for value in values) ) @@ -560,7 +564,7 @@ class ImageFileDirectory_v2(_IFDv2Base): """ - _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _load_dispatch: dict[int, tuple[int, _LoaderFunc]] = {} _write_dispatch: dict[int, Callable[..., Any]] = {} def __init__( @@ -653,10 +657,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __contains__(self, tag: object) -> bool: return tag in self._tags_v2 or tag in self._tagdata - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: self._setitem(tag, value, self.legacy_api) - def _setitem(self, tag: int, value, legacy_api: bool) -> None: + def _setitem(self, tag: int, value: Any, legacy_api: bool) -> None: basetypes = (Number, bytes, str) info = TiffTags.lookup(tag, self.group) @@ -744,10 +748,10 @@ class ImageFileDirectory_v2(_IFDv2Base): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) - def _unpack(self, fmt: str, data: bytes): + def _unpack(self, fmt: str, data: bytes) -> tuple[Any, ...]: return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values) -> bytes: + def _pack(self, fmt: str, *values: Any) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -824,7 +828,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value @_register_loader(10, 8) - def load_signed_rational(self, data: bytes, legacy_api: bool = True): + def load_signed_rational( + self, data: bytes, legacy_api: bool = True + ) -> tuple[tuple[int, int] | IFDRational, ...]: vals = self._unpack(f"{len(data) // 4}l", data) def combine(a: int, b: int) -> tuple[int, int] | IFDRational: @@ -1088,7 +1094,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: for legacy_api in (False, True): self._setitem(tag, value, legacy_api) diff --git a/tox.ini b/tox.ini index 4b4059455..70b8bf145 100644 --- a/tox.ini +++ b/tox.ini @@ -36,4 +36,4 @@ deps = extras = typing commands = - mypy src Tests {posargs} + mypy docs src winbuild Tests {posargs} diff --git a/winbuild/build_prepare.py b/winbuild/build_prepare.py index 7129699eb..1021e4f22 100644 --- a/winbuild/build_prepare.py +++ b/winbuild/build_prepare.py @@ -7,6 +7,7 @@ import re import shutil import struct import subprocess +from typing import Any def cmd_cd(path: str) -> str: @@ -43,21 +44,19 @@ def cmd_nmake( target: str = "", params: list[str] | None = None, ) -> str: - params = "" if params is None else " ".join(params) - return " ".join( [ "{nmake}", "-nologo", f'-f "{makefile}"' if makefile is not None else "", - f"{params}", + f'{" ".join(params)}' if params is not None else "", f'"{target}"', ] ) def cmds_cmake( - target: str | tuple[str, ...] | list[str], *params, build_dir: str = "." + target: str | tuple[str, ...] | list[str], *params: str, build_dir: str = "." ) -> list[str]: if not isinstance(target, str): target = " ".join(target) @@ -129,7 +128,7 @@ V["ZLIB_DOTLESS"] = V["ZLIB"].replace(".", "") # dependencies, listed in order of compilation -DEPS = { +DEPS: dict[str, dict[str, Any]] = { "libjpeg": { "url": f"{SF_PROJECTS}/libjpeg-turbo/files/{V['JPEGTURBO']}/" f"libjpeg-turbo-{V['JPEGTURBO']}.tar.gz/download", @@ -538,7 +537,7 @@ def write_script( print(" " + line) -def get_footer(dep: dict) -> list[str]: +def get_footer(dep: dict[str, Any]) -> list[str]: lines = [] for out in dep.get("headers", []): lines.append(cmd_copy(out, "{inc_dir}")) @@ -583,6 +582,7 @@ def build_dep(name: str, prefs: dict[str, str], verbose: bool) -> str: license_text += f.read() if "license_pattern" in dep: match = re.search(dep["license_pattern"], license_text, re.DOTALL) + assert match is not None license_text = "\n".join(match.groups()) assert len(license_text) > 50 with open(os.path.join(license_dir, f"{directory}.txt"), "w") as f: From d4c72da6b2ae5aabc45f679f8feeaf2726976a2b Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Aug 2024 14:10:31 +1000 Subject: [PATCH 4/4] Added type hints to example code --- docs/handbook/image-file-formats.rst | 12 +++++++----- docs/handbook/tutorial.rst | 6 +++--- docs/handbook/writing-your-own-image-plugin.rst | 6 +++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/handbook/image-file-formats.rst b/docs/handbook/image-file-formats.rst index 861e09a43..ca0e05eb6 100644 --- a/docs/handbook/image-file-formats.rst +++ b/docs/handbook/image-file-formats.rst @@ -1517,19 +1517,21 @@ To add other read or write support, use :py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF handler. :: - from PIL import Image + from typing import IO + + from PIL import Image, ImageFile from PIL import WmfImagePlugin - class WmfHandler: - def open(self, im): + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: ... - def load(self, im): + def load(self, im: ImageFile.StubImageFile) -> Image.Image: ... return image - def save(self, im, fp, filename): + def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: ... diff --git a/docs/handbook/tutorial.rst b/docs/handbook/tutorial.rst index c36011362..3df8e0d20 100644 --- a/docs/handbook/tutorial.rst +++ b/docs/handbook/tutorial.rst @@ -186,7 +186,7 @@ Rolling an image :: - def roll(im, delta): + def roll(im: Image.Image, delta: int) -> Image.Image: """Roll an image sideways.""" xsize, ysize = im.size @@ -211,7 +211,7 @@ Merging images :: - def merge(im1, im2): + def merge(im1: Image.Image, im2: Image.Image) -> Image.Image: w = im1.size[0] + im2.size[0] h = max(im1.size[1], im2.size[1]) im = Image.new("RGBA", (w, h)) @@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality. import glob from PIL import Image - def compress_image(source_path, dest_path): + def compress_image(source_path: str, dest_path: str) -> None: with Image.open(source_path) as img: if img.mode != "RGB": img = img.convert("RGB") diff --git a/docs/handbook/writing-your-own-image-plugin.rst b/docs/handbook/writing-your-own-image-plugin.rst index 956d63aa7..2e853224d 100644 --- a/docs/handbook/writing-your-own-image-plugin.rst +++ b/docs/handbook/writing-your-own-image-plugin.rst @@ -53,7 +53,7 @@ true color. from PIL import Image, ImageFile - def _accept(prefix): + def _accept(prefix: bytes) -> bool: return prefix[:4] == b"SPAM" @@ -62,7 +62,7 @@ true color. format = "SPAM" format_description = "Spam raster image" - def _open(self): + def _open(self) -> None: header = self.fp.read(128).split() @@ -82,7 +82,7 @@ true color. raise SyntaxError(msg) # data descriptor - self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] + self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))] Image.register_open(SpamImageFile.format, SpamImageFile, _accept)