Skip to content

Commit

Permalink
Merge pull request #8339 from radarhere/type_hint
Browse files Browse the repository at this point in the history
  • Loading branch information
hugovk authored Sep 4, 2024
2 parents 8aa1e92 + e47b181 commit eaeda4a
Show file tree
Hide file tree
Showing 14 changed files with 70 additions and 35 deletions.
1 change: 1 addition & 0 deletions .ci/requirements-mypy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ numpy
packaging
pytest
sphinx
types-atheris
types-defusedxml
types-olefile
types-setuptools
3 changes: 2 additions & 1 deletion Tests/oss-fuzz/fuzz_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@


import atheris
from atheris.import_hook import instrument_imports

with atheris.instrument_imports():
with instrument_imports():
import sys

import fuzzers
Expand Down
3 changes: 2 additions & 1 deletion Tests/oss-fuzz/fuzz_pillow.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@


import atheris
from atheris.import_hook import instrument_imports

with atheris.instrument_imports():
with instrument_imports():
import sys

import fuzzers
Expand Down
4 changes: 4 additions & 0 deletions docs/reference/internal_modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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$',
]
2 changes: 1 addition & 1 deletion src/PIL/BmpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]:
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


Expand Down
7 changes: 6 additions & 1 deletion src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ class Quantize(IntEnum):

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,
Expand Down Expand Up @@ -1598,7 +1603,7 @@ def get_child_images(self) -> list[ImageFile.ImageFile]:
self.fp.seek(offset)
return child_images

def getim(self):
def getim(self) -> CapsuleType:
"""
Returns a capsule that points to the internal image memory.
Expand Down
7 changes: 5 additions & 2 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,19 +745,22 @@ def decode(self, buffer: bytes) -> tuple[int, int]:
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)
Expand Down
6 changes: 3 additions & 3 deletions src/PIL/ImageFont.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
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
Expand Down Expand Up @@ -245,7 +245,7 @@ def __init__(

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
Expand All @@ -267,7 +267,7 @@ def load_from_bytes(f) -> None:
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]
Expand Down
10 changes: 8 additions & 2 deletions src/PIL/Jpeg2KImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io
import os
import struct
from collections.abc import Callable
from typing import IO, cast

from . import Image, ImageFile, ImagePalette, _binary
Expand Down Expand Up @@ -316,8 +317,13 @@ def _parse_comment(self) -> None:
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
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/MspImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def decode(self, buffer: bytes) -> tuple[int, int]:
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

Expand Down
4 changes: 3 additions & 1 deletion src/PIL/PngImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 31 additions & 17 deletions src/PIL/TiffImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
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.
Expand Down Expand Up @@ -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

Expand All @@ -318,8 +323,10 @@ def _limit_signed_rational(val, max_val, min_val):
_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
Expand Down Expand Up @@ -358,7 +365,10 @@ def __init__(
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:
Expand All @@ -371,14 +381,14 @@ def __init__(
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
Expand Down Expand Up @@ -406,14 +416,18 @@ def __eq__(self, other: object) -> bool:
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

Expand Down Expand Up @@ -471,8 +485,8 @@ def decorator(func: _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

Expand Down
4 changes: 3 additions & 1 deletion src/PIL/_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union

if TYPE_CHECKING:
from numbers import _IntegralLike as IntegralLike

try:
import numpy.typing as npt

Expand Down Expand Up @@ -38,4 +40,4 @@ def read(self, __length: int = ...) -> _T_co: ...
StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"]


__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"]
__all__ = ["IntegralLike", "StrOrBytesPath", "SupportsRead", "TypeGuard"]

0 comments on commit eaeda4a

Please sign in to comment.