Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added type hints #8285

Merged
merged 4 commits into from
Aug 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ci/requirements-mypy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ ipython
numpy
packaging
pytest
sphinx
types-defusedxml
types-olefile
types-setuptools
30 changes: 24 additions & 6 deletions Tests/test_imagefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,13 @@ def test_setimage(self) -> None:

fp = BytesIO()
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
im,
fp,
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB"
)
],
)

assert MockPyEncoder.last
Expand All @@ -333,7 +339,7 @@ def test_extents_none(self) -> None:
im.tile = [("MOCK", None, 32, None)]

fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])

assert MockPyEncoder.last
assert MockPyEncoder.last.state.xoff == 0
Expand All @@ -350,15 +356,19 @@ def test_negsize(self) -> None:
MockPyEncoder.last = None
with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")],
)
last: MockPyEncoder | None = MockPyEncoder.last
assert last
assert last.cleanup_called

with pytest.raises(ValueError):
ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")]
im,
fp,
[ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 0, "RGB")],
)

def test_oversize(self) -> None:
Expand All @@ -371,14 +381,22 @@ def test_oversize(self) -> None:
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB")],
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 0, "RGB"
)
],
)

with pytest.raises(ValueError):
ImageFile._save(
im,
fp,
[("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB")],
[
ImageFile._Tile(
"MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 0, "RGB"
)
],
)

def test_encode(self) -> None:
Expand Down
29 changes: 16 additions & 13 deletions docs/example/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import struct
from io import BytesIO
from typing import IO

from PIL import Image, ImageFile

Expand Down Expand Up @@ -94,26 +95,26 @@
DXT5_FOURCC = 0x35545844


def _decode565(bits):
def _decode565(bits: int) -> tuple[int, int, int]:
a = ((bits >> 11) & 0x1F) << 3
b = ((bits >> 5) & 0x3F) << 2
c = (bits & 0x1F) << 3
return a, b, c


def _c2a(a, b):
def _c2a(a: int, b: int) -> int:
return (2 * a + b) // 3


def _c2b(a, b):
def _c2b(a: int, b: int) -> int:
return (a + b) // 2


def _c3(a, b):
def _c3(a: int, b: int) -> int:
return (2 * b + a) // 3


def _dxt1(data, width, height):
def _dxt1(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height)

Expand Down Expand Up @@ -151,7 +152,7 @@ def _dxt1(data, width, height):
return bytes(ret)


def _dxtc_alpha(a0, a1, ac0, ac1, ai):
def _dxtc_alpha(a0: int, a1: int, ac0: int, ac1: int, ai: int) -> int:
if ai <= 12:
ac = (ac0 >> ai) & 7
elif ai == 15:
Expand All @@ -175,7 +176,7 @@ def _dxtc_alpha(a0, a1, ac0, ac1, ai):
return alpha


def _dxt5(data, width, height):
def _dxt5(data: IO[bytes], width: int, height: int) -> bytes:
# TODO implement this function as pixel format in decode.c
ret = bytearray(4 * width * height)

Expand Down Expand Up @@ -211,7 +212,7 @@ class DdsImageFile(ImageFile.ImageFile):
format = "DDS"
format_description = "DirectDraw Surface"

def _open(self):
def _open(self) -> None:
if not _accept(self.fp.read(4)):
msg = "not a DDS file"
raise SyntaxError(msg)
Expand Down Expand Up @@ -242,19 +243,20 @@ def _open(self):
elif fourcc == b"DXT5":
self.decoder = "DXT5"
else:
msg = f"Unimplemented pixel format {fourcc}"
msg = f"Unimplemented pixel format {repr(fourcc)}"
raise NotImplementedError(msg)

self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]

def load_seek(self, pos):
def load_seek(self, pos: int) -> None:
pass


class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer):
def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e:
Expand All @@ -266,7 +268,8 @@ def decode(self, buffer):
class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True

def decode(self, buffer):
def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))
except struct.error as e:
Expand All @@ -279,7 +282,7 @@ def decode(self, buffer):
Image.register_decoder("DXT5", DXT5Decoder)


def _accept(prefix):
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS "


Expand Down
12 changes: 7 additions & 5 deletions docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1517,19 +1517,21 @@ To add other read or write support, use
:py:func:`PIL.WmfImagePlugin.register_handler` to register a WMF and EMF
handler. ::

from PIL import Image
from typing import IO

from PIL import Image, ImageFile
from PIL import WmfImagePlugin


class WmfHandler:
def open(self, im):
class WmfHandler(ImageFile.StubHandler):
def open(self, im: ImageFile.StubImageFile) -> None:
...

def load(self, im):
def load(self, im: ImageFile.StubImageFile) -> Image.Image:
...
return image

