Added type hints

This commit is contained in:
Andrew Murray 2024-02-20 15:41:20 +11:00
parent 380bc1766b
commit e39765d755
19 changed files with 73 additions and 45 deletions

View File

@ -20,7 +20,7 @@ from PIL import _deprecate
), ),
], ],
) )
def test_version(version, expected) -> None: def test_version(version: int | None, expected: str) -> None:
with pytest.warns(DeprecationWarning, match=expected): with pytest.warns(DeprecationWarning, match=expected):
_deprecate.deprecate("Old thing", version, "new thing") _deprecate.deprecate("Old thing", version, "new thing")
@ -46,7 +46,7 @@ def test_unknown_version() -> None:
), ),
], ],
) )
def test_old_version(deprecated, plural, expected) -> None: def test_old_version(deprecated: str, plural: bool, expected: str) -> None:
expected = r"" expected = r""
with pytest.raises(RuntimeError, match=expected): with pytest.raises(RuntimeError, match=expected):
_deprecate.deprecate(deprecated, 1, plural=plural) _deprecate.deprecate(deprecated, 1, plural=plural)
@ -76,7 +76,7 @@ def test_replacement_and_action() -> None:
"Upgrade to new thing.", "Upgrade to new thing.",
], ],
) )
def test_action(action) -> None: def test_action(action: str) -> None:
expected = ( expected = (
r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. " r"Old thing is deprecated and will be removed in Pillow 11 \(2024-10-15\)\. "
r"Upgrade to new thing\." r"Upgrade to new thing\."

View File

@ -135,7 +135,7 @@ def test_different_bit_depths(tmp_path: Path) -> None:
@pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA")) @pytest.mark.parametrize("mode", ("1", "L", "P", "RGB", "RGBA"))
def test_save_to_bytes_bmp(mode) -> None: def test_save_to_bytes_bmp(mode: str) -> None:
output = io.BytesIO() output = io.BytesIO()
im = hopper(mode) im = hopper(mode)
im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)]) im.save(output, "ico", bitmap_format="bmp", sizes=[(32, 32), (64, 64)])

View File

@ -98,7 +98,7 @@ def test_i() -> None:
assert ret == 97 assert ret == 97
def test_dump(monkeypatch) -> None: def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange # Arrange
c = b"abc" c = b"abc"
# Temporarily redirect stdout # Temporarily redirect stdout

View File

@ -52,7 +52,7 @@ def test_open_windows_v1() -> None:
assert isinstance(im, MspImagePlugin.MspImageFile) assert isinstance(im, MspImagePlugin.MspImageFile)
def _assert_file_image_equal(source_path, target_path) -> None: def _assert_file_image_equal(source_path: str, target_path: str) -> None:
with Image.open(source_path) as im: with Image.open(source_path) as im:
assert_image_equal_tofile(im, target_path) assert_image_equal_tofile(im, target_path)

View File

@ -6,6 +6,7 @@ import warnings
import zlib import zlib
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from types import ModuleType
from typing import Any from typing import Any
import pytest import pytest
@ -23,6 +24,7 @@ from .helper import (
skip_unless_feature, skip_unless_feature,
) )
ElementTree: ModuleType | None
try: try:
from defusedxml import ElementTree from defusedxml import ElementTree
except ImportError: except ImportError:

View File

@ -157,7 +157,7 @@ def test_combined_larger_than_size() -> None:
("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError), ("Tests/images/timeout-dedc7a4ebd856d79b4359bbcc79e8ef231ce38f6.psd", OSError),
], ],
) )
def test_crashes(test_file, raises) -> None: def test_crashes(test_file: str, raises) -> None:
with open(test_file, "rb") as f: with open(test_file, "rb") as f:
with pytest.raises(raises): with pytest.raises(raises):
with Image.open(f): with Image.open(f):

View File

@ -22,8 +22,8 @@ _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1}
@pytest.mark.parametrize("mode", _MODES) @pytest.mark.parametrize("mode", _MODES)
def test_sanity(mode, tmp_path: Path) -> None: def test_sanity(mode: str, tmp_path: Path) -> None:
def roundtrip(original_im) -> None: def roundtrip(original_im: Image.Image) -> None:
out = str(tmp_path / "temp.tga") out = str(tmp_path / "temp.tga")
original_im.save(out, rle=rle) original_im.save(out, rle=rle)

View File

@ -189,7 +189,9 @@ def test_iptc(tmp_path: Path) -> None:
@pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1"))) @pytest.mark.parametrize("value, expected", ((b"test", "test"), (1, "1")))
def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None: def test_writing_other_types_to_ascii(
value: bytes | int, expected: str, tmp_path: Path
) -> None:
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()
tag = TiffTags.TAGS_V2[271] tag = TiffTags.TAGS_V2[271]
@ -206,7 +208,7 @@ def test_writing_other_types_to_ascii(value, expected, tmp_path: Path) -> None:
@pytest.mark.parametrize("value", (1, IFDRational(1))) @pytest.mark.parametrize("value", (1, IFDRational(1)))
def test_writing_other_types_to_bytes(value, tmp_path: Path) -> None: def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) -> None:
im = hopper() im = hopper()
info = TiffImagePlugin.ImageFileDirectory_v2() info = TiffImagePlugin.ImageFileDirectory_v2()

View File

