Merge pull request #8319 from radarhere/type_hint

Added type hints
This commit is contained in:
Andrew Murray 2024-08-24 23:54:19 +10:00 committed by GitHub
commit 4721c31b19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 133 additions and 87 deletions

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -684,6 +684,13 @@ class TestFileTiff:
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")

View File

@ -42,6 +42,12 @@ try:
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:
@ -91,16 +97,15 @@ class TestImage:
# 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"

View File

@ -412,9 +412,8 @@ class TestPyEncoder(CodecsTest):
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):

View File

@ -5,8 +5,6 @@ import sys
from io import BytesIO
from pathlib import Path
import pytest
from PIL import Image, PSDraw
@ -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
@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""
assert mystdout.buffer.getvalue() != b""

View File

@ -362,6 +362,7 @@ Classes
:undoc-members:
:show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImagePointTransform
.. autoclass:: PIL.Image.ImageTransformHandler
Protocols

View File

@ -319,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile):
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)

View File

@ -221,6 +221,8 @@ if TYPE_CHECKING:
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] = []
@ -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)
# --------------------------------------------------------------------
@ -677,7 +689,7 @@ class Image:
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),
@ -1880,7 +1892,13 @@ class Image:
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:
"""
@ -1897,7 +1915,7 @@ class Image:
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
@ -1916,10 +1934,10 @@ class Image:
# 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
@ -2855,11 +2873,11 @@ class Image:
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]
@ -3994,15 +4012,19 @@ class Exif(_ExifBase):
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)
@ -4059,7 +4081,9 @@ class Exif(_ExifBase):
(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
@ -4075,16 +4099,18 @@ class Exif(_ExifBase):
][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 = {

View File

@ -31,6 +31,7 @@ from __future__ import annotations
import abc
import io
import itertools
import os
import struct
import sys
from typing import IO, Any, NamedTuple
@ -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:
@ -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
@ -801,7 +803,7 @@ class PyEncoder(PyCodec):
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.
@ -814,5 +816,5 @@ class PyEncoder(PyCodec):
while errcode == 0:
status, errcode, buf = self.encode(bufsize)
if status > 0:
fh.write(buf[status:])
os.write(fh, buf[status:])
return errcode

View File

@ -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:

View File

@ -17,7 +17,7 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING
from typing import IO, TYPE_CHECKING
from . import EpsImagePlugin
@ -28,15 +28,12 @@ from . import EpsImagePlugin
class PSDraw:
"""
Sets up printing to the given file. If ``fp`` is omitted,
``sys.stdout.buffer`` or ``sys.stdout`` is assumed.
``sys.stdout.buffer`` is assumed.
"""
def __init__(self, fp=None):
def __init__(self, fp: IO[bytes] | None = None) -> None:
if not fp:
try:
fp = sys.stdout.buffer
except AttributeError:
fp = sys.stdout
self.fp = fp
def begin_document(self, id: str | None = None) -> None:

View File

@ -624,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None
self._offset = None
self._offset: int | None = None
def __str__(self) -> str:
return str(dict(self))
def named(self):
def named(self) -> dict[str, Any]:
"""
:returns: dict of name|key: value
@ -643,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag):
def __getitem__(self, tag: int) -> Any:
if tag not in self._tags_v2: # unpack on the fly
data = self._tagdata[tag]
typ = self.tagtype[tag]
@ -855,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise OSError(msg)
return ret
def load(self, fp):
def load(self, fp: IO[bytes]) -> None:
self.reset()
self._offset = fp.tell()
@ -1098,7 +1098,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
for legacy_api in (False, True):
self._setitem(tag, value, legacy_api)
def __getitem__(self, tag):
def __getitem__(self, tag: int) -> Any:
if tag not in self._tags_v1: # unpack on the fly
data = self._tagdata[tag]
typ = self.tagtype[tag]
@ -1124,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile):
format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False
def __init__(self, fp=None, filename=None):
self.tag_v2 = None
def __init__(
self,
fp: StrOrBytesPath | IO[bytes] | None = None,
filename: str | bytes | None = None,
) -> None:
self.tag_v2: ImageFileDirectory_v2
""" Image file directory (tag dictionary) """
self.tag = None
self.tag: ImageFileDirectory_v1
""" Legacy tag entries """
super().__init__(fp, filename)
@ -1143,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.tag_v2 = ImageFileDirectory_v2(ifh)
# legacy IFD entries will be filled in later
self.ifd: ImageFileDirectory_v1 | None = None
# setup frame pointers
self.__first = self.__next = self.tag_v2.next
self.__frame = -1
@ -1396,8 +1397,11 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
# size
xsize = int(self.tag_v2.get(IMAGEWIDTH))
ysize = int(self.tag_v2.get(IMAGELENGTH))
xsize = self.tag_v2.get(IMAGEWIDTH)
ysize = self.tag_v2.get(IMAGELENGTH)
if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions"
raise ValueError(msg)
self._size = xsize, ysize
logger.debug("- size: %s", self.size)
@ -1545,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile):
else:
# tiled image
offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(TILEWIDTH)
tilewidth = self.tag_v2.get(TILEWIDTH)
h = self.tag_v2.get(TILELENGTH)
if not isinstance(tilewidth, int) or not isinstance(h, int):
msg = "Invalid tile dimensions"
raise ValueError(msg)
w = tilewidth
for offset in offsets:
if x + w > xsize:
@ -1624,7 +1632,7 @@ SAVE_INFO = {
}
def _save(im, fp, filename):
def _save(im: Image.Image, fp, filename: str | bytes) -> None:
try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e:
@ -1760,6 +1768,7 @@ def _save(im, fp, filename):
if im.mode == "1":
inverted_im = im.copy()
px = inverted_im.load()
if px is not None:
for y in range(inverted_im.height):
for x in range(inverted_im.width):
px[x, y] = 0 if px[x, y] == 255 else 255
@ -1805,11 +1814,11 @@ def _save(im, fp, filename):
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
if im.mode == "YCbCr":
for tag, value in {
for tag, default_value in {
YCBCRSUBSAMPLING: (1, 1),
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
}.items():
ifd.setdefault(tag, value)
ifd.setdefault(tag, default_value)
blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
if libtiff:
@ -1852,7 +1861,7 @@ def _save(im, fp, filename):
]
# bits per sample is a single short in the tiff directory, not a list.
atts = {BITSPERSAMPLE: bits[0]}
atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
# Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can
# save(load('')) == original file.
@ -1923,13 +1932,15 @@ def _save(im, fp, filename):
offset = ifd.save(fp)
ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
im,
fp,
[ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
)
# -- helper for multi-page save --
if "_debug_multipage" in encoderinfo:
# just to access o32 and o16 (using correct byte order)
im._debug_multipage = ifd
setattr(im, "_debug_multipage", ifd)
class AppendingTiffWriter: