From accfaf1c092517f7f3918c33b8f9fc38f9ae6a62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 30 Jul 2024 20:20:09 +1000 Subject: [PATCH] Added type hints --- Tests/test_file_jpeg.py | 2 +- Tests/test_image.py | 2 +- docs/reference/plugins.rst | 8 +++++++ src/PIL/EpsImagePlugin.py | 20 ++++++++++++---- src/PIL/Image.py | 48 +++++++++++++++++++++----------------- src/PIL/ImageFile.py | 19 +++++++++++---- src/PIL/JpegImagePlugin.py | 25 ++++++++++++-------- src/PIL/PngImagePlugin.py | 16 +++++++++---- src/PIL/TiffImagePlugin.py | 14 +++++++---- 9 files changed, 101 insertions(+), 53 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 9660e2828..b4b3f71cb 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -154,7 +154,7 @@ class TestFileJpeg: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, int, int]: + def getchannels(im: JpegImagePlugin.JpegImageFile) -> tuple[int, ...]: return tuple(v[0] for v in im.layer) im = hopper() diff --git a/Tests/test_image.py b/Tests/test_image.py index 4fdc41791..d8372789b 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -99,7 +99,7 @@ class TestImage: im = Image.new("L", (100, 100)) p = Pretty() - im._repr_pretty_(p, None) + im._repr_pretty_(p, False) assert p.pretty_output == "" def test_open_formats(self) -> None: diff --git a/docs/reference/plugins.rst b/docs/reference/plugins.rst index 18cd99cf3..454b94d8c 100644 --- a/docs/reference/plugins.rst +++ b/docs/reference/plugins.rst @@ -185,6 +185,14 @@ Plugin reference :undoc-members: :show-inheritance: +:mod:`~PIL.MpoImagePlugin` Module +---------------------------------- + +.. automodule:: PIL.MpoImagePlugin + :members: + :undoc-members: + :show-inheritance: + :mod:`~PIL.MspImagePlugin` Module --------------------------------- diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 7a73d1f69..35915e652 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -65,16 +65,24 @@ def has_ghostscript() -> bool: return gs_binary is not False -def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image: +def Ghostscript( + tile: list[ImageFile._Tile], + size: tuple[int, int], + fp: IO[bytes], + scale: int = 1, + transparency: bool = False, +) -> Image.Image: """Render an image using Ghostscript""" global gs_binary if not has_ghostscript(): msg = "Unable to locate Ghostscript on paths" raise OSError(msg) + assert isinstance(gs_binary, str) # Unpack decoder tile - decoder, tile, offset, data = tile[0] - length, bbox = data + args = tile[0].args + assert isinstance(args, tuple) + length, bbox = args # Hack to support hi-res rendering scale = int(scale) or 1 @@ -227,7 +235,11 @@ class EpsImageFile(ImageFile.ImageFile): # put floating point values there anyway. box = [int(float(i)) for i in v.split()] self._size = box[2] - box[0], box[3] - box[1] - self.tile = [("eps", (0, 0) + self.size, offset, (length, box))] + self.tile = [ + ImageFile._Tile( + "eps", (0, 0) + self.size, offset, (length, box) + ) + ] except Exception: pass return True diff --git a/src/PIL/Image.py b/src/PIL/Image.py index ae8634539..ebf4f46c4 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -220,7 +220,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): if TYPE_CHECKING: from xml.etree.ElementTree import Element - from . import ImageFile, ImagePalette + from . import ImageFile, ImagePalette, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard ID: list[str] = [] OPEN: dict[ @@ -676,7 +676,7 @@ class Image: id(self), ) - def _repr_pretty_(self, p, cycle) -> None: + def _repr_pretty_(self, p, cycle: bool) -> None: """IPython plain text display support""" # Same as __repr__ but without unpredictable id(self), @@ -1551,6 +1551,7 @@ class Image: ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) if ifd1 and ifd1.get(513): + assert exif._info is not None ifds.append((ifd1, exif._info.next)) offset = None @@ -1560,12 +1561,13 @@ class Image: offset = current_offset fp = self.fp - thumbnail_offset = ifd.get(513) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) - fp = io.BytesIO(data) + if ifd is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + data = self.fp.read(ifd.get(514)) + fp = io.BytesIO(data) with open(fp) as im: from . import TiffImagePlugin @@ -3869,14 +3871,14 @@ class Exif(_ExifBase): bigtiff = False _loaded = False - def __init__(self): - self._data = {} - self._hidden_data = {} - self._ifds = {} - self._info = None - self._loaded_exif = None + def __init__(self) -> None: + self._data: dict[int, Any] = {} + self._hidden_data: dict[int, Any] = {} + self._ifds: dict[int, dict[int, Any]] = {} + self._info: TiffImagePlugin.ImageFileDirectory_v2 | None = None + self._loaded_exif: bytes | None = None - def _fixup(self, value): + def _fixup(self, value: Any) -> Any: try: if len(value) == 1 and isinstance(value, tuple): return value[0] @@ -3884,24 +3886,26 @@ class Exif(_ExifBase): pass return value - def _fixup_dict(self, src_dict): + def _fixup_dict(self, src_dict: dict[int, Any]) -> dict[int, Any]: # Helper function # returns a dict with any single item tuples/lists as individual values return {k: self._fixup(v) for k, v in src_dict.items()} - def _get_ifd_dict(self, offset: int, group: int | None = None): + def _get_ifd_dict( + self, offset: int, group: int | None = None + ) -> dict[int, Any] | None: try: # an offset pointer to the location of the nested embedded IFD. # It should be a long, but may be corrupted. self.fp.seek(offset) except (KeyError, TypeError): - pass + return None else: from . import TiffImagePlugin info = TiffImagePlugin.ImageFileDirectory_v2(self.head, group=group) info.load(self.fp) - return self._fixup_dict(info) + return self._fixup_dict(dict(info)) def _get_head(self) -> bytes: version = b"\x2B" if self.bigtiff else b"\x2A" @@ -3966,7 +3970,7 @@ class Exif(_ExifBase): self.fp.seek(offset) self._info.load(self.fp) - def _get_merged_dict(self): + def _get_merged_dict(self) -> dict[int, Any]: merged_dict = dict(self) # get EXIF extension @@ -4124,7 +4128,7 @@ class Exif(_ExifBase): keys.update(self._info) return len(keys) - def __getitem__(self, tag: int): + def __getitem__(self, tag: int) -> Any: if self._info is not None and tag not in self._data and tag in self._info: self._data[tag] = self._fixup(self._info[tag]) del self._info[tag] @@ -4133,7 +4137,7 @@ class Exif(_ExifBase): def __contains__(self, tag: object) -> bool: return tag in self._data or (self._info is not None and tag in self._info) - def __setitem__(self, tag: int, value) -> None: + def __setitem__(self, tag: int, value: Any) -> None: if self._info is not None and tag in self._info: del self._info[tag] self._data[tag] = value diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e4a7dba44..2a8846c1a 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -86,7 +86,7 @@ def raise_oserror(error: int) -> OSError: raise _get_oserror(error, encoder=False) -def _tilesort(t) -> int: +def _tilesort(t: _Tile) -> int: # sort on offset return t[2] @@ -161,7 +161,7 @@ class ImageFile(Image.Image): return Image.MIME.get(self.format.upper()) return None - def __setstate__(self, state) -> None: + def __setstate__(self, state: list[Any]) -> None: self.tile = [] super().__setstate__(state) @@ -525,7 +525,7 @@ class Parser: # -------------------------------------------------------------------- -def _save(im, fp, tile, bufsize: int = 0) -> None: +def _save(im: Image.Image, fp: IO[bytes], tile, bufsize: int = 0) -> None: """Helper to save image based on tile list :param im: Image object. @@ -554,7 +554,12 @@ def _save(im, fp, tile, bufsize: int = 0) -> None: def _encode_tile( - im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None + im: Image.Image, + fp: IO[bytes], + tile: list[_Tile], + bufsize: int, + fh, + exc: BaseException | None = None, ) -> None: for encoder_name, extents, offset, args in tile: if offset > 0: @@ -664,7 +669,11 @@ class PyCodec: """ self.fd = fd - def setimage(self, im, extents=None): + def setimage( + self, + im: Image.core.ImagingCore, + extents: tuple[int, int, int, int] | None = None, + ) -> None: """ Called from ImageFile to set the core output image for the codec diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index d83d60b7b..3a83f9ca6 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -42,7 +42,7 @@ import subprocess import sys import tempfile import warnings -from typing import IO, Any +from typing import IO, TYPE_CHECKING, Any from . import Image, ImageFile from ._binary import i16be as i16 @@ -51,6 +51,9 @@ from ._binary import o8 from ._binary import o16be as o16 from .JpegPresets import presets +if TYPE_CHECKING: + from .MpoImagePlugin import MpoImageFile + # # Parser @@ -329,7 +332,7 @@ class JpegImageFile(ImageFile.ImageFile): format = "JPEG" format_description = "JPEG (ISO 10918)" - def _open(self): + def _open(self) -> None: s = self.fp.read(3) if not _accept(s): @@ -342,13 +345,13 @@ class JpegImageFile(ImageFile.ImageFile): self._exif_offset = 0 # JPEG specifics (internal) - self.layer = [] - self.huffman_dc = {} - self.huffman_ac = {} - self.quantization = {} - self.app = {} # compatibility - self.applist = [] - self.icclist = [] + self.layer: list[tuple[int, int, int, int]] = [] + self.huffman_dc: dict[Any, Any] = {} + self.huffman_ac: dict[Any, Any] = {} + self.quantization: dict[int, list[int]] = {} + self.app: dict[str, bytes] = {} # compatibility + self.applist: list[tuple[str, bytes]] = [] + self.icclist: list[bytes] = [] while True: i = s[0] @@ -831,7 +834,9 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: ## # Factory for making JPEG and MPO instances -def jpeg_factory(fp: IO[bytes] | None = None, filename: str | bytes | None = None): +def jpeg_factory( + fp: IO[bytes] | None = None, filename: str | bytes | None = None +) -> JpegImageFile | MpoImageFile: im = JpegImageFile(fp, filename) try: mpheader = im._getmp() diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index 52cfcd13a..910fa9755 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -40,7 +40,7 @@ import warnings import zlib from collections.abc import Callable from enum import IntEnum -from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn +from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn, cast from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence from ._binary import i16be as i16 @@ -1223,7 +1223,11 @@ def _write_multiple_frames( if default_image: if im.mode != mode: im = im.convert(mode) - ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)]) + ImageFile._save( + im, + cast(IO[bytes], _idat(fp, chunk)), + [("zip", (0, 0) + im.size, 0, rawmode)], + ) seq_num = 0 for frame, frame_data in enumerate(im_frames): @@ -1258,14 +1262,14 @@ def _write_multiple_frames( # first frame must be in IDAT chunks for backwards compatibility ImageFile._save( im_frame, - _idat(fp, chunk), + cast(IO[bytes], _idat(fp, chunk)), [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) else: fdat_chunks = _fdat(fp, chunk, seq_num) ImageFile._save( im_frame, - fdat_chunks, + cast(IO[bytes], fdat_chunks), [("zip", (0, 0) + im_frame.size, 0, rawmode)], ) seq_num = fdat_chunks.seq_num @@ -1465,7 +1469,9 @@ def _save( ) if single_im: ImageFile._save( - single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)] + single_im, + cast(IO[bytes], _idat(fp, chunk)), + [("zip", (0, 0) + single_im.size, 0, rawmode)], ) if info: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index dd013a0cf..d9d1bab5a 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -288,8 +288,10 @@ def _accept(prefix: bytes) -> bool: return prefix[:4] in PREFIXES -def _limit_rational(val, max_val): - inv = abs(val) > 1 +def _limit_rational( + val: float | Fraction | IFDRational, max_val: int +) -> tuple[float, float]: + inv = abs(float(val)) > 1 n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) return n_d[::-1] if inv else n_d @@ -792,7 +794,9 @@ class ImageFileDirectory_v2(_IFDv2Base): return value + b"\0" @_register_loader(5, 8) - def load_rational(self, data, legacy_api: bool = True): + def load_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: @@ -801,7 +805,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(5) - def write_rational(self, *values) -> bytes: + def write_rational(self, *values: IFDRational) -> bytes: return b"".join( self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values ) @@ -828,7 +832,7 @@ class ImageFileDirectory_v2(_IFDv2Base): return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @_register_writer(10) - def write_signed_rational(self, *values) -> bytes: + def write_signed_rational(self, *values: IFDRational) -> bytes: return b"".join( self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31))) for frac in values