Skip to content

Commit

Permalink
Merge pull request #8319 from radarhere/type_hint
Browse files Browse the repository at this point in the history
Added type hints
  • Loading branch information
radarhere authored Aug 24, 2024
2 parents cfb093a + 8aa58e3 commit 4721c31
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ build_script:

test_script:
- cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma'
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'
Expand Down
1 change: 1 addition & 0 deletions .ci/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel
python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/macos-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"

python3 -m pip install coverage
python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile
python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-cygwin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ jobs:
perl
python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter
Expand Down
7 changes: 7 additions & 0 deletions Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,13 @@ def test_planar_configuration_save(self, tmp_path: Path) -> None:
with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile)

def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)

@pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
Expand Down
17 changes: 11 additions & 6 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@
except ImportError:
ElementTree = None

PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None


# Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
Expand Down Expand Up @@ -91,16 +97,15 @@ def test_sanity(self) -> None:
# with pytest.raises(MemoryError):
# Image.new("L", (1000000, 1000000))

@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text

im = Image.new("L", (100, 100))

p = Pretty()
output = io.StringIO()
assert PrettyPrinter is not None
p = PrettyPrinter(output)
im._repr_pretty_(p, False)
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>"
assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"

def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png"
Expand Down
3 changes: 1 addition & 2 deletions Tests/test_imagefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,9 +412,8 @@ def test_encode(self) -> None:
with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd()

fh = BytesIO()
with pytest.raises(NotImplementedError):
encoder.encode_to_file(fh, 0)
encoder.encode_to_file(0, 0)

def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError):
Expand Down
11 changes: 3 additions & 8 deletions Tests/test_psdraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from io import BytesIO
from pathlib import Path

import pytest

from PIL import Image, PSDraw


Expand Down Expand Up @@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0


@pytest.mark.parametrize("buffer", (True, False))
def test_stdout(buffer: bool) -> None:
def test_stdout() -> None:
# Temporarily redirect stdout
old_stdout = sys.stdout

class MyStdOut:
buffer = BytesIO()

mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
mystdout = MyStdOut()

sys.stdout = mystdout

Expand All @@ -67,6 +64,4 @@ class MyStdOut:
# Reset stdout
sys.stdout = old_stdout

if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""
assert mystdout.buffer.getvalue() != b""
1 change: 1 addition & 0 deletions docs/reference/Image.rst
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ Classes
:undoc-members:
:show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImagePointTransform
.. autoclass:: PIL.Image.ImageTransformHandler

Protocols
Expand Down
4 changes: 2 additions & 2 deletions src/PIL/IcoImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,11 @@ def _open(self) -> None:
self.load()

@property
def size(self):
def size(self) -> tuple[int, int]:
return self._size

@size.setter
def size(self, value):
def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg)
Expand Down
92 changes: 59 additions & 33 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ class Quantize(IntEnum):
import mmap
from xml.etree.ElementTree import Element

from IPython.lib.pretty import PrettyPrinter

from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
Expand Down Expand Up @@ -468,43 +470,53 @@ def _getencoder(
# Simple expression analyzer


class _E:
class ImagePointTransform:
"""
Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than
8 bits, this represents an affine transformation, where the value is multiplied by
``scale`` and ``offset`` is added.
"""

def __init__(self, scale: float, offset: float) -> None:
self.scale = scale
self.offset = offset

def __neg__(self) -> _E:
return _E(-self.scale, -self.offset)
def __neg__(self) -> ImagePointTransform:
return ImagePointTransform(-self.scale, -self.offset)

def __add__(self, other: _E | float) -> _E:
if isinstance(other, _E):
return _E(self.scale + other.scale, self.offset + other.offset)
return _E(self.scale, self.offset + other)
def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return ImagePointTransform(
self.scale + other.scale, self.offset + other.offset
)
return ImagePointTransform(self.scale, self.offset + other)

__radd__ = __add__

def __sub__(self, other: _E | float) -> _E:
def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return self + -other

def __rsub__(self, other: _E | float) -> _E:
def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return other + -self

def __mul__(self, other: _E | float) -> _E:
if isinstance(other, _E):
def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return NotImplemented
return _E(self.scale * other, self.offset * other)
return ImagePointTransform(self.scale * other, self.offset * other)

__rmul__ = __mul__

def __truediv__(self, other: _E | float) -> _E:
if isinstance(other, _E):
def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, ImagePointTransform):
return NotImplemented
return _E(self.scale / other, self.offset / other)
return ImagePointTransform(self.scale / other, self.offset / other)


def _getscaleoffset(expr) -> tuple[float, float]:
a = expr(_E(1, 0))
return (a.scale, a.offset) if isinstance(a, _E) else (0, a)
def _getscaleoffset(
expr: Callable[[ImagePointTransform], ImagePointTransform | float]
) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)


# --------------------------------------------------------------------
Expand Down Expand Up @@ -677,7 +689,7 @@ def __repr__(self) -> str:
id(self),
)

def _repr_pretty_(self, p, cycle: bool) -> None:
def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
"""IPython plain text display support"""

# Same as __repr__ but without unpredictable id(self),
Expand Down Expand Up @@ -1880,7 +1892,13 @@ def alpha_composite(

def point(
self,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler,
lut: (
Sequence[float]
| NumpyArray
| Callable[[int], float]
| Callable[[ImagePointTransform], ImagePointTransform | float]
| ImagePointHandler
),
mode: str | None = None,
) -> Image:
"""
Expand All @@ -1897,7 +1915,7 @@ def point(
object::
class Example(Image.ImagePointHandler):
def point(self, data):
def point(self, im: Image) -> Image:
# Return result
:param mode: Output mode (default is same as input). This can only be used if
the source image has mode "L" or "P", and the output has mode "1" or the
Expand All @@ -1916,10 +1934,10 @@ def point(self, data):
# check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing
# a gamma function point transform on > 8bit images.
scale, offset = _getscaleoffset(lut)
scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table
flatLut = [lut(i) for i in range(256)] * self.im.bands
flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
else:
flatLut = lut

Expand Down Expand Up @@ -2855,11 +2873,11 @@ def __transformer(
self,
box: tuple[int, int, int, int],
image: Image,
method,
data,
method: Transform,
data: Sequence[float],
resample: int = Resampling.NEAREST,
fill: bool = True,
):
) -> None:
w = box[2] - box[0]
h = box[3] - box[1]

Expand Down Expand Up @@ -3994,15 +4012,19 @@ def tobytes(self, offset: int = 8) -> bytes:
ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset)

def get_ifd(self, tag):
def get_ifd(self, tag: int) -> dict[int, Any]:
if tag not in self._ifds:
if tag == ExifTags.IFD.IFD1:
if self._info is not None and self._info.next != 0:
self._ifds[tag] = self._get_ifd_dict(self._info.next)
ifd = self._get_ifd_dict(self._info.next)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
offset = self._hidden_data.get(tag, self.get(tag))
if offset is not None:
self._ifds[tag] = self._get_ifd_dict(offset, tag)
ifd = self._get_ifd_dict(offset, tag)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif)
Expand Down Expand Up @@ -4059,7 +4081,9 @@ def get_ifd(self, tag):
(offset,) = struct.unpack(">L", data)
self.fp.seek(offset)

camerainfo = {"ModelID": self.fp.read(4)}
camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}

self.fp.read(4)
# Seconds since 2000
Expand All @@ -4075,16 +4099,18 @@ def get_ifd(self, tag):
][1]
camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False
)
)[0]

self.fp.read(4)
camerainfo["Category"] = self.fp.read(2)

makernote = {0x1101: dict(self._fixup_dict(camerainfo))}
makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote
else:
# Interop
self._ifds[tag] = self._get_ifd_dict(tag_data, tag)
ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None:
self._ifds[tag] = ifd
ifd = self._ifds.get(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = {
Expand Down
8 changes: 5 additions & 3 deletions src/PIL/ImageFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import abc
import io
import itertools
import os
import struct
import sys
from typing import IO, Any, NamedTuple
Expand Down Expand Up @@ -555,7 +556,7 @@ def _encode_tile(
fp: IO[bytes],
tile: list[_Tile],
bufsize: int,
fh,
fh: int | None,
exc: BaseException | None = None,
) -> None:
for encoder_name, extents, offset, args in tile:
Expand All @@ -577,6 +578,7 @@ def _encode_tile(
break
else:
# slight speedup: compress to real file object
assert fh is not None
errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0:
raise _get_oserror(errcode, encoder=True) from exc
Expand Down Expand Up @@ -801,7 +803,7 @@ def encode_to_pyfd(self) -> tuple[int, int]:
self.fd.write(data)
return bytes_consumed, errcode

def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
def encode_to_file(self, fh: int, bufsize: int) -> int:
"""
:param fh: File handle.
:param bufsize: Buffer size.
Expand All @@ -814,5 +816,5 @@ def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int:
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
os.write(fh, buf[status:])
return errcode
2 changes: 1 addition & 1 deletion src/PIL/IptcImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def getiptcinfo(
# as 4-byte integers, so we cannot use the get method...)
try:
data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK]
except (AttributeError, KeyError):
except KeyError:
pass

if data is None:
Expand Down
Loading

0 comments on commit 4721c31

Please sign in to comment.