@ -237,7 +237,7 @@ def test_invalid_color_temperature() -> None:
@pytest.mark.parametrize("flag", ("my string", -1)) @pytest.mark.parametrize("flag", ("my string", -1))
def test_invalid_flag(flag) -> None: def test_invalid_flag(flag: str | int) -> None:
with hopper() as im: with hopper() as im:
with pytest.raises( with pytest.raises(
ImageCms.PyCMSError, match="flags must be an integer between 0 and " ImageCms.PyCMSError, match="flags must be an integer between 0 and "
@ -335,12 +335,26 @@ def test_extended_information() -> None:
o = ImageCms.getOpenProfile(SRGB) o = ImageCms.getOpenProfile(SRGB)
p = o.profile p = o.profile
def assert_truncated_tuple_equal(tup1, tup2, digits: int = 10) -> None: def assert_truncated_tuple_equal(
tup1: tuple[tuple[float, float, float], ...] | tuple[float],
tup2: (
tuple[tuple[tuple[float, float, float], ...], ...]
| tuple[tuple[float, float, float], ...]
| tuple[float]
),
digits: int = 10,
) -> None:
# Helper function to reduce precision of tuples of floats # Helper function to reduce precision of tuples of floats
# recursively and then check equality. # recursively and then check equality.
power = 10**digits power = 10**digits
def truncate_tuple(tuple_or_float): def truncate_tuple(
tuple_or_float: (
tuple[tuple[tuple[float, float, float], ...], ...]
| tuple[tuple[float, float, float], ...]
| tuple[float, ...]
)
) -> tuple[tuple[float, ...], ...]:
return tuple( return tuple(
( (
truncate_tuple(val) truncate_tuple(val)
@ -504,8 +518,10 @@ def test_profile_typesafety() -> None:
ImageCms.ImageCmsProfile(1).tobytes() ImageCms.ImageCmsProfile(1).tobytes()
def assert_aux_channel_preserved(mode, transform_in_place, preserved_channel) -> None: def assert_aux_channel_preserved(
def create_test_image(): mode: str, transform_in_place: bool, preserved_channel: str
) -> None:
def create_test_image() -> Image.Image:
# set up test image with something interesting in the tested aux channel. # set up test image with something interesting in the tested aux channel.
# fmt: off # fmt: off
nine_grid_deltas = [ nine_grid_deltas = [
@ -633,7 +649,7 @@ def test_auxiliary_channels_isolated() -> None:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "RGBX"))
def test_rgb_lab(mode) -> None: def test_rgb_lab(mode: str) -> None:
im = Image.new(mode, (1, 1)) im = Image.new(mode, (1, 1))
converted_im = im.convert("LAB") converted_im = im.convert("LAB")
assert converted_im.getpixel((0, 0)) == (0, 128, 128) assert converted_im.getpixel((0, 0)) == (0, 128, 128)

View File

@ -7,7 +7,7 @@ import shutil
import sys import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import BinaryIO from typing import Any, BinaryIO
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -44,7 +44,7 @@ def test_sanity() -> None:
pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")), pytest.param(ImageFont.Layout.RAQM, marks=skip_unless_feature("raqm")),
], ],
) )
def layout_engine(request): def layout_engine(request: pytest.FixtureRequest) -> ImageFont.Layout:
return request.param return request.param
@ -535,21 +535,23 @@ def test_unicode_extended(layout_engine: ImageFont.Layout) -> None:
(("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")), (("linux", "/usr/local/share/fonts"), ("darwin", "/System/Library/Fonts")),
) )
@pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS")
def test_find_font(monkeypatch, platform, font_directory) -> None: def test_find_font(
monkeypatch: pytest.MonkeyPatch, platform: str, font_directory: str
) -> None:
def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None: def _test_fake_loading_font(path_to_fake: str, fontname: str) -> None:
# Make a copy of FreeTypeFont so we can patch the original # Make a copy of FreeTypeFont so we can patch the original
free_type_font = copy.deepcopy(ImageFont.FreeTypeFont) free_type_font = copy.deepcopy(ImageFont.FreeTypeFont)
with monkeypatch.context() as m: with monkeypatch.context() as m:
m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False) m.setattr(ImageFont, "_FreeTypeFont", free_type_font, raising=False)
def loadable_font(filepath, size, index, encoding, *args, **kwargs): def loadable_font(
filepath: str, size: int, index: int, encoding: str, *args: Any
):
if filepath == path_to_fake: if filepath == path_to_fake:
return ImageFont._FreeTypeFont( return ImageFont._FreeTypeFont(
FONT_PATH, size, index, encoding, *args, **kwargs FONT_PATH, size, index, encoding, *args
) )
return ImageFont._FreeTypeFont( return ImageFont._FreeTypeFont(filepath, size, index, encoding, *args)
filepath, size, index, encoding, *args, **kwargs
)
m.setattr(ImageFont, "FreeTypeFont", loadable_font) m.setattr(ImageFont, "FreeTypeFont", loadable_font)
font = ImageFont.truetype(fontname) font = ImageFont.truetype(fontname)
@ -563,7 +565,7 @@ def test_find_font(monkeypatch, platform, font_directory) -> None:
if platform == "linux": if platform == "linux":
monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/") monkeypatch.setenv("XDG_DATA_DIRS", "/usr/share/:/usr/local/share/")
def fake_walker(path): def fake_walker(path: str) -> list[tuple[str, list[str], list[str]]]:
if path == font_directory: if path == font_directory:
return [ return [
( (
@ -1101,7 +1103,7 @@ def test_oom(test_file: str) -> None:
font.getmask("Test Text") font.getmask("Test Text")
def test_raqm_missing_warning(monkeypatch) -> None: def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False) monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
with pytest.warns(UserWarning) as record: with pytest.warns(UserWarning) as record:
font = ImageFont.truetype( font = ImageFont.truetype(

View File

@ -84,6 +84,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_file(self) -> None: def test_grabclipboard_file(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
assert p.stdin is not None
p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"') p.stdin.write(rb'Set-Clipboard -Path "Tests\images\hopper.gif"')
p.communicate() p.communicate()
@ -94,6 +95,7 @@ $bmp = New-Object Drawing.Bitmap 200, 200
@pytest.mark.skipif(sys.platform != "win32", reason="Windows only") @pytest.mark.skipif(sys.platform != "win32", reason="Windows only")
def test_grabclipboard_png(self) -> None: def test_grabclipboard_png(self) -> None:
p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE) p = subprocess.Popen(["powershell", "-command", "-"], stdin=subprocess.PIPE)
assert p.stdin is not None
p.stdin.write( p.stdin.write(
rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png") rb"""$bytes = [System.IO.File]::ReadAllBytes("Tests\images\hopper.png")
$ms = new-object System.IO.MemoryStream(, $bytes) $ms = new-object System.IO.MemoryStream(, $bytes)
@ -113,7 +115,7 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only", reason="Linux with wl-clipboard only",
) )
@pytest.mark.parametrize("ext", ("gif", "png", "ico")) @pytest.mark.parametrize("ext", ("gif", "png", "ico"))
def test_grabclipboard_wl_clipboard(self, ext) -> None: def test_grabclipboard_wl_clipboard(self, ext: str) -> None:
image_path = "Tests/images/hopper." + ext image_path = "Tests/images/hopper." + ext
with open(image_path, "rb") as fp: with open(image_path, "rb") as fp:
subprocess.call(["wl-copy"], stdin=fp) subprocess.call(["wl-copy"], stdin=fp)
@ -128,6 +130,6 @@ $ms = new-object System.IO.MemoryStream(, $bytes)
reason="Linux with wl-clipboard only", reason="Linux with wl-clipboard only",
) )
@pytest.mark.parametrize("arg", ("text", "--clear")) @pytest.mark.parametrize("arg", ("text", "--clear"))
def test_grabclipboard_wl_clipboard_errors(self, arg): def test_grabclipboard_wl_clipboard_errors(self, arg: str) -> None:
subprocess.call(["wl-copy", arg]) subprocess.call(["wl-copy", arg])
assert ImageGrab.grabclipboard() is None assert ImageGrab.grabclipboard() is None

View File

@ -58,7 +58,9 @@ def test_path() -> None:
ImagePath.Path((0, 1)), ImagePath.Path((0, 1)),
), ),
) )
def test_path_constructors(coords) -> None: def test_path_constructors(
coords: Sequence[float] | array.array[float] | ImagePath.Path,
) -> None:
# Arrange / Act # Arrange / Act
p = ImagePath.Path(coords) p = ImagePath.Path(coords)
@ -206,9 +208,9 @@ class Evil:
def __init__(self) -> None: def __init__(self) -> None:
self.corrupt = Image.core.path(0x4000000000000000) self.corrupt = Image.core.path(0x4000000000000000)
def __getitem__(self, i): def __getitem__(self, i: int) -> bytes:
x = self.corrupt[i] x = self.corrupt[i]
return struct.pack("dd", x[0], x[1]) return struct.pack("dd", x[0], x[1])
def __setitem__(self, i, x) -> None: def __setitem__(self, i: int, x: bytes) -> None:
self.corrupt[i] = struct.unpack("dd", x) self.corrupt[i] = struct.unpack("dd", x)

