Added type hints

This commit is contained in:
Andrew Murray 2024-08-31 18:48:16 +10:00
parent 44c2ff3f0b
commit e47b181084
14 changed files with 70 additions and 35 deletions

View File

@ -6,6 +6,7 @@ numpy
packaging packaging
pytest pytest
sphinx sphinx
types-atheris
types-defusedxml types-defusedxml
types-olefile types-olefile
types-setuptools types-setuptools

View File

@ -16,8 +16,9 @@
import atheris import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports(): with instrument_imports():
import sys import sys
import fuzzers import fuzzers

View File

@ -14,8 +14,9 @@
import atheris import atheris
from atheris.import_hook import instrument_imports
with atheris.instrument_imports(): with instrument_imports():
import sys import sys
import fuzzers import fuzzers

View File

@ -33,6 +33,10 @@ Internal Modules
Provides a convenient way to import type hints that are not available Provides a convenient way to import type hints that are not available
on some Python versions. on some Python versions.
.. py:class:: IntegralLike
Typing alias.
.. py:class:: NumpyArray .. py:class:: NumpyArray
Typing alias. Typing alias.

View File

@ -163,7 +163,3 @@ follow_imports = "silent"
warn_redundant_casts = true warn_redundant_casts = true
warn_unreachable = true warn_unreachable = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = [
'^Tests/oss-fuzz/fuzz_font.py$',
'^Tests/oss-fuzz/fuzz_pillow.py$',
]

View File

@ -387,7 +387,7 @@ class BmpRleDecoder(ImageFile.PyDecoder):
if self.fd.tell() % 2 != 0: if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR) self.fd.seek(1, os.SEEK_CUR)
rawmode = "L" if self.mode == "L" else "P" 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 return -1, 0

View File

@ -225,6 +225,11 @@ if TYPE_CHECKING:
from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin from . import ImageFile, ImageFilter, ImagePalette, ImageQt, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
if sys.version_info >= (3, 13):
from types import CapsuleType
else:
CapsuleType = object
ID: list[str] = [] ID: list[str] = []
OPEN: dict[ OPEN: dict[
str, str,
@ -1598,7 +1603,7 @@ class Image:
self.fp.seek(offset) self.fp.seek(offset)
return child_images return child_images
def getim(self): def getim(self) -> CapsuleType:
""" """
Returns a capsule that points to the internal image memory. Returns a capsule that points to the internal image memory.

View File

@ -736,19 +736,22 @@ class PyDecoder(PyCodec):
msg = "unavailable in base decoder" msg = "unavailable in base decoder"
raise NotImplementedError(msg) 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 Convenience method to set the internal image from a stream of raw data
:param data: Bytes to be set :param data: Bytes to be set
:param rawmode: The rawmode to be used for the decoder. :param rawmode: The rawmode to be used for the decoder.
If not specified, it will default to the mode of the image If not specified, it will default to the mode of the image
:param extra: Extra arguments for the decoder.
:returns: None :returns: None
""" """
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, extra)
assert self.im is not None 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)

View File

@ -34,7 +34,7 @@ import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from types import ModuleType 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 . import Image
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
@ -245,7 +245,7 @@ class FreeTypeFont:
self.layout_engine = layout_engine 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_bytes = f.read()
self.font = core.getfont( self.font = core.getfont(
"", size, index, encoding, self.font_bytes, layout_engine "", size, index, encoding, self.font_bytes, layout_engine
@ -267,7 +267,7 @@ class FreeTypeFont:
font, size, index, encoding, layout_engine=layout_engine font, size, index, encoding, layout_engine=layout_engine
) )
else: else:
load_from_bytes(font) load_from_bytes(cast(IO[bytes], font))
def __getstate__(self) -> list[Any]: def __getstate__(self) -> list[Any]:
return [self.path, self.size, self.index, self.encoding, self.layout_engine] return [self.path, self.size, self.index, self.encoding, self.layout_engine]

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import io import io
import os import os
import struct import struct
from collections.abc import Callable
from typing import IO, cast from typing import IO, cast
from . import Image, ImageFile, ImagePalette, _binary from . import Image, ImageFile, ImagePalette, _binary
@ -316,8 +317,13 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
else: else:
self.fp.seek(length - 2, os.SEEK_CUR) self.fp.seek(length - 2, os.SEEK_CUR)
@property @property # type: ignore[override]
def reduce(self): 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 # https://github.com/python-pillow/Pillow/issues/4343 found that the
# new Image 'reduce' method was shadowed by this plugin's 'reduce' # new Image 'reduce' method was shadowed by this plugin's 'reduce'
# property. This attempts to allow for both scenarios # property. This attempts to allow for both scenarios

