Skip to content

Commit

Permalink
Merge branch 'main' into type_hint_image
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere authored Jun 18, 2024
2 parents 6b5b2f6 + 5d3338f commit 66ad497
Show file tree
Hide file tree
Showing 25 changed files with 159 additions and 96 deletions.
2 changes: 1 addition & 1 deletion Tests/test_decompression_bomb.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@


class TestDecompressionBomb:
def teardown_method(self, method) -> None:
def teardown_method(self) -> None:
Image.MAX_IMAGE_PIXELS = ORIGINAL_LIMIT

def test_no_warning_small_file(self) -> None:
Expand Down
19 changes: 11 additions & 8 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,9 @@ def test_smooth(self) -> None:
assert_image(im1, im2.mode, im2.size)

def test_subsampling(self) -> None:
def getsampling(im: JpegImagePlugin.JpegImageFile):
def getsampling(
im: JpegImagePlugin.JpegImageFile,
) -> tuple[int, int, int, int, int, int]:
layer = im.layer
return layer[0][1:3] + layer[1][1:3] + layer[2][1:3]

Expand Down Expand Up @@ -699,7 +701,7 @@ def test_load_djpeg(self) -> None:
def test_save_cjpeg(self, tmp_path: Path) -> None:
with Image.open(TEST_FILE) as img:
tempfile = str(tmp_path / "temp.jpg")
JpegImagePlugin._save_cjpeg(img, 0, tempfile)
JpegImagePlugin._save_cjpeg(img, BytesIO(), tempfile)
# Default save quality is 75%, so a tiny bit of difference is alright
assert_image_similar_tofile(img, tempfile, 17)

Expand Down Expand Up @@ -917,24 +919,25 @@ def test_icc_after_SOF(self) -> None:
with Image.open("Tests/images/icc-after-SOF.jpg") as im:
assert im.info["icc_profile"] == b"profile"

def test_jpeg_magic_number(self) -> None:
def test_jpeg_magic_number(self, monkeypatch: pytest.MonkeyPatch) -> None:
size = 4097
buffer = BytesIO(b"\xFF" * size) # Many xFF bytes
buffer.max_pos = 0
max_pos = 0
orig_read = buffer.read

def read(n=-1):
def read(n: int | None = -1) -> bytes:
nonlocal max_pos
res = orig_read(n)
buffer.max_pos = max(buffer.max_pos, buffer.tell())
max_pos = max(max_pos, buffer.tell())
return res

buffer.read = read
monkeypatch.setattr(buffer, "read", read)
with pytest.raises(UnidentifiedImageError):
with Image.open(buffer):
pass

# Assert the entire file has not been read
assert 0 < buffer.max_pos < size
assert 0 < max_pos < size

def test_getxmp(self) -> None:
with Image.open("Tests/images/xmp_test.jpg") as im:
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_file_jpeg2k.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ def test_plt_marker() -> None:
out.seek(length - 2, os.SEEK_CUR)


def test_9bit():
def test_9bit() -> None:
with Image.open("Tests/images/9bit.j2k") as im:
assert im.mode == "I;16"
assert im.size == (128, 128)
2 changes: 1 addition & 1 deletion Tests/test_file_tiff.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def test_bigtiff(self, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif")
im.save(outfile, save_all=True, append_images=[im], tiffinfo=im.tag_v2)

def test_seek_too_large(self):
def test_seek_too_large(self) -> None:
with pytest.raises(ValueError, match="Unable to seek to frame"):
Image.open("Tests/images/seek_too_large.tif")

Expand Down
2 changes: 1 addition & 1 deletion Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ def test_bad_mode(self) -> None:

def test_stringio(self) -> None:
with pytest.raises(ValueError):
with Image.open(io.StringIO()):
with Image.open(io.StringIO()): # type: ignore[arg-type]
pass

def test_pathlib(self, tmp_path: Path) -> None:
Expand Down
6 changes: 5 additions & 1 deletion Tests/test_image_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ def test_sanity() -> None:
),
)
def test_properties(
mode, expected_base, expected_type, expected_bands, expected_band_names
mode: str,
expected_base: str,
expected_type: str,
expected_bands: int,
expected_band_names: tuple[str, ...],
) -> None:
assert Image.getmodebase(mode) == expected_base
assert Image.getmodetype(mode) == expected_type
Expand Down
8 changes: 4 additions & 4 deletions Tests/test_image_putdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None:
im = Image.new("L", (1, 1))
with pytest.raises(TypeError):
im.putdata([[0]])
im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError):
im.putdata([[0]], 2)
im.putdata([[0]], 2) # type: ignore[list-item]