View File

@ -28,7 +28,7 @@ def test_rgb() -> None:
assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255) assert qRgb(0, 0, 0) == qRgba(0, 0, 0, 255)
def checkrgb(r, g, b) -> None: def checkrgb(r: int, g: int, b: int) -> None:
val = ImageQt.rgb(r, g, b) val = ImageQt.rgb(r, g, b)
val = val % 2**24 # drop the alpha val = val % 2**24 # drop the alpha
assert val >> 16 == r assert val >> 16 == r

View File

@ -1,5 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
import pytest import pytest
from PIL import Image, ImageShow from PIL import Image, ImageShow
@ -24,9 +26,9 @@ def test_register() -> None:
"order", "order",
[-1, 0], [-1, 0],
) )
def test_viewer_show(order) -> None: def test_viewer_show(order: int) -> None:
class TestViewer(ImageShow.Viewer): class TestViewer(ImageShow.Viewer):
def show_image(self, image, **options) -> bool: def show_image(self, image: Image.Image, **options: Any) -> bool:
self.methodCalled = True self.methodCalled = True
return True return True
@ -48,7 +50,7 @@ def test_viewer_show(order) -> None:
reason="Only run on CIs; hangs on Windows CIs", reason="Only run on CIs; hangs on Windows CIs",
) )
@pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA")) @pytest.mark.parametrize("mode", ("1", "I;16", "LA", "RGB", "RGBA"))
def test_show(mode) -> None: def test_show(mode: str) -> None:
im = hopper(mode) im = hopper(mode)
assert ImageShow.show(im) assert ImageShow.show(im)
@ -73,7 +75,7 @@ def test_viewer() -> None:
@pytest.mark.parametrize("viewer", ImageShow._viewers) @pytest.mark.parametrize("viewer", ImageShow._viewers)
def test_viewers(viewer) -> None: def test_viewers(viewer: ImageShow.Viewer) -> None:
try: try:
viewer.get_command("test.jpg") viewer.get_command("test.jpg")
except NotImplementedError: except NotImplementedError:

