From e47b1810841be44ca90327259a01ce9d9a71e5ad Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 31 Aug 2024 18:48:16 +1000 Subject: [PATCH] Added type hints --- .ci/requirements-mypy.txt | 1 + Tests/oss-fuzz/fuzz_font.py | 3 +- Tests/oss-fuzz/fuzz_pillow.py | 3 +- docs/reference/internal_modules.rst | 4 +++ pyproject.toml | 4 --- src/PIL/BmpImagePlugin.py | 2 +- src/PIL/Image.py | 7 ++++- src/PIL/ImageFile.py | 7 +++-- src/PIL/ImageFont.py | 6 ++-- src/PIL/Jpeg2KImagePlugin.py | 10 ++++-- src/PIL/MspImagePlugin.py | 2 +- src/PIL/PngImagePlugin.py | 4 ++- src/PIL/TiffImagePlugin.py | 48 +++++++++++++++++++---------- src/PIL/_typing.py | 4 ++- 14 files changed, 70 insertions(+), 35 deletions(-) diff --git a/.ci/requirements-mypy.txt b/.ci/requirements-mypy.txt index 5b09ef64c..dcb3996e2 100644 --- a/.ci/requirements-mypy.txt +++ b/.ci/requirements-mypy.txt @@ -6,6 +6,7 @@ numpy packaging pytest sphinx +types-atheris types-defusedxml types-olefile types-setuptools diff --git a/Tests/oss-fuzz/fuzz_font.py b/Tests/oss-fuzz/fuzz_font.py index 8788d7021..f4e40ea36 100755 --- a/Tests/oss-fuzz/fuzz_font.py +++ b/Tests/oss-fuzz/fuzz_font.py @@ -16,8 +16,9 @@ import atheris +from atheris.import_hook import instrument_imports -with atheris.instrument_imports(): +with instrument_imports(): import sys import fuzzers diff --git a/Tests/oss-fuzz/fuzz_pillow.py b/Tests/oss-fuzz/fuzz_pillow.py index 9137391b6..d58a6f015 100644 --- a/Tests/oss-fuzz/fuzz_pillow.py +++ b/Tests/oss-fuzz/fuzz_pillow.py @@ -14,8 +14,9 @@ import atheris +from atheris.import_hook import instrument_imports -with atheris.instrument_imports(): +with instrument_imports(): import sys import fuzzers diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index 2fb4ff8c0..31d60cd83 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,10 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: IntegralLike + + Typing alias. + .. py:class:: NumpyArray Typing alias. diff --git a/pyproject.toml b/pyproject.toml index e4ae73acf..4042bd9ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,7 +163,3 @@ follow_imports = "silent" warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true -exclude = [ - '^Tests/oss-fuzz/fuzz_font.py$', - '^Tests/oss-fuzz/fuzz_pillow.py$', -] diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index de441c6b5..707fb2192 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -387,7 +387,7 @@ class BmpRleDecoder(ImageFile.PyDecoder): if self.fd.tell() % 2 != 0: self.fd.seek(1, os.SEEK_CUR) rawmode = "L" if self.mode == "L" else "P" - self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1])) + self.set_as_raw(bytes(data), rawmode, (0, self.args[-1])) return -1, 0 diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 95b4c64ee..d27328a83 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -225,6 +225,11 @@ if TYPE_CHECKING: from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from ._typing import NumpyArray, StrOrBytesPath, TypeGuard + + if sys.version_info >= (3, 13): + from types import CapsuleType + else: + CapsuleType = object ID: list[str] = [] OPEN: dict[ str, @@ -1598,7 +1603,7 @@ class Image: self.fp.seek(offset) return child_images - def getim(self): + def getim(self) -> CapsuleType: """ Returns a capsule that points to the internal image memory. diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index a1c33f219..0ecc9053c 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -736,19 +736,22 @@ class PyDecoder(PyCodec): msg = "unavailable in base decoder" raise NotImplementedError(msg) - def set_as_raw(self, data: bytes, rawmode=None) -> None: + def set_as_raw( + self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = () + ) -> None: """ Convenience method to set the internal image from a stream of raw data :param data: Bytes to be set :param rawmode: The rawmode to be used for the decoder. If not specified, it will default to the mode of the image + :param extra: Extra arguments for the decoder. :returns: None """ if not rawmode: rawmode = self.mode - d = Image._getdecoder(self.mode, "raw", rawmode) + d = Image._getdecoder(self.mode, "raw", rawmode, extra) assert self.im is not None d.setimage(self.im, self.state.extents()) s = d.decode(data) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 2ab65bfef..acc7ca1d2 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -34,7 +34,7 @@ import warnings from enum import IntEnum from io import BytesIO from types import ModuleType -from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict +from typing import IO, TYPE_CHECKING, Any, BinaryIO, TypedDict, cast from . import Image from ._typing import StrOrBytesPath @@ -245,7 +245,7 @@ class FreeTypeFont: self.layout_engine = layout_engine - def load_from_bytes(f) -> None: + def load_from_bytes(f: IO[bytes]) -> None: self.font_bytes = f.read() self.font = core.getfont( "", size, index, encoding, self.font_bytes, layout_engine @@ -267,7 +267,7 @@ class FreeTypeFont: font, size, index, encoding, layout_engine=layout_engine ) else: - load_from_bytes(font) + load_from_bytes(cast(IO[bytes], font)) def __getstate__(self) -> list[Any]: return [self.path, self.size, self.index, self.encoding, self.layout_engine] diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index 02c2e48cf..5c88ef0bb 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -18,6 +18,7 @@ from __future__ import annotations import io import os import struct +from collections.abc import Callable from typing import IO, cast from . import Image, ImageFile, ImagePalette, _binary @@ -316,8 +317,13 @@ class Jpeg2KImageFile(ImageFile.ImageFile): else: self.fp.seek(length - 2, os.SEEK_CUR) - @property - def reduce(self): + @property # type: ignore[override] + def reduce( + self, + ) -> ( + Callable[[int | tuple[int, int], tuple[int, int, int, int] | None], Image.Image] + | int + ): # https://github.com/python-pillow/Pillow/issues/4343 found that the # new Image 'reduce' method was shadowed by this plugin's 'reduce' # property. This attempts to allow for both scenarios diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 40e5fa435..987d20c13 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -152,7 +152,7 @@ class MspDecoder(ImageFile.PyDecoder): msg = f"Corrupted MSP file in row {x}" raise OSError(msg) from e - self.set_as_raw(img.getvalue(), ("1", 0, 1)) + self.set_as_raw(img.getvalue(), "1") return -1, 0 diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index dc7face2f..a87e1a625 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -258,7 +258,9 @@ class iTXt(str): tkey: str | bytes | None @staticmethod - def __new__(cls, text, lang=None, tkey=None): + def __new__( + cls, text: str, lang: str | None = None, tkey: str | None = None + ) -> iTXt: """ :param cls: the class to use when creating the instance :param text: value for this key diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index ab23356d4..08d598f77 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -61,6 +61,9 @@ from ._typing import StrOrBytesPath from ._util import is_path from .TiffTags import TYPES +if TYPE_CHECKING: + from ._typing import IntegralLike + logger = logging.getLogger(__name__) # Set these to true to force use of libtiff for reading or writing. @@ -291,22 +294,24 @@ def _accept(prefix: bytes) -> bool: def _limit_rational( val: float | Fraction | IFDRational, max_val: int -) -> tuple[float, float]: +) -> tuple[IntegralLike, IntegralLike]: 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 -def _limit_signed_rational(val, max_val, min_val): +def _limit_signed_rational( + val: IFDRational, max_val: int, min_val: int +) -> tuple[IntegralLike, IntegralLike]: frac = Fraction(val) - n_d = frac.numerator, frac.denominator + n_d: tuple[IntegralLike, IntegralLike] = frac.numerator, frac.denominator - if min(n_d) < min_val: + if min(float(i) for i in n_d) < min_val: n_d = _limit_rational(val, abs(min_val)) - if max(n_d) > max_val: - val = Fraction(*n_d) - n_d = _limit_rational(val, max_val) + n_d_float = tuple(float(i) for i in n_d) + if max(n_d_float) > max_val: + n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val) return n_d @@ -318,8 +323,10 @@ _load_dispatch = {} _write_dispatch = {} -def _delegate(op: str): - def delegate(self, *args): +def _delegate(op: str) -> Any: + def delegate( + self: IFDRational, *args: tuple[float, ...] + ) -> bool | float | Fraction: return getattr(self._val, op)(*args) return delegate @@ -358,7 +365,10 @@ class IFDRational(Rational): self._numerator = value.numerator self._denominator = value.denominator else: - self._numerator = value + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, value) + else: + self._numerator = value self._denominator = denominator if denominator == 0: @@ -371,14 +381,14 @@ class IFDRational(Rational): self._val = Fraction(value / denominator) @property - def numerator(self): + def numerator(self) -> IntegralLike: return self._numerator @property def denominator(self) -> int: return self._denominator - def limit_rational(self, max_denominator: int) -> tuple[float, int]: + def limit_rational(self, max_denominator: int) -> tuple[IntegralLike, int]: """ :param max_denominator: Integer, the maximum denominator value @@ -406,14 +416,18 @@ class IFDRational(Rational): val = float(val) return val == other - def __getstate__(self) -> list[float | Fraction]: + def __getstate__(self) -> list[float | Fraction | IntegralLike]: return [self._val, self._numerator, self._denominator] - def __setstate__(self, state: list[float | Fraction]) -> None: + def __setstate__(self, state: list[float | Fraction | IntegralLike]) -> None: IFDRational.__init__(self, 0) _val, _numerator, _denominator = state + assert isinstance(_val, (float, Fraction)) self._val = _val - self._numerator = _numerator + if TYPE_CHECKING: + self._numerator = cast(IntegralLike, _numerator) + else: + self._numerator = _numerator assert isinstance(_denominator, int) self._denominator = _denominator @@ -471,8 +485,8 @@ def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc return decorator -def _register_writer(idx: int): - def decorator(func): +def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: _write_dispatch[idx] = func # noqa: F821 return func diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index b6bb8d89a..093778464 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -6,6 +6,8 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union if TYPE_CHECKING: + from numbers import _IntegralLike as IntegralLike + try: import numpy.typing as npt @@ -38,4 +40,4 @@ class SupportsRead(Protocol[_T_co]): StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] +__all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]