with pytest.raises(TypeError):
im = Image.new("I", (1, 1))
im.putdata([[0]])
im.putdata([[0]]) # type: ignore[list-item]
with pytest.raises(TypeError):
im = Image.new("F", (1, 1))
im.putdata([[0]])
im.putdata([[0]]) # type: ignore[list-item]
2 changes: 1 addition & 1 deletion Tests/test_image_quantize.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def test_quantize_dither_diff() -> None:
@pytest.mark.parametrize(
"method", (Image.Quantize.MEDIANCUT, Image.Quantize.MAXCOVERAGE)
)
def test_quantize_kmeans(method) -> None:
def test_quantize_kmeans(method: Image.Quantize) -> None:
im = hopper()
no_kmeans = im.quantize(kmeans=0, method=method)
kmeans = im.quantize(kmeans=1, method=method)
Expand Down
12 changes: 8 additions & 4 deletions Tests/test_image_reduce.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) ->
@pytest.mark.parametrize(
"size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError))
)
def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None:
def test_args_factor_error(
size: float | tuple[int, int], expected_error: type[Exception]
) -> None:
im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
im.reduce(size)
im.reduce(size) # type: ignore[arg-type]


@pytest.mark.parametrize(
Expand All @@ -86,10 +88,12 @@ def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) ->
((5, 0, 5, 10), ValueError),
),
)
def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None:
def test_args_box_error(
size: str | tuple[int, int, int, int], expected_error: type[Exception]
) -> None:
im = Image.new("L", (10, 10))
with pytest.raises(expected_error):
im.reduce(2, size).size
im.reduce(2, size).size # type: ignore[arg-type]


@pytest.mark.parametrize("mode", ("P", "1", "I;16"))
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_image_thumbnail.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

def test_sanity() -> None:
im = hopper()
assert im.thumbnail((100, 100)) is None
im.thumbnail((100, 100))

assert im.size == (100, 100)

Expand Down
6 changes: 5 additions & 1 deletion Tests/test_imagedraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -1562,7 +1562,11 @@ def test_compute_regular_polygon_vertices(
],
)
def test_compute_regular_polygon_vertices_input_error_handling(
n_sides, bounding_circle, rotation, expected_error, error_message
n_sides: int,
bounding_circle: int | tuple[int | tuple[int] | str, ...],
rotation: int | str,
expected_error: type[Exception],
error_message: str,
) -> None:
with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation)
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_imagefont.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ def test_render_multiline(font: ImageFont.FreeTypeFont) -> None:
draw = ImageDraw.Draw(im)
line_spacing = font.getbbox("A")[3] + 4
lines = TEST_TEXT.split("\n")
y = 0
y: float = 0
for line in lines:
draw.text((0, y), line, font=font)
y += line_spacing
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_imageops.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,7 @@ def test_autocontrast_cutoff() -> None:
# Test the cutoff argument of autocontrast
with Image.open("Tests/images/bw_gradient.png") as img:

def autocontrast(cutoff: int | tuple[int, int]):
def autocontrast(cutoff: int | tuple[int, int]) -> list[int]:
return ImageOps.autocontrast(img, cutoff).histogram()

assert autocontrast(10) == autocontrast((10, 10))
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_imagewin_pointers.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class BITMAPINFOHEADER(ctypes.Structure):
]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP

def serialize_dib(bi, pixels) -> bytearray:
def serialize_dib(bi: BITMAPINFOHEADER, pixels: ctypes.c_void_p) -> bytearray:
bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize
Expand Down
2 changes: 1 addition & 1 deletion Tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"args, report",
((["PIL"], False), (["PIL", "--report"], True), (["PIL.report"], True)),
)
def test_main(args, report) -> None:
def test_main(args: list[str], report: bool) -> None:
args = [sys.executable, "-m"] + args
out = subprocess.check_output(args).decode("utf-8")
lines = out.splitlines()
Expand Down
25 changes: 17 additions & 8 deletions Tests/test_numpy.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
from __future__ import annotations

import warnings
from typing import TYPE_CHECKING, Any

import pytest

from PIL import Image

from .helper import assert_deep_equal, assert_image, hopper, skip_unless_feature

numpy = pytest.importorskip("numpy", reason="NumPy not installed")
if TYPE_CHECKING:
import numpy
import numpy.typing
else:
numpy = pytest.importorskip("numpy", reason="NumPy not installed")

TEST_IMAGE_SIZE = (10, 10)