View File

@ -70,7 +70,7 @@ if is_win32():
] ]
CreateDIBSection.restype = ctypes.wintypes.HBITMAP CreateDIBSection.restype = ctypes.wintypes.HBITMAP
def serialize_dib(bi, pixels): def serialize_dib(bi, pixels) -> bytearray:
bf = BITMAPFILEHEADER() bf = BITMAPFILEHEADER()
bf.bfType = 0x4D42 bf.bfType = 0x4D42
bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize bf.bfOffBits = ctypes.sizeof(bf) + bi.biSize

View File

@ -14,7 +14,7 @@ TEST_IMAGE_SIZE = (10, 10)
def test_numpy_to_image() -> None: def test_numpy_to_image() -> None:
def to_image(dtype, bands: int = 1, boolean: int = 0): def to_image(dtype, bands: int = 1, boolean: int = 0) -> Image.Image:
if bands == 1: if bands == 1:
if boolean: if boolean:
data = [0, 255] * 50 data = [0, 255] * 50
@ -99,7 +99,7 @@ def test_1d_array() -> None:
assert_image(Image.fromarray(a), "L", (1, 5)) assert_image(Image.fromarray(a), "L", (1, 5))
def _test_img_equals_nparray(img, np) -> None: def _test_img_equals_nparray(img: Image.Image, np) -> None:
assert len(np.shape) >= 2 assert len(np.shape) >= 2
np_size = np.shape[1], np.shape[0] np_size = np.shape[1], np.shape[0]
assert img.size == np_size assert img.size == np_size
@ -157,7 +157,7 @@ def test_save_tiff_uint16() -> None:
("HSV", numpy.uint8), ("HSV", numpy.uint8),
), ),
) )
def test_to_array(mode, dtype) -> None: def test_to_array(mode: str, dtype) -> None:
img = hopper(mode) img = hopper(mode)
# Resize to non-square # Resize to non-square

View File

@ -4,7 +4,7 @@ from pathlib import Path
import pytest import pytest
from PIL import ImageQt from PIL import Image, ImageQt
from .helper import assert_image_equal_tofile, assert_image_similar, hopper from .helper import assert_image_equal_tofile, assert_image_similar, hopper
@ -37,7 +37,7 @@ if ImageQt.qt_is_installed:
lbl.setPixmap(pixmap1.copy()) lbl.setPixmap(pixmap1.copy())
def roundtrip(expected) -> None: def roundtrip(expected: Image.Image) -> None:
result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected)) result = ImageQt.fromqpixmap(ImageQt.toqpixmap(expected))
# Qt saves all pixmaps as rgb # Qt saves all pixmaps as rgb
assert_image_similar(result, expected.convert("RGB"), 1) assert_image_similar(result, expected.convert("RGB"), 1)

View File

@ -17,7 +17,7 @@ if ImageQt.qt_is_installed:
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1")) @pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
def test_sanity(mode, tmp_path: Path) -> None: def test_sanity(mode: str, tmp_path: Path) -> None:
src = hopper(mode) src = hopper(mode)
data = ImageQt.toqimage(src) data = ImageQt.toqimage(src)

View File

@ -10,7 +10,7 @@ from PIL import _util
@pytest.mark.parametrize( @pytest.mark.parametrize(
"test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")]
) )
def test_is_path(test_path) -> None: def test_is_path(test_path: str | Path | PurePath) -> None:
# Act # Act
it_is = _util.is_path(test_path) it_is = _util.is_path(test_path)