def save(self, im, fp, filename):
def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None:
...


Expand Down
6 changes: 3 additions & 3 deletions docs/handbook/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ Rolling an image

::

def roll(im, delta):
def roll(im: Image.Image, delta: int) -> Image.Image:
"""Roll an image sideways."""
xsize, ysize = im.size

Expand All @@ -211,7 +211,7 @@ Merging images

::

def merge(im1, im2):
def merge(im1: Image.Image, im2: Image.Image) -> Image.Image:
w = im1.size[0] + im2.size[0]
h = max(im1.size[1], im2.size[1])
im = Image.new("RGBA", (w, h))
Expand Down Expand Up @@ -704,7 +704,7 @@ in the current directory can be saved as JPEGs at reduced quality.
import glob
from PIL import Image

def compress_image(source_path, dest_path):
def compress_image(source_path: str, dest_path: str) -> None:
with Image.open(source_path) as img:
if img.mode != "RGB":
img = img.convert("RGB")
Expand Down
6 changes: 3 additions & 3 deletions docs/handbook/writing-your-own-image-plugin.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ true color.
from PIL import Image, ImageFile


def _accept(prefix):
def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"SPAM"


Expand All @@ -62,7 +62,7 @@ true color.
format = "SPAM"
format_description = "Spam raster image"

def _open(self):
def _open(self) -> None:

header = self.fp.read(128).split()

Expand All @@ -82,7 +82,7 @@ true color.
raise SyntaxError(msg)

# data descriptor
self.tile = [("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]
self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 128, (self.mode, 0, 1))]


Image.register_open(SpamImageFile.format, SpamImageFile, _accept)
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/BlpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0))

ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)])
ImageFile._save(im, fp, [ImageFile._Tile("BLP", (0, 0) + im.size, 0, im.mode)])


Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
Expand Down
4 changes: 3 additions & 1 deletion src/PIL/BmpImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,9 @@ def _save(
if palette:
fp.write(palette)

ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))]
)


#
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/EpsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -
if hasattr(fp, "flush"):
fp.flush()

ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
ImageFile._save(im, fp, [ImageFile._Tile("eps", (0, 0) + im.size, 0, None)])

fp.write(b"\n%%%%EndBinary\n")
fp.write(b"grestore end\n")
Expand Down
8 changes: 6 additions & 2 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,9 @@ def _write_single_frame(
_write_local_header(fp, im, (0, 0), flags)

im_out.encoderconfig = (8, get_interlace(im))
ImageFile._save(im_out, fp, [("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])])
ImageFile._save(
im_out, fp, [ImageFile._Tile("gif", (0, 0) + im.size, 0, RAWMODE[im_out.mode])]
)

fp.write(b"\0") # end of image data

Expand Down Expand Up @@ -1054,7 +1056,9 @@ def _write_frame_data(
_write_local_header(fp, im_frame, offset, 0)

ImageFile._save(
im_frame, fp, [("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])]
im_frame,
fp,
[ImageFile._Tile("gif", (0, 0) + im_frame.size, 0, RAWMODE[im_frame.mode])],
)

fp.write(b"\0") # end of image data
Expand Down
4 changes: 3 additions & 1 deletion src/PIL/IcoImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if bits != 32:
and_mask = Image.new("1", size)
ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
and_mask,
image_io,
[ImageFile._Tile("raw", (0, 0) + size, 0, ("1", 0, -1))],
)
else:
frame.save(image_io, "png")
Expand Down
4 changes: 3 additions & 1 deletion src/PIL/ImImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])
ImageFile._save(
im, fp, [ImageFile._Tile("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))]
)


#
Expand Down
8 changes: 3 additions & 5 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,10 @@
# Registries

if TYPE_CHECKING:
import mmap
from xml.etree.ElementTree import Element

from . import ImageFile, ImagePalette, TiffImagePlugin
from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = []
OPEN: dict[
Expand Down Expand Up @@ -612,7 +613,7 @@
logger.debug("Error closing: %s", msg)

if getattr(self, "map", None):
self.map = None
self.map: mmap.mmap | None = None

Check warning on line 616 in src/PIL/Image.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/Image.py#L616

Added line #L616 was not covered by tests

# Instead of simply setting to None, we're setting up a
# deferred error that will better explain that the core image
Expand Down Expand Up @@ -1336,9 +1337,6 @@
self.load()
return self._new(self.im.expand(xmargin, ymargin))

if TYPE_CHECKING:
from . import ImageFilter

def filter(self, filter: ImageFilter.Filter | type[ImageFilter.Filter]) -> Image:
"""
Filters this image using the given filter. For a list of
Expand Down
Loading
Loading