def test_numpy_to_image() -> None:
def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image:
def to_image(
dtype: numpy.typing.DTypeLike, bands: int = 1, boolean: int = 0
) -> Image.Image:
if bands == 1:
if boolean:
data = [0, 255] * 50
Expand Down Expand Up @@ -99,14 +106,16 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5))


def _test_img_equals_nparray(img: Image.Image, np) -> None:
assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0]
def _test_img_equals_nparray(
img: Image.Image, np_img: numpy.typing.NDArray[Any]
) -> None:
assert len(np_img.shape) >= 2
np_size = np_img.shape[1], np_img.shape[0]
assert img.size == np_size
px = img.load()
for x in range(0, img.size[0], int(img.size[0] / 10)):
for y in range(0, img.size[1], int(img.size[1] / 10)):
assert_deep_equal(px[x, y], np[y, x])
assert_deep_equal(px[x, y], np_img[y, x])


def test_16bit() -> None:
Expand Down Expand Up @@ -157,7 +166,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8),
),
)
def test_to_array(mode: str, dtype) -> None:
def test_to_array(mode: str, dtype: numpy.typing.DTypeLike) -> None:
img = hopper(mode)

# Resize to non-square
Expand Down Expand Up @@ -207,7 +216,7 @@ def test_putdata() -> None:
numpy.float64,
),
)
def test_roundtrip_eye(dtype) -> None:
def test_roundtrip_eye(dtype: numpy.typing.DTypeLike) -> None:
arr = numpy.eye(10, dtype=dtype)
numpy.testing.assert_array_equal(arr, numpy.array(Image.fromarray(arr)))

Expand Down
7 changes: 4 additions & 3 deletions Tests/test_shell_injection.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from __future__ import annotations

import shutil
from io import BytesIO
from pathlib import Path
from typing import Callable
from typing import IO, Callable

import pytest

Expand All @@ -22,11 +23,11 @@ def assert_save_filename_check(
self,
tmp_path: Path,
src_img: Image.Image,
save_func: Callable[[Image.Image, int, str], None],
save_func: Callable[[Image.Image, IO[bytes], str | bytes], None],
) -> None:
for filename in test_filenames:
dest_file = str(tmp_path / filename)
save_func(src_img, 0, dest_file)
save_func(src_img, BytesIO(), dest_file)
# If file can't be opened, shell injection probably occurred
with Image.open(dest_file) as im:
im.load()
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/BdfFontFile.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def bdf_char(
class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format."""

def __init__(self, fp: BinaryIO):
def __init__(self, fp: BinaryIO) -> None:
super().__init__()

s = fp.readline()
Expand Down
20 changes: 11 additions & 9 deletions src/PIL/ImageDraw.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,16 @@
import math
import numbers
import struct
from types import ModuleType
from typing import TYPE_CHECKING, AnyStr, Sequence, cast

from . import Image, ImageColor
from ._deprecate import deprecate
from ._typing import Coords

if TYPE_CHECKING:
from . import ImageDraw2, ImageFont

"""
A simple 2D drawing interface for PIL images.
<p>
Expand Down Expand Up @@ -93,9 +97,6 @@ def __init__(self, im: Image.Image, mode: str | None = None) -> None:
self.fontmode = "L" # aliasing is okay for other modes
self.fill = False

if TYPE_CHECKING:
from . import ImageFont

def getfont(
self,
) -> ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont:
Expand Down Expand Up @@ -879,7 +880,7 @@ def multiline_textbbox(
return bbox


def Draw(im, mode: str | None = None) -> ImageDraw:
def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw:
"""
A simple 2D drawing interface for PIL images.
Expand All @@ -891,7 +892,7 @@ def Draw(im, mode: str | None = None) -> ImageDraw:
defaults to the mode of the image.
"""
try:
return im.getdraw(mode)
return getattr(im, "getdraw")(mode)
except AttributeError:
return ImageDraw(im, mode)

Expand All @@ -903,7 +904,9 @@ def Draw(im, mode: str | None = None) -> ImageDraw:
Outline = None


def getdraw(im=None, hints=None):
def getdraw(
im: Image.Image | None = None, hints: list[str] | None = None
) -> tuple[ImageDraw2.Draw | None, ModuleType]:
"""
:param im: The image to draw in.
:param hints: An optional list of hints. Deprecated.
Expand All @@ -913,9 +916,8 @@ def getdraw(im=None, hints=None):
deprecate("'hints' parameter", 12)
from . import ImageDraw2

if im:
im = ImageDraw2.Draw(im)
return im, ImageDraw2
draw = ImageDraw2.Draw(im) if im is not None else None
return draw, ImageDraw2


def floodfill(
Expand Down
Loading

0 comments on commit 66ad497

Please sign in to comment.