View File

@ -152,7 +152,7 @@ class MspDecoder(ImageFile.PyDecoder):
msg = f"Corrupted MSP file in row {x}" msg = f"Corrupted MSP file in row {x}"
raise OSError(msg) from e raise OSError(msg) from e
self.set_as_raw(img.getvalue(), ("1", 0, 1)) self.set_as_raw(img.getvalue(), "1")
return -1, 0 return -1, 0

View File

@ -258,7 +258,9 @@ class iTXt(str):
tkey: str | bytes | None tkey: str | bytes | None
@staticmethod @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 cls: the class to use when creating the instance
:param text: value for this key :param text: value for this key

View File

@ -61,6 +61,9 @@ from ._typing import StrOrBytesPath
from ._util import is_path from ._util import is_path
from .TiffTags import TYPES from .TiffTags import TYPES
if TYPE_CHECKING:
from ._typing import IntegralLike
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Set these to true to force use of libtiff for reading or writing. # 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( def _limit_rational(
val: float | Fraction | IFDRational, max_val: int val: float | Fraction | IFDRational, max_val: int
) -> tuple[float, float]: ) -> tuple[IntegralLike, IntegralLike]:
inv = abs(float(val)) > 1 inv = abs(float(val)) > 1
n_d = IFDRational(1 / val if inv else val).limit_rational(max_val) n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
return n_d[::-1] if inv else n_d 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) 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)) n_d = _limit_rational(val, abs(min_val))
if max(n_d) > max_val: n_d_float = tuple(float(i) for i in n_d)
val = Fraction(*n_d) if max(n_d_float) > max_val:
n_d = _limit_rational(val, max_val) n_d = _limit_rational(n_d_float[0] / n_d_float[1], max_val)
return n_d return n_d
@ -318,8 +323,10 @@ _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
def _delegate(op: str): def _delegate(op: str) -> Any:
def delegate(self, *args): def delegate(
self: IFDRational, *args: tuple[float, ...]
) -> bool | float | Fraction:
return getattr(self._val, op)(*args) return getattr(self._val, op)(*args)
return delegate return delegate
@ -358,7 +365,10 @@ class IFDRational(Rational):
self._numerator = value.numerator self._numerator = value.numerator
self._denominator = value.denominator self._denominator = value.denominator
else: else:
self._numerator = value if TYPE_CHECKING:
self._numerator = cast(IntegralLike, value)
else:
self._numerator = value
self._denominator = denominator self._denominator = denominator
if denominator == 0: if denominator == 0:
@ -371,14 +381,14 @@ class IFDRational(Rational):
self._val = Fraction(value / denominator) self._val = Fraction(value / denominator)
@property @property
def numerator(self): def numerator(self) -> IntegralLike:
return self._numerator return self._numerator
@property @property
def denominator(self) -> int: def denominator(self) -> int:
return self._denominator 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 :param max_denominator: Integer, the maximum denominator value
@ -406,14 +416,18 @@ class IFDRational(Rational):
val = float(val) val = float(val)
return val == other return val == other
def __getstate__(self) -> list[float | Fraction]: def __getstate__(self) -> list[float | Fraction | IntegralLike]:
return [self._val, self._numerator, self._denominator] 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) IFDRational.__init__(self, 0)
_val, _numerator, _denominator = state _val, _numerator, _denominator = state
assert isinstance(_val, (float, Fraction))
self._val = _val self._val = _val
self._numerator = _numerator if TYPE_CHECKING:
self._numerator = cast(IntegralLike, _numerator)
else:
self._numerator = _numerator
assert isinstance(_denominator, int) assert isinstance(_denominator, int)
self._denominator = _denominator self._denominator = _denominator
@ -471,8 +485,8 @@ def _register_loader(idx: int, size: int) -> Callable[[_LoaderFunc], _LoaderFunc
return decorator return decorator
def _register_writer(idx: int): def _register_writer(idx: int) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(func): def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
_write_dispatch[idx] = func # noqa: F821 _write_dispatch[idx] = func # noqa: F821
return func return func

View File

@ -6,6 +6,8 @@ from collections.abc import Sequence
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from numbers import _IntegralLike as IntegralLike
try: try:
import numpy.typing as npt import numpy.typing as npt
@ -38,4 +40,4 @@ class SupportsRead(Protocol[_T_co]):
StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]
__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] __all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]