From 231d54b9df90dcce1cdbb9928f8d21899eed8153 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:37:37 +0200 Subject: [PATCH 01/29] Replace io.BytesIO in type hints --- docs/reference/internal_design.rst | 4 ++-- docs/reference/internal_modules.rst | 12 ++++++++++++ src/PIL/GdImageFile.py | 7 +++++-- src/PIL/MpegImagePlugin.py | 5 ++--- src/PIL/_typing.py | 16 +++++++++++++++- src/PIL/_util.py | 4 ++-- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/reference/internal_design.rst b/docs/reference/internal_design.rst index 2e2d3322f..99a18e9ea 100644 --- a/docs/reference/internal_design.rst +++ b/docs/reference/internal_design.rst @@ -1,5 +1,5 @@ -Internal Reference Docs -======================= +Internal Reference +================== .. toctree:: :maxdepth: 2 diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index f2932c322..c3cc70060 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,6 +33,18 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. +.. py:class:: FileDescriptor + + Typing alias. + +.. py:class:: StrOrBytesPath + + Typing alias. + +.. py:class:: SupportsRead + + An object that supports the read method. + .. py:data:: TypeGuard :value: typing.TypeGuard diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 7bb4736af..315ac6d6c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -27,11 +27,12 @@ """ from __future__ import annotations -from io import BytesIO +from typing import IO from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 +from ._typing import FileDescriptor, StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -80,7 +81,9 @@ class GdImageFile(ImageFile.ImageFile): ] -def open(fp: BytesIO, mode: str = "r") -> GdImageFile: +def open( + fp: StrOrBytesPath | FileDescriptor | IO[bytes], mode: str = "r" +) -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/MpegImagePlugin.py b/src/PIL/MpegImagePlugin.py index b9e9243e5..1565612f8 100644 --- a/src/PIL/MpegImagePlugin.py +++ b/src/PIL/MpegImagePlugin.py @@ -14,17 +14,16 @@ # from __future__ import annotations -from io import BytesIO - from . import Image, ImageFile from ._binary import i8 +from ._typing import SupportsRead # # Bitstream parser class BitStream: - def __init__(self, fp: BytesIO) -> None: + def __init__(self, fp: SupportsRead[bytes]) -> None: self.fp = fp self.bits = 0 self.bitbuffer = 0 diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 608b2b41f..6eb25c1c1 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,8 @@ from __future__ import annotations +import os import sys +from typing import Protocol, TypeVar, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +17,16 @@ else: return bool -__all__ = ["TypeGuard"] +_T_co = TypeVar("_T_co", covariant=True) + + +class SupportsRead(Protocol[_T_co]): + def read(self, __length: int = ...) -> _T_co: + ... + + +FileDescriptor = int +StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] + + +__all__ = ["FileDescriptor", "TypeGuard", "StrOrBytesPath", "SupportsRead"] diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 13f369cca..4ecdc4bd3 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -4,10 +4,10 @@ import os from pathlib import Path from typing import Any, NoReturn -from ._typing import TypeGuard +from ._typing import StrOrBytesPath, TypeGuard -def is_path(f: Any) -> TypeGuard[bytes | str | Path]: +def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, Path)) From e3932b7dbaf6aff8ef4b7a24007f4de07477ec91 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Tue, 23 Jan 2024 23:58:41 +0200 Subject: [PATCH 02/29] Exclude from coverage: empty bodies in protocols or abstract methods --- .coveragerc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coveragerc b/.coveragerc index 46df3f90d..5678e4566 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,9 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + # Empty bodies in protocols or abstract methods + ^\s*def [a-zA-Z0-9_]+\(.*\)(\s*->.*)?:\s*\.\.\.(\s*#.*)?$ + ^\s*\.\.\.(\s*#.*)?$ [run] omit = From 945253672a74415807b5f685f54ebb1533ff468e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:11:18 +0200 Subject: [PATCH 03/29] Handle os.PathLike in is_path --- src/PIL/ImageFont.py | 2 +- src/PIL/_util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33..9eecad1ca 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,7 +230,7 @@ class FreeTypeFont: ) if is_path(font): - if isinstance(font, Path): + if isinstance(font, os.PathLike): font = str(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() diff --git a/src/PIL/_util.py b/src/PIL/_util.py index 4ecdc4bd3..b649500ab 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -8,7 +8,7 @@ from ._typing import StrOrBytesPath, TypeGuard def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: - return isinstance(f, (bytes, str, Path)) + return isinstance(f, (bytes, str, os.PathLike)) def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: From f613a9213f4edc7b58ac84a4793223c8e4fd9191 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:15:19 +0200 Subject: [PATCH 04/29] Parameterise test --- Tests/test_util.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 3395ef753..71a862569 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,27 +1,14 @@ from __future__ import annotations +from pathlib import Path + import pytest from PIL import _util -def test_is_path(): - # Arrange - fp = "filename.ext" - - # Act - it_is = _util.is_path(fp) - - # Assert - assert it_is - - -def test_path_obj_is_path(): - # Arrange - from pathlib import Path - - test_path = Path("filename.ext") - +@pytest.mark.parametrize("test_path", ["filename.ext", Path("filename.ext")]) +def test_is_path(test_path): # Act it_is = _util.is_path(test_path) From 16d4068b42f0a6069e14b2327302df713dabbfed Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 19:17:13 +0200 Subject: [PATCH 05/29] Test os.PathLike that's not pathlib.Path --- Tests/test_util.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/test_util.py b/Tests/test_util.py index 71a862569..617e5f7c6 100644 --- a/Tests/test_util.py +++ b/Tests/test_util.py @@ -1,13 +1,15 @@ from __future__ import annotations -from pathlib import Path +from pathlib import Path, PurePath import pytest from PIL import _util -@pytest.mark.parametrize("test_path", ["filename.ext", Path("filename.ext")]) +@pytest.mark.parametrize( + "test_path", ["filename.ext", Path("filename.ext"), PurePath("filename.ext")] +) def test_is_path(test_path): # Act it_is = _util.is_path(test_path) From d631afc266c3c1214e12373d3ad0d16978867a7f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Fri, 26 Jan 2024 20:46:58 +0200 Subject: [PATCH 06/29] Use os.fspath instead of isinstance and str --- src/PIL/ImageFont.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9eecad1ca..1feaf447a 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,8 +230,7 @@ class FreeTypeFont: ) if is_path(font): - if isinstance(font, os.PathLike): - font = str(font) + font = os.fspath(font) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 61d47c3dfa200b186ecacd7b9a5090cedb5523b6 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 27 Jan 2024 14:06:06 +0200 Subject: [PATCH 07/29] More support for arbitrary os.PathLike --- Tests/test_image.py | 3 +-- docs/reference/open_files.rst | 2 +- src/PIL/Image.py | 16 ++++++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index dd989ad99..84189df54 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -7,6 +7,7 @@ import shutil import sys import tempfile import warnings +from pathlib import Path import pytest @@ -161,8 +162,6 @@ class TestImage: pass def test_pathlib(self, tmp_path): - from PIL.Image import Path - with Image.open(Path("Tests/images/multipage-mmap.tiff")) as im: assert im.mode == "P" assert im.size == (10, 10) diff --git a/docs/reference/open_files.rst b/docs/reference/open_files.rst index f31941c9a..730c8da5b 100644 --- a/docs/reference/open_files.rst +++ b/docs/reference/open_files.rst @@ -3,7 +3,7 @@ File Handling in Pillow ======================= -When opening a file as an image, Pillow requires a filename, ``pathlib.Path`` +When opening a file as an image, Pillow requires a filename, ``os.PathLike`` object, or a file-like object. Pillow uses the filename or ``Path`` to open a file, so for the rest of this article, they will all be treated as a file-like object. diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 553f36703..48125b317 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -39,7 +39,6 @@ import tempfile import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum -from pathlib import Path try: from defusedxml import ElementTree @@ -2370,7 +2369,7 @@ class Image: implement the ``seek``, ``tell``, and ``write`` methods, and be opened in binary mode. - :param fp: A filename (string), pathlib.Path object or file object. + :param fp: A filename (string), os.PathLike object or file object. :param format: Optional format override. If omitted, the format to use is determined from the filename extension. If a file object was used instead of a filename, this @@ -2385,11 +2384,8 @@ class Image: filename = "" open_fp = False - if isinstance(fp, Path): - filename = str(fp) - open_fp = True - elif is_path(fp): - filename = fp + if is_path(fp): + filename = os.fspath(fp) open_fp = True elif fp == sys.stdout: try: @@ -3206,7 +3202,7 @@ def open(fp, mode="r", formats=None) -> Image: :py:meth:`~PIL.Image.Image.load` method). See :py:func:`~PIL.Image.new`. See :ref:`file-handling`. - :param fp: A filename (string), pathlib.Path object or a file object. + :param fp: A filename (string), os.PathLike object or a file object. The file object must implement ``file.read``, ``file.seek``, and ``file.tell`` methods, and be opened in binary mode. The file object will also seek to zero @@ -3244,8 +3240,8 @@ def open(fp, mode="r", formats=None) -> Image: exclusive_fp = False filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) + if isinstance(fp, os.PathLike): + filename = os.path.realpath(os.fspath(fp)) elif is_path(fp): filename = fp From bb1fece57a2c894773597c9a6fb10bd81e36123d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 31 Jan 2024 21:55:32 +1100 Subject: [PATCH 08/29] Added type hints --- Tests/test_bmp_reference.py | 6 +- Tests/test_box_blur.py | 12 +- Tests/test_file_apng.py | 6 +- Tests/test_file_container.py | 10 +- Tests/test_file_gif.py | 30 ++-- Tests/test_file_mpo.py | 19 +-- Tests/test_file_ppm.py | 22 +-- Tests/test_file_sgi.py | 2 +- Tests/test_image_frombytes.py | 2 +- Tests/test_image_load.py | 2 +- Tests/test_imagedraw.py | 291 ++++++++++++++++++++++++++++------ Tests/test_imagetk.py | 4 +- Tests/test_lib_pack.py | 16 +- Tests/test_mode_i16.py | 6 +- 14 files changed, 327 insertions(+), 101 deletions(-) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 22ac9443e..0ad496135 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -10,7 +10,7 @@ from .helper import assert_image_similar base = os.path.join("Tests", "images", "bmp") -def get_files(d, ext: str = ".bmp"): +def get_files(d: str, ext: str = ".bmp") -> list[str]: return [ os.path.join(base, d, f) for f in os.listdir(os.path.join(base, d)) if ext in f ] @@ -29,7 +29,7 @@ def test_bad() -> None: pass -def test_questionable(): +def test_questionable() -> None: """These shouldn't crash/dos, but it's not well defined that these are in spec""" supported = [ @@ -80,7 +80,7 @@ def test_good() -> None: "rgb32bf.bmp": "rgb24.png", } - def get_compare(f): + def get_compare(f: str) -> str: name = os.path.split(f)[1] if name in file_map: return os.path.join(base, "html", file_map[name]) diff --git a/Tests/test_box_blur.py b/Tests/test_box_blur.py index dfedb48d9..1f6ed6127 100644 --- a/Tests/test_box_blur.py +++ b/Tests/test_box_blur.py @@ -23,11 +23,11 @@ def test_imageops_box_blur() -> None: assert isinstance(i, Image.Image) -def box_blur(image, radius: int = 1, n: int = 1): +def box_blur(image: Image.Image, radius: float = 1, n: int = 1) -> Image.Image: return image._new(image.im.box_blur((radius, radius), n)) -def assert_image(im, data, delta: int = 0) -> None: +def assert_image(im: Image.Image, data: list[list[int]], delta: int = 0) -> None: it = iter(im.getdata()) for data_row in data: im_row = [next(it) for _ in range(im.size[0])] @@ -37,7 +37,13 @@ def assert_image(im, data, delta: int = 0) -> None: next(it) -def assert_blur(im, radius, data, passes: int = 1, delta: int = 0) -> None: +def assert_blur( + im: Image.Image, + radius: float, + data: list[list[int]], + passes: int = 1, + delta: int = 0, +) -> None: # check grayscale image assert_image(box_blur(im, radius, passes), data, delta) rgba = Image.merge("RGBA", (im, im, im, im)) diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index f9edf6e98..395165b36 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -47,7 +47,7 @@ def test_apng_basic() -> None: "filename", ("Tests/images/apng/split_fdat.png", "Tests/images/apng/split_fdat_zero_chunk.png"), ) -def test_apng_fdat(filename) -> None: +def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) @@ -338,7 +338,7 @@ def test_apng_syntax_errors() -> None: "sequence_fdat_fctl.png", ), ) -def test_apng_sequence_errors(test_file) -> None: +def test_apng_sequence_errors(test_file: str) -> None: with pytest.raises(SyntaxError): with Image.open(f"Tests/images/apng/{test_file}") as im: im.seek(im.n_frames - 1) @@ -681,7 +681,7 @@ def test_seek_after_close() -> None: @pytest.mark.parametrize("default_image", (True, False)) @pytest.mark.parametrize("duplicate", (True, False)) def test_different_modes_in_later_frames( - mode, default_image, duplicate, tmp_path: Path + mode: str, default_image: bool, duplicate: bool, tmp_path: Path ) -> None: test_file = str(tmp_path / "temp.png") diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 4dba4be5d..813b444db 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -64,7 +64,7 @@ def test_seek_mode_2() -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n0(bytesmode) -> None: +def test_read_n0(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -80,7 +80,7 @@ def test_read_n0(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_n(bytesmode) -> None: +def test_read_n(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -96,7 +96,7 @@ def test_read_n(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_read_eof(bytesmode) -> None: +def test_read_eof(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 22, 100) @@ -112,7 +112,7 @@ def test_read_eof(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readline(bytesmode) -> None: +def test_readline(bytesmode: bool) -> None: # Arrange with open(TEST_FILE, "rb" if bytesmode else "r") as fh: container = ContainerIO.ContainerIO(fh, 0, 120) @@ -127,7 +127,7 @@ def test_readline(bytesmode) -> None: @pytest.mark.parametrize("bytesmode", (True, False)) -def test_readlines(bytesmode) -> None: +def test_readlines(bytesmode: bool) -> None: # Arrange expected = [ "This is line 1\n", diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 3f550fd11..db9d3586c 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -3,6 +3,7 @@ from __future__ import annotations import warnings from io import BytesIO from pathlib import Path +from typing import Generator import pytest @@ -144,13 +145,13 @@ def test_strategy() -> None: def test_optimize() -> None: - def test_grayscale(optimize): + def test_grayscale(optimize: int) -> int: im = Image.new("L", (1, 1), 0) filename = BytesIO() im.save(filename, "GIF", optimize=optimize) return len(filename.getvalue()) - def test_bilevel(optimize): + def test_bilevel(optimize: int) -> int: im = Image.new("1", (1, 1), 0) test_file = BytesIO() im.save(test_file, "GIF", optimize=optimize) @@ -178,7 +179,9 @@ def test_optimize() -> None: (4, 513, 256), ), ) -def test_optimize_correctness(colors, size, expected_palette_length) -> None: +def test_optimize_correctness( + colors: int, size: int, expected_palette_length: int +) -> None: # 256 color Palette image, posterize to > 128 and < 128 levels. # Size bigger and smaller than 512x512. # Check the palette for number of colors allocated. @@ -297,7 +300,7 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: ("Tests/images/dispose_bgnd_rgba.gif", "RGBA"), ), ) -def test_loading_multiple_palettes(path, mode) -> None: +def test_loading_multiple_palettes(path: str, mode: str) -> None: with Image.open(path) as im: assert im.mode == "P" first_frame_colors = im.palette.colors.keys() @@ -347,9 +350,9 @@ def test_palette_handling(tmp_path: Path) -> None: def test_palette_434(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/434 - def roundtrip(im, *args, **kwargs): + def roundtrip(im: Image.Image, **kwargs: bool) -> Image.Image: out = str(tmp_path / "temp.gif") - im.copy().save(out, *args, **kwargs) + im.copy().save(out, **kwargs) reloaded = Image.open(out) return reloaded @@ -429,7 +432,7 @@ def test_seek_rewind() -> None: ("Tests/images/iss634.gif", 42), ), ) -def test_n_frames(path, n_frames) -> None: +def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: assert im.is_animated == (n_frames != 1) @@ -541,7 +544,10 @@ def test_dispose_background_transparency() -> None: ), ), ) -def test_transparent_dispose(loading_strategy, expected_colors) -> None: +def test_transparent_dispose( + loading_strategy: GifImagePlugin.LoadingStrategy, + expected_colors: tuple[tuple[int | tuple[int, int, int, int], ...]], +) -> None: GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/transparent_dispose.gif") as img: @@ -889,7 +895,9 @@ def test_identical_frames(tmp_path: Path) -> None: 1500, ), ) -def test_identical_frames_to_single_frame(duration, tmp_path: Path) -> None: +def test_identical_frames_to_single_frame( + duration: int | list[int], tmp_path: Path +) -> None: out = str(tmp_path / "temp.gif") im_list = [ Image.new("L", (100, 100), "#000"), @@ -1049,7 +1057,7 @@ def test_retain_comment_in_subsequent_frames(tmp_path: Path) -> None: def test_version(tmp_path: Path) -> None: out = str(tmp_path / "temp.gif") - def assert_version_after_save(im, version) -> None: + def assert_version_after_save(im: Image.Image, version: bytes) -> None: im.save(out) with Image.open(out) as reread: assert reread.info["version"] == version @@ -1088,7 +1096,7 @@ def test_append_images(tmp_path: Path) -> None: assert reread.n_frames == 3 # Tests appending using a generator - def im_generator(ims): + def im_generator(ims: list[Image.Image]) -> Generator[Image.Image, None, None]: yield from ims im.save(out, save_all=True, append_images=im_generator(ims)) diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index 55b04a1e0..4fb00d699 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -2,6 +2,7 @@ from __future__ import annotations import warnings from io import BytesIO +from typing import Any import pytest @@ -19,7 +20,7 @@ test_files = ["Tests/images/sugarshack.mpo", "Tests/images/frozenpond.mpo"] pytestmark = skip_unless_feature("jpg") -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "MPO", **options) test_bytes = out.tell() @@ -30,7 +31,7 @@ def roundtrip(im, **options): @pytest.mark.parametrize("test_file", test_files) -def test_sanity(test_file) -> None: +def test_sanity(test_file: str) -> None: with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -70,7 +71,7 @@ def test_context_manager() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_app(test_file) -> None: +def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: assert im.applist[0][0] == "APP1" @@ -82,7 +83,7 @@ def test_app(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_exif(test_file) -> None: +def test_exif(test_file: str) -> None: with Image.open(test_file) as im_original: im_reloaded = roundtrip(im_original, save_all=True, exif=im_original.getexif()) @@ -143,7 +144,7 @@ def test_reload_exif_after_seek() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_mp(test_file) -> None: +def test_mp(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() assert mpinfo[45056] == b"0100" @@ -168,7 +169,7 @@ def test_mp_no_data() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_mp_attribute(test_file) -> None: +def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: mpinfo = im._getmp() for frame_number, mpentry in enumerate(mpinfo[0xB002]): @@ -185,7 +186,7 @@ def test_mp_attribute(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_seek(test_file) -> None: +def test_seek(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 # prior to first image raises an error, both blatant and borderline @@ -229,7 +230,7 @@ def test_eoferror() -> None: @pytest.mark.parametrize("test_file", test_files) -def test_image_grab(test_file) -> None: +def test_image_grab(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 im0 = im.tobytes() @@ -244,7 +245,7 @@ def test_image_grab(test_file) -> None: @pytest.mark.parametrize("test_file", test_files) -def test_save(test_file) -> None: +def test_save(test_file: str) -> None: with Image.open(test_file) as im: assert im.tell() == 0 jpg0 = roundtrip(im) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 94f66ee7d..6e0fa32e4 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -70,7 +70,9 @@ def test_sanity() -> None: ), ), ) -def test_arbitrary_maxval(data, mode, pixels) -> None: +def test_arbitrary_maxval( + data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...] +) -> None: fp = BytesIO(data) with Image.open(fp) as im: assert im.size == (3, 1) @@ -139,7 +141,7 @@ def test_pfm_big_endian(tmp_path: Path) -> None: b"Pf 1 1 -0.0 \0\0\0\0", ], ) -def test_pfm_invalid(data) -> None: +def test_pfm_invalid(data: bytes) -> None: with pytest.raises(ValueError): with Image.open(BytesIO(data)): pass @@ -162,7 +164,7 @@ def test_pfm_invalid(data) -> None: ), ), ) -def test_plain(plain_path, raw_path) -> None: +def test_plain(plain_path: str, raw_path: str) -> None: with Image.open(plain_path) as im: assert_image_equal_tofile(im, raw_path) @@ -186,7 +188,9 @@ def test_16bit_plain_pgm() -> None: (b"P3\n2 2\n255", b"0 0 0 001 1 1 2 2 2 255 255 255", 10**6), ), ) -def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> None: +def test_plain_data_with_comment( + tmp_path: Path, header: bytes, data: bytes, comment_count: int +) -> None: path1 = str(tmp_path / "temp1.ppm") path2 = str(tmp_path / "temp2.ppm") comment = b"# comment" * comment_count @@ -199,7 +203,7 @@ def test_plain_data_with_comment(tmp_path: Path, header, data, comment_count) -> @pytest.mark.parametrize("data", (b"P1\n128 128\n", b"P3\n128 128\n255\n")) -def test_plain_truncated_data(tmp_path: Path, data) -> None: +def test_plain_truncated_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -210,7 +214,7 @@ def test_plain_truncated_data(tmp_path: Path, data) -> None: @pytest.mark.parametrize("data", (b"P1\n128 128\n1009", b"P3\n128 128\n255\n100A")) -def test_plain_invalid_data(tmp_path: Path, data) -> None: +def test_plain_invalid_data(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -227,7 +231,7 @@ def test_plain_invalid_data(tmp_path: Path, data) -> None: b"P3\n128 128\n255\n012345678910 0", # token too long ), ) -def test_plain_ppm_token_too_long(tmp_path: Path, data) -> None: +def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(data) @@ -313,7 +317,7 @@ def test_not_enough_image_data(tmp_path: Path) -> None: @pytest.mark.parametrize("maxval", (b"0", b"65536")) -def test_invalid_maxval(maxval, tmp_path: Path) -> None: +def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) @@ -351,7 +355,7 @@ def test_mimetypes(tmp_path: Path) -> None: @pytest.mark.parametrize("buffer", (True, False)) -def test_save_stdout(buffer) -> None: +def test_save_stdout(buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_file_sgi.py b/Tests/test_file_sgi.py index 92aea0735..e13a8019e 100644 --- a/Tests/test_file_sgi.py +++ b/Tests/test_file_sgi.py @@ -72,7 +72,7 @@ def test_invalid_file() -> None: def test_write(tmp_path: Path) -> None: - def roundtrip(img) -> None: + def roundtrip(img: Image.Image) -> None: out = str(tmp_path / "temp.sgi") img.save(out, format="sgi") assert_image_equal_tofile(img, out) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 6474daba1..98c0ea0b4 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -8,7 +8,7 @@ from .helper import assert_image_equal, hopper @pytest.mark.parametrize("data_type", ("bytes", "memoryview")) -def test_sanity(data_type) -> None: +def test_sanity(data_type: str) -> None: im1 = hopper() data = im1.tobytes() diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 5b1a9ee2d..0605821e0 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -26,7 +26,7 @@ def test_close() -> None: im.getpixel((0, 0)) -def test_close_after_load(caplog) -> None: +def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: im = Image.open("Tests/images/hopper.gif") im.load() with caplog.at_level(logging.DEBUG): diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 86d25b1eb..c02ac49dd 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -74,7 +74,14 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox, start, end) -> None: +def test_arc( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], + start: float, + end: float, +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -87,7 +94,12 @@ def test_arc(bbox, start, end) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox) -> None: +def test_arc_end_le_start( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -102,7 +114,12 @@ def test_arc_end_le_start(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox) -> None: +def test_arc_no_loops( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -118,7 +135,12 @@ def test_arc_no_loops(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox) -> None: +def test_arc_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -131,7 +153,12 @@ def test_arc_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox) -> None: +def test_arc_width_pieslice_large( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -145,7 +172,12 @@ def test_arc_width_pieslice_large(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox) -> None: +def test_arc_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -158,7 +190,12 @@ def test_arc_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox) -> None: +def test_arc_width_non_whole_angle( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -200,7 +237,13 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode, bbox) -> None: +def test_chord( + mode: str, + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], +) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -214,7 +257,12 @@ def test_chord(mode, bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox) -> None: +def test_chord_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -227,7 +275,12 @@ def test_chord_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox) -> None: +def test_chord_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -240,7 +293,12 @@ def test_chord_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox) -> None: +def test_chord_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -266,7 +324,13 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode, bbox) -> None: +def test_ellipse( + mode: str, + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], +) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -280,7 +344,12 @@ def test_ellipse(mode, bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox) -> None: +def test_ellipse_translucent( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -317,7 +386,12 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox) -> None: +def test_ellipse_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -342,7 +416,12 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox) -> None: +def test_ellipse_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -355,7 +434,12 @@ def test_ellipse_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox) -> None: +def test_ellipse_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -367,7 +451,7 @@ def test_ellipse_zero_width(bbox) -> None: assert_image_equal_tofile(im, "Tests/images/imagedraw_ellipse_zero_width.png") -def ellipse_various_sizes_helper(filled): +def ellipse_various_sizes_helper(filled: bool) -> Image.Image: ellipse_sizes = range(32) image_size = sum(ellipse_sizes) + len(ellipse_sizes) + 1 im = Image.new("RGB", (image_size, image_size)) @@ -409,7 +493,12 @@ def test_ellipse_various_sizes_filled() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line(points) -> None: +def test_line( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -482,7 +571,14 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice(bbox, start, end) -> None: +def test_pieslice( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int], + start: float, + end: float, +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -495,7 +591,12 @@ def test_pieslice(bbox, start, end) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox) -> None: +def test_pieslice_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -508,7 +609,12 @@ def test_pieslice_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox) -> None: +def test_pieslice_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -522,7 +628,12 @@ def test_pieslice_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox) -> None: +def test_pieslice_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -577,7 +688,12 @@ def test_pieslice_no_spikes() -> None: @pytest.mark.parametrize("points", POINTS) -def test_point(points) -> None: +def test_point( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -602,7 +718,12 @@ def test_point_I16() -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon(points) -> None: +def test_polygon( + points: tuple[tuple[int, int], ...] + | list[tuple[int, int]] + | tuple[int, ...] + | list[int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -616,7 +737,9 @@ def test_polygon(points) -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("kite_points", KITE_POINTS) -def test_polygon_kite(mode, kite_points) -> None: +def test_polygon_kite( + mode: str, kite_points: tuple[tuple[int, int], ...] | list[tuple[int, int]] +) -> None: # Test drawing lines of different gradients (dx>dy, dy>dx) and # vertical (dx==0) and horizontal (dy==0) lines # Arrange @@ -673,7 +796,12 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox) -> None: +def test_rectangle( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -700,7 +828,12 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox) -> None: +def test_rectangle_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -714,7 +847,12 @@ def test_rectangle_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox) -> None: +def test_rectangle_width_fill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -728,7 +866,12 @@ def test_rectangle_width_fill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox) -> None: +def test_rectangle_zero_width( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -741,7 +884,12 @@ def test_rectangle_zero_width(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox) -> None: +def test_rectangle_I16( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -754,7 +902,12 @@ def test_rectangle_I16(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox) -> None: +def test_rectangle_translucent_outline( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -772,7 +925,11 @@ def test_rectangle_translucent_outline(bbox) -> None: "xy", [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) -def test_rounded_rectangle(xy) -> None: +def test_rounded_rectangle( + xy: tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -789,7 +946,7 @@ def test_rounded_rectangle(xy) -> None: @pytest.mark.parametrize("bottom_right", (True, False)) @pytest.mark.parametrize("bottom_left", (True, False)) def test_rounded_rectangle_corners( - top_left, top_right, bottom_right, bottom_left + top_left: bool, top_right: bool, bottom_right: bool, bottom_left: bool ) -> None: corners = (top_left, top_right, bottom_right, bottom_left) @@ -824,7 +981,9 @@ def test_rounded_rectangle_corners( ((10, 20, 190, 181), 85, "height"), ], ) -def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: +def test_rounded_rectangle_non_integer_radius( + xy: tuple[int, int, int, int], radius: float, type: str +) -> None: # Arrange im = Image.new("RGB", (200, 200)) draw = ImageDraw.Draw(im) @@ -840,7 +999,12 @@ def test_rounded_rectangle_non_integer_radius(xy, radius, type) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox) -> None: +def test_rounded_rectangle_zero_radius( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -862,7 +1026,9 @@ def test_rounded_rectangle_zero_radius(bbox) -> None: ((20, 20, 80, 80), "both"), ], ) -def test_rounded_rectangle_translucent(xy, suffix) -> None: +def test_rounded_rectangle_translucent( + xy: tuple[int, int, int, int], suffix: str +) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -879,7 +1045,12 @@ def test_rounded_rectangle_translucent(xy, suffix) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox) -> None: +def test_floodfill( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -912,7 +1083,12 @@ def test_floodfill(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox) -> None: +def test_floodfill_border( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # floodfill() is experimental # Arrange @@ -934,7 +1110,12 @@ def test_floodfill_border(bbox) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox) -> None: +def test_floodfill_thresh( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # floodfill() is experimental # Arrange @@ -968,8 +1149,11 @@ def test_floodfill_not_negative() -> None: def create_base_image_draw( - size, mode=DEFAULT_MODE, background1=WHITE, background2=GRAY -): + size: tuple[int, int], + mode: str = DEFAULT_MODE, + background1: tuple[int, int, int] = WHITE, + background2: tuple[int, int, int] = GRAY, +) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) for x in range(0, size[0]): for y in range(0, size[1]): @@ -1003,7 +1187,7 @@ def test_triangle_right() -> None: "fill, suffix", ((BLACK, "width"), (None, "width_no_fill")), ) -def test_triangle_right_width(fill, suffix) -> None: +def test_triangle_right_width(fill: tuple[int, int, int] | None, suffix: str) -> None: img, draw = create_base_image_draw((100, 100)) draw.polygon([(15, 25), (85, 25), (50, 60)], fill, WHITE, width=5) assert_image_equal_tofile( @@ -1235,7 +1419,7 @@ def test_wide_line_larger_than_int() -> None: ], ], ) -def test_line_joint(xy) -> None: +def test_line_joint(xy: list[tuple[int, int]] | tuple[int, ...] | list[int]) -> None: im = Image.new("RGB", (500, 325)) draw = ImageDraw.Draw(im) @@ -1388,7 +1572,12 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox) -> None: +def test_same_color_outline( + bbox: tuple[tuple[int, int], tuple[int, int]] + | list[tuple[int, int]] + | list[int] + | tuple[int, int, int, int] +) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 @@ -1402,7 +1591,8 @@ def test_same_color_outline(bbox) -> None: # Begin for mode in ["RGB", "L"]: - for fill, outline in [["red", None], ["red", "red"], ["red", "#f00"]]: + fill = "red" + for outline in [None, "red", "#f00"]: for operation, args in { "chord": [bbox, 0, 180], "ellipse": [bbox], @@ -1417,6 +1607,7 @@ def test_same_color_outline(bbox) -> None: # Act draw_method = getattr(draw, operation) + assert isinstance(args, list) args += [fill, outline] draw_method(*args) @@ -1434,7 +1625,9 @@ def test_same_color_outline(bbox) -> None: (3, "triangle_width", {"width": 5, "outline": "yellow"}), ], ) -def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: +def test_draw_regular_polygon( + n_sides: int, polygon_name: str, args: dict[str, int | str] +) -> None: im = Image.new("RGBA", size=(W, H), color=(255, 0, 0, 0)) filename = f"Tests/images/imagedraw_{polygon_name}.png" draw = ImageDraw.Draw(im) @@ -1471,7 +1664,9 @@ def test_draw_regular_polygon(n_sides, polygon_name, args) -> None: ), ], ) -def test_compute_regular_polygon_vertices(n_sides, expected_vertices) -> None: +def test_compute_regular_polygon_vertices( + n_sides: int, expected_vertices: list[tuple[float, float]] +) -> None: bounding_circle = (W // 2, H // 2, 25) vertices = ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, 0) assert vertices == expected_vertices @@ -1569,7 +1764,7 @@ def test_polygon2() -> None: @pytest.mark.parametrize("xy", ((1, 1, 0, 1), (1, 1, 1, 0))) -def test_incorrectly_ordered_coordinates(xy) -> None: +def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None: im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) with pytest.raises(ValueError): diff --git a/Tests/test_imagetk.py b/Tests/test_imagetk.py index a216bd21d..b607b8c43 100644 --- a/Tests/test_imagetk.py +++ b/Tests/test_imagetk.py @@ -57,7 +57,7 @@ def test_kw() -> None: @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage(mode) -> None: +def test_photoimage(mode: str) -> None: # test as image: im = hopper(mode) @@ -79,7 +79,7 @@ def test_photoimage_apply_transparency() -> None: @pytest.mark.parametrize("mode", TK_MODES) -def test_photoimage_blank(mode) -> None: +def test_photoimage_blank(mode: str) -> None: # test a image using mode/size: im_tk = ImageTk.PhotoImage(mode, (100, 100)) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index c8d6d33d2..629a6dc7a 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -10,7 +10,13 @@ X = 255 class TestLibPack: - def assert_pack(self, mode, rawmode, data, *pixels) -> None: + def assert_pack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ @@ -228,7 +234,13 @@ class TestLibPack: class TestLibUnpack: - def assert_unpack(self, mode, rawmode, data, *pixels) -> None: + def assert_unpack( + self, + mode: str, + rawmode: str, + data: int | bytes, + *pixels: int | float | tuple[int, ...], + ) -> None: """ data - either raw bytes with data or just number of bytes in rawmode. """ diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index f2540bb46..903f7e0c6 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -11,7 +11,7 @@ from .helper import hopper original = hopper().resize((32, 32)).convert("I") -def verify(im1) -> None: +def verify(im1: Image.Image) -> None: im2 = original.copy() assert im1.size == im2.size pix1 = im1.load() @@ -27,7 +27,7 @@ def verify(im1) -> None: @pytest.mark.parametrize("mode", ("L", "I;16", "I;16B", "I;16L", "I")) -def test_basic(tmp_path: Path, mode) -> None: +def test_basic(tmp_path: Path, mode: str) -> None: # PIL 1.1 has limited support for 16-bit image data. Check that # create/copy/transform and save works as expected. @@ -78,7 +78,7 @@ def test_basic(tmp_path: Path, mode) -> None: def test_tobytes() -> None: - def tobytes(mode): + def tobytes(mode: str) -> Image.Image: return Image.new(mode, (1, 1), 1).tobytes() order = 1 if Image._ENDIAN == "<" else -1 From f2228e0a7c19d74c83b99f92edc113ba0cac7625 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Wed, 31 Jan 2024 12:37:53 -0700 Subject: [PATCH 09/29] Replace bytes | str | Path with StrOrBytesPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondrej Baranovič --- src/PIL/_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index b649500ab..f7a69fae1 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -11,7 +11,7 @@ def is_path(f: Any) -> TypeGuard[StrOrBytesPath]: return isinstance(f, (bytes, str, os.PathLike)) -def is_directory(f: Any) -> TypeGuard[bytes | str | Path]: +def is_directory(f: Any) -> TypeGuard[StrOrBytesPath]: """Checks if an object is a string, and that it points to a directory.""" return is_path(f) and os.path.isdir(f) From 256f3f1966d6b56f178d0a9bb2bc4b0b334c77b1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jan 2024 19:38:49 +0000 Subject: [PATCH 10/29] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/PIL/_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PIL/_util.py b/src/PIL/_util.py index f7a69fae1..6bc762816 100644 --- a/src/PIL/_util.py +++ b/src/PIL/_util.py @@ -1,7 +1,6 @@ from __future__ import annotations import os -from pathlib import Path from typing import Any, NoReturn from ._typing import StrOrBytesPath, TypeGuard From 2515938cdd321a5940a070f808c01ed48ad4e10e Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 2 Feb 2024 19:04:22 +1100 Subject: [PATCH 11/29] Simplified type hints --- Tests/test_imagedraw.py | 222 ++++++---------------------------------- 1 file changed, 32 insertions(+), 190 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index c02ac49dd..6e7dce420 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,6 +2,7 @@ from __future__ import annotations import contextlib import os.path +from typing import Sequence import pytest @@ -74,14 +75,7 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], - start: float, - end: float, -) -> None: +def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -94,12 +88,7 @@ def test_arc( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -114,12 +103,7 @@ def test_arc_end_le_start( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -135,12 +119,7 @@ def test_arc_no_loops( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -153,12 +132,7 @@ def test_arc_width( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -172,12 +146,7 @@ def test_arc_width_pieslice_large( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -190,12 +159,7 @@ def test_arc_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -237,13 +201,7 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord( - mode: str, - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], -) -> None: +def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -257,12 +215,7 @@ def test_chord( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -275,12 +228,7 @@ def test_chord_width( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -293,12 +241,7 @@ def test_chord_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -324,13 +267,7 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse( - mode: str, - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], -) -> None: +def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -344,12 +281,7 @@ def test_ellipse( @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -386,12 +318,7 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -416,12 +343,7 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -434,12 +356,7 @@ def test_ellipse_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -572,12 +489,7 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) def test_pieslice( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int], - start: float, - end: float, + bbox: Sequence[int | Sequence[int]], start: float, end: float ) -> None: # Arrange im = Image.new("RGB", (W, H)) @@ -591,12 +503,7 @@ def test_pieslice( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -609,12 +516,7 @@ def test_pieslice_width( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -628,12 +530,7 @@ def test_pieslice_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -796,12 +693,7 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -828,12 +720,7 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -847,12 +734,7 @@ def test_rectangle_width( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -866,12 +748,7 @@ def test_rectangle_width_fill( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -884,12 +761,7 @@ def test_rectangle_zero_width( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -902,12 +774,7 @@ def test_rectangle_I16( @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -999,12 +866,7 @@ def test_rounded_rectangle_non_integer_radius( @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -1045,12 +907,7 @@ def test_rounded_rectangle_translucent( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -1083,12 +940,7 @@ def test_floodfill( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: # floodfill() is experimental # Arrange @@ -1110,12 +962,7 @@ def test_floodfill_border( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None: # floodfill() is experimental # Arrange @@ -1572,12 +1419,7 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline( - bbox: tuple[tuple[int, int], tuple[int, int]] - | list[tuple[int, int]] - | list[int] - | tuple[int, int, int, int] -) -> None: +def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 From 5a8e7dda79e5ee4d0f8436179f61881a5d8bd286 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 5 Feb 2024 20:36:34 +1100 Subject: [PATCH 12/29] Added type hints --- Tests/test_imagedraw.py | 2 +- src/PIL/Image.py | 2 +- src/PIL/ImageDraw.py | 73 +++++++++++++++++++++-------------------- src/PIL/ImageFont.py | 2 +- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 6e7dce420..4503a9292 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1519,7 +1519,7 @@ def test_compute_regular_polygon_vertices( [ (None, (50, 50, 25), 0, TypeError, "n_sides should be an int"), (1, (50, 50, 25), 0, ValueError, "n_sides should be an int > 2"), - (3, 50, 0, TypeError, "bounding_circle should be a tuple"), + (3, 50, 0, TypeError, "bounding_circle should be a sequence"), ( 3, (50, 50, 100, 100), diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 553f36703..111d06012 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -571,7 +571,7 @@ class Image: # object is gone. self.im = DeferredError(ValueError("Operation on closed image")) - def _copy(self): + def _copy(self) -> None: self.load() self.im = self.im.copy() self.pyaccess = None diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 84665f54f..650e30857 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -48,7 +48,7 @@ directly. class ImageDraw: font = None - def __init__(self, im, mode=None): + def __init__(self, im: Image.Image, mode: str | None = None) -> None: """ Create a drawing instance. @@ -115,7 +115,7 @@ class ImageDraw: self.font = ImageFont.load_default() return self.font - def _getfont(self, font_size): + def _getfont(self, font_size: float | None): if font_size is not None: from . import ImageFont @@ -124,7 +124,7 @@ class ImageDraw: font = self.getfont() return font - def _getink(self, ink, fill=None): + def _getink(self, ink, fill=None) -> tuple[int | None, int | None]: if ink is None and fill is None: if self.fill: fill = self.ink @@ -145,13 +145,13 @@ class ImageDraw: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1): + def arc(self, xy, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None): + def bitmap(self, xy, bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +160,7 @@ class ImageDraw: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1): + def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +168,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1): + def ellipse(self, xy, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,7 +176,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None): + def line(self, xy, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: @@ -236,7 +236,7 @@ class ImageDraw: ] self.line(gap_coords, fill, width=3) - def shape(self, shape, fill=None, outline=None): + def shape(self, shape, fill=None, outline=None) -> None: """(Experimental) Draw a shape.""" shape.close() ink, fill = self._getink(outline, fill) @@ -245,7 +245,7 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1): + def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +253,13 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None): + def point(self, xy, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1): + def polygon(self, xy, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -267,7 +267,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: if width == 1: self.draw.draw_polygon(xy, ink, 0, width) - else: + elif self.im is not None: # To avoid expanding the polygon outwards, # use the fill as a mask mask = Image.new("1", self.im.size) @@ -291,12 +291,12 @@ class ImageDraw: def regular_polygon( self, bounding_circle, n_sides, rotation=0, fill=None, outline=None, width=1 - ): + ) -> None: """Draw a regular polygon.""" xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1): + def rectangle(self, xy, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -306,7 +306,7 @@ class ImageDraw: def rounded_rectangle( self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None - ): + ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): (x0, y0), (x1, y1) = xy @@ -346,7 +346,7 @@ class ImageDraw: r = d // 2 ink, fill = self._getink(outline, fill) - def draw_corners(pieslice): + def draw_corners(pieslice) -> None: if full_x: # Draw top and bottom halves parts = ( @@ -431,12 +431,12 @@ class ImageDraw: right[3] -= r + 1 self.draw.draw_rectangle(right, ink, 1) - def _multiline_check(self, text): + def _multiline_check(self, text) -> bool: split_character = "\n" if isinstance(text, str) else b"\n" return split_character in text - def _multiline_split(self, text): + def _multiline_split(self, text) -> list[str | bytes]: split_character = "\n" if isinstance(text, str) else b"\n" return text.split(split_character) @@ -465,7 +465,7 @@ class ImageDraw: embedded_color=False, *args, **kwargs, - ): + ) -> None: """Draw text.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -497,7 +497,7 @@ class ImageDraw: return fill return ink - def draw_text(ink, stroke_width=0, stroke_offset=None): + def draw_text(ink, stroke_width=0, stroke_offset=None) -> None: mode = self.fontmode if stroke_width == 0 and embedded_color: mode = "RGBA" @@ -547,7 +547,8 @@ class ImageDraw: ink_alpha = struct.pack("i", ink)[3] color.fillband(3, ink_alpha) x, y = coord - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + if self.im is not None: + self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) else: self.draw.draw_bitmap(coord, mask, ink) @@ -584,7 +585,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> None: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -693,7 +694,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: """Get the bounding box of a given string, in pixels.""" if embedded_color and self.mode not in ("RGB", "RGBA"): msg = "Embedded color supported only in RGB and RGBA modes" @@ -738,7 +739,7 @@ class ImageDraw: embedded_color=False, *, font_size=None, - ): + ) -> tuple[int, int, int, int]: if direction == "ttb": msg = "ttb direction is unsupported for multiline text" raise ValueError(msg) @@ -777,7 +778,7 @@ class ImageDraw: elif anchor[1] == "d": top -= (len(lines) - 1) * line_spacing - bbox = None + bbox: tuple[int, int, int, int] | None = None for idx, line in enumerate(lines): left = xy[0] @@ -828,7 +829,7 @@ class ImageDraw: return bbox -def Draw(im, mode=None): +def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -876,7 +877,7 @@ def getdraw(im=None, hints=None): return im, handler -def floodfill(image, xy, value, border=None, thresh=0): +def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: """ (experimental) Fills a bounded region with a given color. @@ -932,7 +933,7 @@ def floodfill(image, xy, value, border=None, thresh=0): edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): +def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -982,7 +983,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 1.2 Check `bounding_circle` has an appropriate value if not isinstance(bounding_circle, (list, tuple)): - msg = "bounding_circle should be a tuple" + msg = "bounding_circle should be a sequence" raise TypeError(msg) if len(bounding_circle) == 3: @@ -1014,7 +1015,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): raise ValueError(msg) # 2. Define Helper Functions - def _apply_rotation(point, degrees, centroid): + def _apply_rotation(point: list[float], degrees: float) -> tuple[int, int]: return ( round( point[0] * math.cos(math.radians(360 - degrees)) @@ -1030,11 +1031,11 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): ), ) - def _compute_polygon_vertex(centroid, polygon_radius, angle): + def _compute_polygon_vertex(angle: float) -> tuple[int, int]: start_point = [polygon_radius, 0] - return _apply_rotation(start_point, angle, centroid) + return _apply_rotation(start_point, angle) - def _get_angles(n_sides, rotation): + def _get_angles(n_sides: int, rotation: float) -> list[float]: angles = [] degrees = 360 / n_sides # Start with the bottom left polygon vertex @@ -1051,11 +1052,11 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation): # 4. Compute Vertices return [ - _compute_polygon_vertex(centroid, polygon_radius, angle) for angle in angles + _compute_polygon_vertex(angle) for angle in angles ] -def _color_diff(color1, color2): +def _color_diff(color1, color2: float | tuple[int, ...]) -> float: """ Uses 1-norm distance to calculate difference between two values. """ diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index a63b73b33..1ec8a9f4d 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -872,7 +872,7 @@ def load_path(filename): raise OSError(msg) -def load_default(size=None): +def load_default(size: float | None = None) -> FreeTypeFont | ImageFont: """If FreeType support is available, load a version of Aileron Regular, https://dotcolon.net/font/aileron, with a more limited character set. From 65cb0b0487c29c91f0145226e1cd173511bc3586 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Tue, 6 Feb 2024 07:49:43 +1100 Subject: [PATCH 13/29] Added _typing.Coords --- Tests/test_imagedraw.py | 87 +++++++++++++++++------------------------ src/PIL/ImageDraw.py | 87 ++++++++++++++++++++++++----------------- src/PIL/_typing.py | 4 ++ 3 files changed, 91 insertions(+), 87 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4503a9292..4e6cedcd1 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -2,11 +2,11 @@ from __future__ import annotations import contextlib import os.path -from typing import Sequence import pytest from PIL import Image, ImageColor, ImageDraw, ImageFont, features +from PIL._typing import Coords from .helper import ( assert_image_equal, @@ -75,7 +75,7 @@ def test_mode_mismatch() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((0, 180), (0.5, 180.4))) -def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> None: +def test_arc(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -88,7 +88,7 @@ def test_arc(bbox: Sequence[int | Sequence[int]], start: float, end: float) -> N @pytest.mark.parametrize("bbox", BBOX) -def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_end_le_start(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -103,7 +103,7 @@ def test_arc_end_le_start(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_no_loops(bbox: Coords) -> None: # No need to go in loops # Arrange im = Image.new("RGB", (W, H)) @@ -119,7 +119,7 @@ def test_arc_no_loops(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -132,7 +132,7 @@ def test_arc_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_pieslice_large(bbox: Coords) -> None: # Tests an arc with a large enough width that it is a pieslice # Arrange im = Image.new("RGB", (W, H)) @@ -146,7 +146,7 @@ def test_arc_width_pieslice_large(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -159,7 +159,7 @@ def test_arc_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_arc_width_non_whole_angle(bbox: Sequence[int | Sequence[int]]) -> None: +def test_arc_width_non_whole_angle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -201,7 +201,7 @@ def test_bitmap() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -215,7 +215,7 @@ def test_chord(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -228,7 +228,7 @@ def test_chord_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -241,7 +241,7 @@ def test_chord_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_chord_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_chord_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -267,7 +267,7 @@ def test_chord_too_fat() -> None: @pytest.mark.parametrize("mode", ("RGB", "L")) @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse(mode: str, bbox: Coords) -> None: # Arrange im = Image.new(mode, (W, H)) draw = ImageDraw.Draw(im) @@ -281,7 +281,7 @@ def test_ellipse(mode: str, bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_translucent(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_translucent(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -318,7 +318,7 @@ def test_ellipse_symmetric() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -343,7 +343,7 @@ def test_ellipse_width_large() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -356,7 +356,7 @@ def test_ellipse_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_ellipse_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_ellipse_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -410,12 +410,7 @@ def test_ellipse_various_sizes_filled() -> None: @pytest.mark.parametrize("points", POINTS) -def test_line( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_line(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -488,9 +483,7 @@ def test_transform() -> None: @pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("start, end", ((-92, 46), (-92.2, 46.2))) -def test_pieslice( - bbox: Sequence[int | Sequence[int]], start: float, end: float -) -> None: +def test_pieslice(bbox: Coords, start: float, end: float) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -503,7 +496,7 @@ def test_pieslice( @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -516,7 +509,7 @@ def test_pieslice_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -530,7 +523,7 @@ def test_pieslice_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_pieslice_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_pieslice_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -585,12 +578,7 @@ def test_pieslice_no_spikes() -> None: @pytest.mark.parametrize("points", POINTS) -def test_point( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_point(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -615,12 +603,7 @@ def test_point_I16() -> None: @pytest.mark.parametrize("points", POINTS) -def test_polygon( - points: tuple[tuple[int, int], ...] - | list[tuple[int, int]] - | tuple[int, ...] - | list[int] -) -> None: +def test_polygon(points: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -693,7 +676,7 @@ def test_polygon_translucent() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -720,7 +703,7 @@ def test_big_rectangle() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -734,7 +717,7 @@ def test_rectangle_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_width_fill(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -748,7 +731,7 @@ def test_rectangle_width_fill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_zero_width(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -761,7 +744,7 @@ def test_rectangle_zero_width(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_I16(bbox: Coords) -> None: # Arrange im = Image.new("I;16", (W, H)) draw = ImageDraw.Draw(im) @@ -774,7 +757,7 @@ def test_rectangle_I16(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_rectangle_translucent_outline(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rectangle_translucent_outline(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im, "RGBA") @@ -866,7 +849,7 @@ def test_rounded_rectangle_non_integer_radius( @pytest.mark.parametrize("bbox", BBOX) -def test_rounded_rectangle_zero_radius(bbox: Sequence[int | Sequence[int]]) -> None: +def test_rounded_rectangle_zero_radius(bbox: Coords) -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) @@ -907,7 +890,7 @@ def test_rounded_rectangle_translucent( @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill(bbox: Coords) -> None: red = ImageColor.getrgb("red") for mode, value in [("L", 1), ("RGBA", (255, 0, 0, 0)), ("RGB", red)]: @@ -940,7 +923,7 @@ def test_floodfill(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill_border(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -962,7 +945,7 @@ def test_floodfill_border(bbox: Sequence[int | Sequence[int]]) -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_floodfill_thresh(bbox: Sequence[int | Sequence[int]]) -> None: +def test_floodfill_thresh(bbox: Coords) -> None: # floodfill() is experimental # Arrange @@ -1419,7 +1402,7 @@ def test_default_font_size() -> None: @pytest.mark.parametrize("bbox", BBOX) -def test_same_color_outline(bbox: Sequence[int | Sequence[int]]) -> None: +def test_same_color_outline(bbox: Coords) -> None: # Prepare shape x0, y0 = 5, 5 x1, y1 = 5, 50 diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index 650e30857..d4e000087 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -34,8 +34,10 @@ from __future__ import annotations import math import numbers import struct +from typing import Sequence, cast from . import Image, ImageColor +from ._typing import Coords """ A simple 2D drawing interface for PIL images. @@ -145,13 +147,13 @@ class ImageDraw: fill = self.draw.draw_ink(fill) return ink, fill - def arc(self, xy, start, end, fill=None, width=1) -> None: + def arc(self, xy: Coords, start, end, fill=None, width=1) -> None: """Draw an arc.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_arc(xy, start, end, ink, width) - def bitmap(self, xy, bitmap, fill=None) -> None: + def bitmap(self, xy: Sequence[int], bitmap, fill=None) -> None: """Draw a bitmap.""" bitmap.load() ink, fill = self._getink(fill) @@ -160,7 +162,7 @@ class ImageDraw: if ink is not None: self.draw.draw_bitmap(xy, bitmap.im, ink) - def chord(self, xy, start, end, fill=None, outline=None, width=1) -> None: + def chord(self, xy: Coords, start, end, fill=None, outline=None, width=1) -> None: """Draw a chord.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -168,7 +170,7 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_chord(xy, start, end, ink, 0, width) - def ellipse(self, xy, fill=None, outline=None, width=1) -> None: + def ellipse(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw an ellipse.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -176,20 +178,29 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_ellipse(xy, ink, 0, width) - def line(self, xy, fill=None, width=0, joint=None) -> None: + def line(self, xy: Coords, fill=None, width=0, joint=None) -> None: """Draw a line, or a connected sequence of line segments.""" ink = self._getink(fill)[0] if ink is not None: self.draw.draw_lines(xy, ink, width) if joint == "curve" and width > 4: - if not isinstance(xy[0], (list, tuple)): - xy = [tuple(xy[i : i + 2]) for i in range(0, len(xy), 2)] - for i in range(1, len(xy) - 1): - point = xy[i] + points: Sequence[Sequence[float]] + if isinstance(xy[0], (list, tuple)): + points = cast(Sequence[Sequence[float]], xy) + else: + points = [ + cast(Sequence[float], tuple(xy[i : i + 2])) + for i in range(0, len(xy), 2) + ] + for i in range(1, len(points) - 1): + point = points[i] angles = [ math.degrees(math.atan2(end[0] - start[0], start[1] - end[1])) % 360 - for start, end in ((xy[i - 1], point), (point, xy[i + 1])) + for start, end in ( + (points[i - 1], point), + (point, points[i + 1]), + ) ] if angles[0] == angles[1]: # This is a straight line, so no joint is required @@ -245,7 +256,9 @@ class ImageDraw: if ink is not None and ink != fill: self.draw.draw_outline(shape, ink, 0) - def pieslice(self, xy, start, end, fill=None, outline=None, width=1) -> None: + def pieslice( + self, xy: Coords, start, end, fill=None, outline=None, width=1 + ) -> None: """Draw a pieslice.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -253,13 +266,13 @@ class ImageDraw: if ink is not None and ink != fill and width != 0: self.draw.draw_pieslice(xy, start, end, ink, 0, width) - def point(self, xy, fill=None) -> None: + def point(self, xy: Coords, fill=None) -> None: """Draw one or more individual pixels.""" ink, fill = self._getink(fill) if ink is not None: self.draw.draw_points(xy, ink) - def polygon(self, xy, fill=None, outline=None, width=1) -> None: + def polygon(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a polygon.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -296,7 +309,7 @@ class ImageDraw: xy = _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) self.polygon(xy, fill, outline, width) - def rectangle(self, xy, fill=None, outline=None, width=1) -> None: + def rectangle(self, xy: Coords, fill=None, outline=None, width=1) -> None: """Draw a rectangle.""" ink, fill = self._getink(outline, fill) if fill is not None: @@ -305,13 +318,13 @@ class ImageDraw: self.draw.draw_rectangle(xy, ink, 0, width) def rounded_rectangle( - self, xy, radius=0, fill=None, outline=None, width=1, *, corners=None + self, xy: Coords, radius=0, fill=None, outline=None, width=1, *, corners=None ) -> None: """Draw a rounded rectangle.""" if isinstance(xy[0], (list, tuple)): - (x0, y0), (x1, y1) = xy + (x0, y0), (x1, y1) = cast(Sequence[Sequence[float]], xy) else: - x0, y0, x1, y1 = xy + x0, y0, x1, y1 = cast(Sequence[float], xy) if x1 < x0: msg = "x1 must be greater than or equal to x0" raise ValueError(msg) @@ -347,6 +360,7 @@ class ImageDraw: ink, fill = self._getink(outline, fill) def draw_corners(pieslice) -> None: + parts: tuple[tuple[tuple[float, float, float, float], int, int], ...] if full_x: # Draw top and bottom halves parts = ( @@ -361,17 +375,18 @@ class ImageDraw: ) else: # Draw four separate corners - parts = [] - for i, part in enumerate( - ( - ((x0, y0, x0 + d, y0 + d), 180, 270), - ((x1 - d, y0, x1, y0 + d), 270, 360), - ((x1 - d, y1 - d, x1, y1), 0, 90), - ((x0, y1 - d, x0 + d, y1), 90, 180), + parts = tuple( + part + for i, part in enumerate( + ( + ((x0, y0, x0 + d, y0 + d), 180, 270), + ((x1 - d, y0, x1, y0 + d), 270, 360), + ((x1 - d, y1 - d, x1, y1), 0, 90), + ((x0, y1 - d, x0 + d, y1), 90, 180), + ) ) - ): - if corners[i]: - parts.append(part) + if corners[i] + ) for part in parts: if pieslice: self.draw.draw_pieslice(*(part + (fill, 1))) @@ -520,7 +535,7 @@ class ImageDraw: *args, **kwargs, ) - coord = coord[0] + offset[0], coord[1] + offset[1] + coord = [coord[0] + offset[0], coord[1] + offset[1]] except AttributeError: try: mask = font.getmask( @@ -539,7 +554,7 @@ class ImageDraw: except TypeError: mask = font.getmask(text) if stroke_offset: - coord = coord[0] + stroke_offset[0], coord[1] + stroke_offset[1] + coord = [coord[0] + stroke_offset[0], coord[1] + stroke_offset[1]] if mode == "RGBA": # font.getmask2(mode="RGBA") returns color in RGB bands and mask in A # extract mask and set text alpha @@ -548,7 +563,9 @@ class ImageDraw: color.fillband(3, ink_alpha) x, y = coord if self.im is not None: - self.im.paste(color, (x, y, x + mask.size[0], y + mask.size[1]), mask) + self.im.paste( + color, (x, y, x + mask.size[0], y + mask.size[1]), mask + ) else: self.draw.draw_bitmap(coord, mask, ink) @@ -829,7 +846,7 @@ class ImageDraw: return bbox -def Draw(im: Image.Image, mode: str | None = None) -> ImageDraw: +def Draw(im, mode: str | None = None) -> ImageDraw: """ A simple 2D drawing interface for PIL images. @@ -933,7 +950,9 @@ def floodfill(image: Image.Image, xy, value, border=None, thresh=0) -> None: edge = new_edge -def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> list[tuple[float, float]]: +def _compute_regular_polygon_vertices( + bounding_circle, n_sides, rotation +) -> list[tuple[float, float]]: """ Generate a list of vertices for a 2D regular polygon. @@ -1051,9 +1070,7 @@ def _compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) -> lis angles = _get_angles(n_sides, rotation) # 4. Compute Vertices - return [ - _compute_polygon_vertex(angle) for angle in angles - ] + return [_compute_polygon_vertex(angle) for angle in angles] def _color_diff(color1, color2: float | tuple[int, ...]) -> float: diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 608b2b41f..ddea0b414 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +from typing import Sequence, Union if sys.version_info >= (3, 10): from typing import TypeGuard @@ -15,4 +16,7 @@ else: return bool +Coords = Union[Sequence[float], Sequence[Sequence[float]]] + + __all__ = ["TypeGuard"] From 91645f9efffc623cb83221a8d9c9a0b98d3ce548 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Feb 2024 10:19:00 +1100 Subject: [PATCH 14/29] Lint fix --- Tests/test_imagedraw.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 4e6cedcd1..f7aea3034 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -776,9 +776,11 @@ def test_rectangle_translucent_outline(bbox: Coords) -> None: [(10, 20, 190, 180), ([10, 20], [190, 180]), ((10, 20), (190, 180))], ) def test_rounded_rectangle( - xy: tuple[int, int, int, int] - | tuple[list[int]] - | tuple[tuple[int, int], tuple[int, int]] + xy: ( + tuple[int, int, int, int] + | tuple[list[int]] + | tuple[tuple[int, int], tuple[int, int]] + ) ) -> None: # Arrange im = Image.new("RGB", (200, 200)) From cdc498e6f3b2060906ca14fe9b9187e0a93a1613 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 7 Feb 2024 19:16:28 +1100 Subject: [PATCH 15/29] Added type hints --- Tests/test_color_lut.py | 4 ++- Tests/test_file_eps.py | 32 ++++++++++---------- Tests/test_file_jpeg.py | 35 +++++++++++----------- Tests/test_file_jpeg2k.py | 21 ++++++------- Tests/test_file_libtiff.py | 38 +++++++++++++----------- Tests/test_file_png.py | 15 +++++----- Tests/test_image_paste.py | 41 +++++++++++++++----------- Tests/test_image_reduce.py | 43 +++++++++++++++------------ Tests/test_image_resample.py | 57 ++++++++++++++++++++---------------- Tests/test_imagemath.py | 8 ++--- 10 files changed, 160 insertions(+), 134 deletions(-) diff --git a/Tests/test_color_lut.py b/Tests/test_color_lut.py index e6c8d7819..2bb1b57d4 100644 --- a/Tests/test_color_lut.py +++ b/Tests/test_color_lut.py @@ -15,7 +15,9 @@ except ImportError: class TestColorLut3DCoreAPI: - def generate_identity_table(self, channels, size): + def generate_identity_table( + self, channels: int, size: int | tuple[int, int, int] + ) -> tuple[int, int, int, int, list[float]]: if isinstance(size, tuple): size_1d, size_2d, size_3d = size else: diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index 06f927c7b..00f5f39e8 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -84,7 +84,7 @@ simple_eps_file_with_long_binary_data = ( ("filename", "size"), ((FILE1, (460, 352)), (FILE2, (360, 252))) ) @pytest.mark.parametrize("scale", (1, 2)) -def test_sanity(filename, size, scale) -> None: +def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: expected_size = tuple(s * scale for s in size) with Image.open(filename) as image: image.load(scale=scale) @@ -129,28 +129,28 @@ def test_binary_header_only() -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_version_comment(prefix) -> None: +def test_missing_version_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_version)) with pytest.raises(SyntaxError): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_missing_boundingbox_comment(prefix) -> None: +def test_missing_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_without_boundingbox)) with pytest.raises(SyntaxError, match='EPS header missing "%%BoundingBox" comment'): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment(prefix) -> None: +def test_invalid_boundingbox_comment(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox)) with pytest.raises(OSError, match="cannot determine EPS bounding box"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: +def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix: bytes) -> None: data = io.BytesIO( prefix + b"\n".join(simple_eps_file_with_invalid_boundingbox_valid_imagedata) ) @@ -161,21 +161,21 @@ def test_invalid_boundingbox_comment_valid_imagedata_comment(prefix) -> None: @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_ascii_comment_too_long(prefix) -> None: +def test_ascii_comment_too_long(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_ascii_comment)) with pytest.raises(SyntaxError, match="not an EPS file"): EpsImagePlugin.EpsImageFile(data) @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_long_binary_data(prefix) -> None: +def test_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) EpsImagePlugin.EpsImageFile(data) @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("prefix", (b"", simple_binary_header)) -def test_load_long_binary_data(prefix) -> None: +def test_load_long_binary_data(prefix: bytes) -> None: data = io.BytesIO(prefix + b"\n".join(simple_eps_file_with_long_binary_data)) with Image.open(data) as img: img.load() @@ -305,7 +305,7 @@ def test_render_scale2() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2, "Tests/images/illu10_preview.eps")) -def test_resize(filename) -> None: +def test_resize(filename: str) -> None: with Image.open(filename) as im: new_size = (100, 100) im = im.resize(new_size) @@ -314,7 +314,7 @@ def test_resize(filename) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") @pytest.mark.parametrize("filename", (FILE1, FILE2)) -def test_thumbnail(filename) -> None: +def test_thumbnail(filename: str) -> None: # Issue #619 with Image.open(filename) as im: new_size = (100, 100) @@ -335,7 +335,7 @@ def test_readline_psfile(tmp_path: Path) -> None: line_endings = ["\r\n", "\n", "\n\r", "\r"] strings = ["something", "else", "baz", "bif"] - def _test_readline(t, ending) -> None: + def _test_readline(t: EpsImagePlugin.PSFile, ending: str) -> None: ending = "Failure with line ending: %s" % ( "".join("%s" % ord(s) for s in ending) ) @@ -344,13 +344,13 @@ def test_readline_psfile(tmp_path: Path) -> None: assert t.readline().strip("\r\n") == "baz", ending assert t.readline().strip("\r\n") == "bif", ending - def _test_readline_io_psfile(test_string, ending) -> None: + def _test_readline_io_psfile(test_string: str, ending: str) -> None: f = io.BytesIO(test_string.encode("latin-1")) with pytest.warns(DeprecationWarning): t = EpsImagePlugin.PSFile(f) _test_readline(t, ending) - def _test_readline_file_psfile(test_string, ending) -> None: + def _test_readline_file_psfile(test_string: str, ending: str) -> None: f = str(tmp_path / "temp.txt") with open(f, "wb") as w: w.write(test_string.encode("latin-1")) @@ -376,7 +376,7 @@ def test_psfile_deprecation() -> None: "line_ending", (b"\r\n", b"\n", b"\n\r", b"\r"), ) -def test_readline(prefix, line_ending) -> None: +def test_readline(prefix: bytes, line_ending: bytes) -> None: simple_file = prefix + line_ending.join(simple_eps_file_with_comments) data = io.BytesIO(simple_file) test_file = EpsImagePlugin.EpsImageFile(data) @@ -394,7 +394,7 @@ def test_readline(prefix, line_ending) -> None: "Tests/images/illuCS6_preview.eps", ), ) -def test_open_eps(filename) -> None: +def test_open_eps(filename: str) -> None: # https://github.com/python-pillow/Pillow/issues/1104 with Image.open(filename) as img: assert img.mode == "RGB" @@ -417,7 +417,7 @@ def test_emptyline() -> None: "test_file", ["Tests/images/timeout-d675703545fee17acab56e5fec644c19979175de.eps"], ) -def test_timeout(test_file) -> None: +def test_timeout(test_file: str) -> None: with open(test_file, "rb") as f: with pytest.raises(Image.UnidentifiedImageError): with Image.open(f): diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index ff278d4c1..6b0662e0b 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -5,6 +5,7 @@ import re import warnings from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -42,7 +43,7 @@ TEST_FILE = "Tests/images/hopper.jpg" @skip_unless_feature("jpg") class TestFileJpeg: - def roundtrip(self, im, **options): + def roundtrip(self, im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG", **options) test_bytes = out.tell() @@ -51,7 +52,7 @@ class TestFileJpeg: im.bytes = test_bytes # for testing only return im - def gen_random_image(self, size, mode: str = "RGB"): + def gen_random_image(self, size: tuple[int, int], mode: str = "RGB") -> Image.Image: """Generates a very hard to compress file :param size: tuple :param mode: optional image mode @@ -71,7 +72,7 @@ class TestFileJpeg: assert im.get_format_mimetype() == "image/jpeg" @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0))) - def test_zero(self, size, tmp_path: Path) -> None: + def test_zero(self, size: tuple[int, int], tmp_path: Path) -> None: f = str(tmp_path / "temp.jpg") im = Image.new("RGB", size) with pytest.raises(ValueError): @@ -108,13 +109,11 @@ class TestFileJpeg: assert "comment" not in reloaded.info # Test that a comment argument overrides the default comment - for comment in ("Test comment text", b"Text comment text"): + for comment in ("Test comment text", b"Test comment text"): out = BytesIO() im.save(out, format="JPEG", comment=comment) with Image.open(out) as reloaded: - if not isinstance(comment, bytes): - comment = comment.encode() - assert reloaded.info["comment"] == comment + assert reloaded.info["comment"] == b"Test comment text" def test_cmyk(self) -> None: # Test CMYK handling. Thanks to Tim and Charlie for test data, @@ -145,7 +144,7 @@ class TestFileJpeg: assert k > 0.9 def test_rgb(self) -> None: - def getchannels(im): + def getchannels(im: Image.Image) -> tuple[int, int, int]: return tuple(v[0] for v in im.layer) im = hopper() @@ -161,8 +160,8 @@ class TestFileJpeg: "test_image_path", [TEST_FILE, "Tests/images/pil_sample_cmyk.jpg"], ) - def test_dpi(self, test_image_path) -> None: - def test(xdpi, ydpi=None): + def test_dpi(self, test_image_path: str) -> None: + def test(xdpi: int, ydpi: int | None = None): with Image.open(test_image_path) as im: im = self.roundtrip(im, dpi=(xdpi, ydpi or xdpi)) return im.info.get("dpi") @@ -207,7 +206,7 @@ class TestFileJpeg: ImageFile.MAXBLOCK * 4 + 3, # large block ), ) - def test_icc_big(self, n) -> None: + def test_icc_big(self, n: int) -> None: # Make sure that the "extra" support handles large blocks # The ICC APP marker can store 65519 bytes per marker, so # using a 4-byte test code should allow us to detect out of @@ -433,7 +432,7 @@ class TestFileJpeg: assert_image(im1, im2.mode, im2.size) def test_subsampling(self) -> None: - def getsampling(im): + def getsampling(im: Image.Image): layer = im.layer return layer[0][1:3] + layer[1][1:3] + layer[2][1:3] @@ -530,7 +529,7 @@ class TestFileJpeg: pytest.mark.valgrind_known_error, "libjpeg_turbo", "2.0", reason="Known Failing" ) def test_qtables(self, tmp_path: Path) -> None: - def _n_qtables_helper(n, test_file) -> None: + def _n_qtables_helper(n: int, test_file: str) -> None: with Image.open(test_file) as im: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) @@ -666,7 +665,7 @@ class TestFileJpeg: "blocks, rows, markers", ((0, 0, 0), (1, 0, 15), (3, 0, 5), (8, 0, 1), (0, 1, 3), (0, 2, 1)), ) - def test_restart_markers(self, blocks, rows, markers) -> None: + def test_restart_markers(self, blocks: int, rows: int, markers: int) -> None: im = Image.new("RGB", (32, 32)) # 16 MCUs out = BytesIO() im.save( @@ -724,13 +723,13 @@ class TestFileJpeg: assert im.format == "JPEG" @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr")) - def test_save_correct_modes(self, mode) -> None: + def test_save_correct_modes(self, mode: str) -> None: out = BytesIO() img = Image.new(mode, (20, 20)) img.save(out, "JPEG") @pytest.mark.parametrize("mode", ("LA", "La", "RGBA", "RGBa", "P")) - def test_save_wrong_modes(self, mode) -> None: + def test_save_wrong_modes(self, mode: str) -> None: # ref https://github.com/python-pillow/Pillow/issues/2005 out = BytesIO() img = Image.new(mode, (20, 20)) @@ -982,12 +981,12 @@ class TestFileJpeg: # Even though this decoder never says that it is finished # the image should still end when there is no new data class InfiniteMockPyDecoder(ImageFile.PyDecoder): - def decode(self, buffer): + def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 decoder = InfiniteMockPyDecoder(None) - def closure(mode, *args): + def closure(mode: str, *args) -> InfiniteMockPyDecoder: decoder.__init__(mode, *args) return decoder diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index e3f1fa8fd..fab19e2ea 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -4,6 +4,7 @@ import os import re from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -36,7 +37,7 @@ test_card.load() # 'Not enough memory to handle tile data' -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "JPEG2000", **options) test_bytes = out.tell() @@ -138,7 +139,7 @@ def test_prog_res_rt() -> None: @pytest.mark.parametrize("num_resolutions", range(2, 6)) -def test_default_num_resolutions(num_resolutions) -> None: +def test_default_num_resolutions(num_resolutions: int) -> None: d = 1 << (num_resolutions - 1) im = test_card.resize((d - 1, d - 1)) with pytest.raises(OSError): @@ -198,9 +199,9 @@ def test_layers_type(tmp_path: Path) -> None: for quality_layers in [[100, 50, 10], (100, 50, 10), None]: test_card.save(outfile, quality_layers=quality_layers) - for quality_layers in ["quality_layers", ("100", "50", "10")]: + for quality_layers_str in ["quality_layers", ("100", "50", "10")]: with pytest.raises(ValueError): - test_card.save(outfile, quality_layers=quality_layers) + test_card.save(outfile, quality_layers=quality_layers_str) def test_layers() -> None: @@ -233,7 +234,7 @@ def test_layers() -> None: ("foo.jp2", {"no_jp2": False}, 4, b"jP"), ), ) -def test_no_jp2(name, args, offset, data) -> None: +def test_no_jp2(name: str, args: dict[str, bool], offset: int, data: bytes) -> None: out = BytesIO() if name: out.name = name @@ -278,7 +279,7 @@ def test_sgnd(tmp_path: Path) -> None: @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_rgba(ext) -> None: +def test_rgba(ext: str) -> None: # Arrange with Image.open("Tests/images/rgb_trns_ycbc" + ext) as im: # Act @@ -289,7 +290,7 @@ def test_rgba(ext) -> None: @pytest.mark.parametrize("ext", (".j2k", ".jp2")) -def test_16bit_monochrome_has_correct_mode(ext) -> None: +def test_16bit_monochrome_has_correct_mode(ext: str) -> None: with Image.open("Tests/images/16bit.cropped" + ext) as im: im.load() assert im.mode == "I;16" @@ -346,12 +347,12 @@ def test_parser_feed() -> None: not os.path.exists(EXTRA_DIR), reason="Extra image files not installed" ) @pytest.mark.parametrize("name", ("subsampling_1", "subsampling_2", "zoo1", "zoo2")) -def test_subsampling_decode(name) -> None: +def test_subsampling_decode(name: str) -> None: test = f"{EXTRA_DIR}/{name}.jp2" reference = f"{EXTRA_DIR}/{name}.ppm" with Image.open(test) as im: - epsilon = 3 # for YCbCr images + epsilon = 3.0 # for YCbCr images with Image.open(reference) as im2: width, height = im2.size if name[-1] == "2": @@ -400,7 +401,7 @@ def test_save_comment() -> None: "Tests/images/crash-d2c93af851d3ab9a19e34503626368b2ecde9c03.j2k", ], ) -def test_crashes(test_file) -> None: +def test_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: # Valgrind should not complain here diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 1386034e5..0994d9904 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -27,7 +27,7 @@ from .helper import ( @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im) -> None: + def _assert_noerr(self, tmp_path: Path, im: Image.Image) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -140,7 +140,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_adobe_deflate.png") @pytest.mark.parametrize("legacy_api", (False, True)) - def test_write_metadata(self, legacy_api, tmp_path: Path) -> None: + def test_write_metadata(self, legacy_api: bool, tmp_path: Path) -> None: """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: @@ -243,7 +243,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.WRITE_LIBTIFF = False def test_custom_metadata(self, tmp_path: Path) -> None: - tc = namedtuple("test_case", "value,type,supported_by_default") + tc = namedtuple("tc", "value,type,supported_by_default") custom = { 37000 + k: v for k, v in enumerate( @@ -284,7 +284,9 @@ class TestFileLibTiff(LibTiffTestCase): for libtiff in libtiffs: TiffImagePlugin.WRITE_LIBTIFF = libtiff - def check_tags(tiffinfo) -> None: + def check_tags( + tiffinfo: TiffImagePlugin.ImageFileDirectory_v2 | dict[int, str] + ) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -502,7 +504,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, out) @pytest.mark.parametrize("im", (hopper("P"), Image.new("P", (1, 1), "#000"))) - def test_palette_save(self, im, tmp_path: Path) -> None: + def test_palette_save(self, im: Image.Image, tmp_path: Path) -> None: out = str(tmp_path / "temp.tif") TiffImagePlugin.WRITE_LIBTIFF = True @@ -514,7 +516,7 @@ class TestFileLibTiff(LibTiffTestCase): assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) - def test_bw_compression_w_rgb(self, compression, tmp_path: Path) -> None: + def test_bw_compression_w_rgb(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB") out = str(tmp_path / "temp.tif") @@ -647,7 +649,7 @@ class TestFileLibTiff(LibTiffTestCase): # Generate test image pilim = hopper() - def save_bytesio(compression=None) -> None: + def save_bytesio(compression: str | None = None) -> None: buffer_io = io.BytesIO() pilim.save(buffer_io, format="tiff", compression=compression) buffer_io.seek(0) @@ -731,7 +733,7 @@ class TestFileLibTiff(LibTiffTestCase): assert icc == icc_libtiff def test_write_icc(self, tmp_path: Path) -> None: - def check_write(libtiff) -> None: + def check_write(libtiff: bool) -> None: TiffImagePlugin.WRITE_LIBTIFF = libtiff with Image.open("Tests/images/hopper.iccprofile.tif") as img: @@ -837,7 +839,7 @@ class TestFileLibTiff(LibTiffTestCase): assert reloaded.mode == "F" assert reloaded.getexif()[SAMPLEFORMAT] == 3 - def test_lzma(self, capfd): + def test_lzma(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_lzma.tif") as im: assert im.mode == "RGB" @@ -853,7 +855,7 @@ class TestFileLibTiff(LibTiffTestCase): sys.stderr.write(captured.err) raise - def test_webp(self, capfd): + def test_webp(self, capfd: pytest.CaptureFixture[str]) -> None: try: with Image.open("Tests/images/hopper_webp.tif") as im: assert im.mode == "RGB" @@ -971,7 +973,7 @@ class TestFileLibTiff(LibTiffTestCase): assert_image_equal_tofile(im, "Tests/images/tiff_16bit_RGBa_target.png") @pytest.mark.parametrize("compression", (None, "jpeg")) - def test_block_tile_tags(self, compression, tmp_path: Path) -> None: + def test_block_tile_tags(self, compression: str | None, tmp_path: Path) -> None: im = hopper() out = str(tmp_path / "temp.tif") @@ -1020,7 +1022,9 @@ class TestFileLibTiff(LibTiffTestCase): ), ], ) - def test_wrong_bits_per_sample(self, file_name, mode, size, tile) -> None: + def test_wrong_bits_per_sample( + self, file_name: str, mode: str, size: tuple[int, int], tile + ) -> None: with Image.open("Tests/images/" + file_name) as im: assert im.mode == mode assert im.size == size @@ -1086,7 +1090,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.READ_LIBTIFF = False @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) - def test_save_multistrip(self, compression, tmp_path: Path) -> None: + def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") im.save(out, compression=compression) @@ -1096,14 +1100,14 @@ class TestFileLibTiff(LibTiffTestCase): assert len(im.tag_v2[STRIPOFFSETS]) > 1 @pytest.mark.parametrize("argument", (True, False)) - def test_save_single_strip(self, argument, tmp_path: Path) -> None: + def test_save_single_strip(self, argument: bool, tmp_path: Path) -> None: im = hopper("RGB").resize((256, 256)) out = str(tmp_path / "temp.tif") if not argument: TiffImagePlugin.STRIP_SIZE = 2**18 try: - arguments = {"compression": "tiff_adobe_deflate"} + arguments: dict[str, str | int] = {"compression": "tiff_adobe_deflate"} if argument: arguments["strip_size"] = 2**18 im.save(out, **arguments) @@ -1114,7 +1118,7 @@ class TestFileLibTiff(LibTiffTestCase): TiffImagePlugin.STRIP_SIZE = 65536 @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", None)) - def test_save_zero(self, compression, tmp_path: Path) -> None: + def test_save_zero(self, compression: str | None, tmp_path: Path) -> None: im = Image.new("RGB", (0, 0)) out = str(tmp_path / "temp.tif") with pytest.raises(SystemError): @@ -1134,7 +1138,7 @@ class TestFileLibTiff(LibTiffTestCase): ("Tests/images/child_ifd_jpeg.tiff", (20,)), ), ) - def test_get_child_images(self, path, sizes) -> None: + def test_get_child_images(self, path: str, sizes: tuple[int, ...]) -> None: with Image.open(path) as im: ims = im.get_child_images() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0f1d96365..d4a634316 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -6,6 +6,7 @@ import warnings import zlib from io import BytesIO from pathlib import Path +from typing import Any import pytest @@ -36,7 +37,7 @@ TEST_PNG_FILE = "Tests/images/hopper.png" MAGIC = PngImagePlugin._MAGIC -def chunk(cid, *data): +def chunk(cid: bytes, *data: bytes) -> bytes: test_file = BytesIO() PngImagePlugin.putchunk(*(test_file, cid) + data) return test_file.getvalue() @@ -52,11 +53,11 @@ HEAD = MAGIC + IHDR TAIL = IDAT + IEND -def load(data): +def load(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def roundtrip(im, **options): +def roundtrip(im: Image.Image, **options: Any) -> Image.Image: out = BytesIO() im.save(out, "PNG", **options) out.seek(0) @@ -65,7 +66,7 @@ def roundtrip(im, **options): @skip_unless_feature("zlib") class TestFilePng: - def get_chunks(self, filename): + def get_chunks(self, filename: str) -> list[bytes]: chunks = [] with open(filename, "rb") as fp: fp.read(8) @@ -436,7 +437,7 @@ class TestFilePng: def test_unicode_text(self) -> None: # Check preservation of non-ASCII characters - def rt_text(value) -> None: + def rt_text(value: str) -> None: im = Image.new("RGB", (32, 32)) info = PngImagePlugin.PngInfo() info.add_text("Text", value) @@ -636,7 +637,7 @@ class TestFilePng: @pytest.mark.parametrize( "cid", (b"IHDR", b"sRGB", b"pHYs", b"acTL", b"fcTL", b"fdAT") ) - def test_truncated_chunks(self, cid) -> None: + def test_truncated_chunks(self, cid: bytes) -> None: fp = BytesIO() with PngImagePlugin.PngStream(fp) as png: with pytest.raises(ValueError): @@ -755,7 +756,7 @@ class TestFilePng: im.seek(1) @pytest.mark.parametrize("buffer", (True, False)) - def test_save_stdout(self, buffer) -> None: + def test_save_stdout(self, buffer: bool) -> None: old_stdout = sys.stdout if buffer: diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index 34a2f8f3d..c4d7a5dd2 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -11,10 +11,9 @@ class TestImagingPaste: masks = {} size = 128 - def assert_9points_image(self, im, expected) -> None: - expected = [ - point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected - ] + def assert_9points_image( + self, im: Image.Image, expected: list[tuple[int, int, int, int]] + ) -> None: px = im.load() actual = [ px[0, 0], @@ -27,9 +26,17 @@ class TestImagingPaste: px[self.size // 2, self.size - 1], px[self.size - 1, self.size - 1], ] - assert actual == expected + assert actual == [ + point[0] if im.mode == "L" else point[: len(im.mode)] for point in expected + ] - def assert_9points_paste(self, im, im2, mask, expected) -> None: + def assert_9points_paste( + self, + im: Image.Image, + im2: Image.Image, + mask: Image.Image, + expected: list[tuple[int, int, int, int]], + ) -> None: im3 = im.copy() im3.paste(im2, (0, 0), mask) self.assert_9points_image(im3, expected) @@ -106,7 +113,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_solid(self, mode) -> None: + def test_image_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "red") im2 = getattr(self, "gradient_" + mode) @@ -116,7 +123,7 @@ class TestImagingPaste: assert_image_equal(im, im2) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_1(self, mode) -> None: + def test_image_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -138,7 +145,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_L(self, mode) -> None: + def test_image_mask_L(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -160,7 +167,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_LA(self, mode) -> None: + def test_image_mask_LA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -182,7 +189,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBA(self, mode) -> None: + def test_image_mask_RGBA(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -204,7 +211,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_image_mask_RGBa(self, mode) -> None: + def test_image_mask_RGBa(self, mode: str) -> None: im = Image.new(mode, (200, 200), "white") im2 = getattr(self, "gradient_" + mode) @@ -226,7 +233,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_solid(self, mode) -> None: + def test_color_solid(self, mode: str) -> None: im = Image.new(mode, (200, 200), "black") rect = (12, 23, 128 + 12, 128 + 23) @@ -239,7 +246,7 @@ class TestImagingPaste: assert sum(head[:255]) == 0 @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_1(self, mode) -> None: + def test_color_mask_1(self, mode: str) -> None: im = Image.new(mode, (200, 200), (50, 60, 70, 80)[: len(mode)]) color = (10, 20, 30, 40)[: len(mode)] @@ -261,7 +268,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_L(self, mode) -> None: + def test_color_mask_L(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -283,7 +290,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBA(self, mode) -> None: + def test_color_mask_RGBA(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" @@ -305,7 +312,7 @@ class TestImagingPaste: ) @pytest.mark.parametrize("mode", ["RGBA", "RGB", "L"]) - def test_color_mask_RGBa(self, mode) -> None: + def test_color_mask_RGBa(self, mode: str) -> None: im = getattr(self, "gradient_" + mode).copy() color = "white" diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index c29830a7e..33b33d6b7 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -48,7 +48,7 @@ gradients_image.load() ((1, 3), (10, 4)), ), ) -def test_args_factor(size, expected) -> None: +def test_args_factor(size: int | tuple[int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(size).size @@ -56,7 +56,7 @@ def test_args_factor(size, expected) -> None: @pytest.mark.parametrize( "size, expected_error", ((0, ValueError), (2.0, TypeError), ((0, 10), ValueError)) ) -def test_args_factor_error(size, expected_error) -> None: +def test_args_factor_error(size: float | tuple[int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(size) @@ -69,7 +69,7 @@ def test_args_factor_error(size, expected_error) -> None: ((5, 5, 6, 6), (1, 1)), ), ) -def test_args_box(size, expected) -> None: +def test_args_box(size: tuple[int, int, int, int], expected: tuple[int, int]) -> None: im = Image.new("L", (10, 10)) assert expected == im.reduce(2, size).size @@ -86,20 +86,20 @@ def test_args_box(size, expected) -> None: ((5, 0, 5, 10), ValueError), ), ) -def test_args_box_error(size, expected_error) -> None: +def test_args_box_error(size: str | tuple[int, int, int, int], expected_error) -> None: im = Image.new("L", (10, 10)) with pytest.raises(expected_error): im.reduce(2, size).size @pytest.mark.parametrize("mode", ("P", "1", "I;16")) -def test_unsupported_modes(mode) -> None: +def test_unsupported_modes(mode: str) -> None: im = Image.new("P", (10, 10)) with pytest.raises(ValueError): im.reduce(3) -def get_image(mode): +def get_image(mode: str) -> Image.Image: mode_info = ImageMode.getmode(mode) if mode_info.basetype == "L": bands = [gradients_image] @@ -119,7 +119,7 @@ def get_image(mode): return im.crop((0, 0, im.width, im.height - 5)) -def compare_reduce_with_box(im, factor) -> None: +def compare_reduce_with_box(im: Image.Image, factor: int | tuple[int, int]) -> None: box = (11, 13, 146, 164) reduced = im.reduce(factor, box=box) reference = im.crop(box).reduce(factor) @@ -127,7 +127,10 @@ def compare_reduce_with_box(im, factor) -> None: def compare_reduce_with_reference( - im, factor, average_diff: float = 0.4, max_diff: int = 1 + im: Image.Image, + factor: int | tuple[int, int], + average_diff: float = 0.4, + max_diff: int = 1, ) -> None: """Image.reduce() should look very similar to Image.resize(BOX). @@ -173,7 +176,9 @@ def compare_reduce_with_reference( assert_compare_images(reduced, reference, average_diff, max_diff) -def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: +def assert_compare_images( + a: Image.Image, b: Image.Image, max_average_diff: float, max_diff: int = 255 +) -> None: assert a.mode == b.mode, f"got mode {repr(a.mode)}, expected {repr(b.mode)}" assert a.size == b.size, f"got size {repr(a.size)}, expected {repr(b.size)}" @@ -201,20 +206,20 @@ def assert_compare_images(a, b, max_average_diff, max_diff: int = 255) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_L(factor) -> None: +def test_mode_L(factor: int | tuple[int, int]) -> None: im = get_image("L") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA(factor) -> None: +def test_mode_LA(factor: int | tuple[int, int]) -> None: im = get_image("LA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_LA_opaque(factor) -> None: +def test_mode_LA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("LA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -223,27 +228,27 @@ def test_mode_LA_opaque(factor) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_La(factor) -> None: +def test_mode_La(factor: int | tuple[int, int]) -> None: im = get_image("La") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGB(factor) -> None: +def test_mode_RGB(factor: int | tuple[int, int]) -> None: im = get_image("RGB") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA(factor) -> None: +def test_mode_RGBA(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") compare_reduce_with_reference(im, factor, 0.8, 5) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBA_opaque(factor) -> None: +def test_mode_RGBA_opaque(factor: int | tuple[int, int]) -> None: im = get_image("RGBA") # With opaque alpha, an error should be way smaller. im.putalpha(Image.new("L", im.size, 255)) @@ -252,21 +257,21 @@ def test_mode_RGBA_opaque(factor) -> None: @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_RGBa(factor) -> None: +def test_mode_RGBa(factor: int | tuple[int, int]) -> None: im = get_image("RGBa") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_I(factor) -> None: +def test_mode_I(factor: int | tuple[int, int]) -> None: im = get_image("I") compare_reduce_with_reference(im, factor) compare_reduce_with_box(im, factor) @pytest.mark.parametrize("factor", remarkable_factors) -def test_mode_F(factor) -> None: +def test_mode_F(factor: int | tuple[int, int]) -> None: im = get_image("F") compare_reduce_with_reference(im, factor, 0, 0) compare_reduce_with_box(im, factor) diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f4c9eb0e6..f3ec12c05 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -1,6 +1,7 @@ from __future__ import annotations from contextlib import contextmanager +from typing import Generator import pytest @@ -51,7 +52,7 @@ class TestImagingResampleVulnerability: class TestImagingCoreResampleAccuracy: - def make_case(self, mode, size, color): + def make_case(self, mode: str, size: tuple[int, int], color: int) -> Image.Image: """Makes a sample image with two dark and two bright squares. For example: e0 e0 1f 1f @@ -66,7 +67,7 @@ class TestImagingCoreResampleAccuracy: return Image.merge(mode, [case] * len(mode)) - def make_sample(self, data, size): + def make_sample(self, data: str, size: tuple[int, int]) -> Image.Image: """Restores a sample image from given data string which contains hex-encoded pixels from the top left fourth of a sample. """ @@ -83,7 +84,7 @@ class TestImagingCoreResampleAccuracy: s_px[size[0] - x - 1, y] = 255 - val return sample - def check_case(self, case, sample) -> None: + def check_case(self, case: Image.Image, sample: Image.Image) -> None: s_px = sample.load() c_px = case.load() for y in range(case.size[1]): @@ -95,7 +96,7 @@ class TestImagingCoreResampleAccuracy: ) assert s_px[x, y] == c_px[x, y], message - def serialize_image(self, image): + def serialize_image(self, image: Image.Image) -> str: s_px = image.load() return "\n".join( " ".join(f"{s_px[x, y]:02x}" for x in range(image.size[0])) @@ -103,7 +104,7 @@ class TestImagingCoreResampleAccuracy: ) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_box(self, mode) -> None: + def test_reduce_box(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -114,7 +115,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bilinear(self, mode) -> None: + def test_reduce_bilinear(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -125,7 +126,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_hamming(self, mode) -> None: + def test_reduce_hamming(self, mode: str) -> None: case = self.make_case(mode, (8, 8), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -136,7 +137,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_bicubic(self, mode) -> None: + def test_reduce_bicubic(self, mode: str) -> None: case = self.make_case(mode, (12, 12), 0xE1) case = case.resize((6, 6), Image.Resampling.BICUBIC) # fmt: off @@ -148,7 +149,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (6, 6))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_reduce_lanczos(self, mode) -> None: + def test_reduce_lanczos(self, mode: str) -> None: case = self.make_case(mode, (16, 16), 0xE1) case = case.resize((8, 8), Image.Resampling.LANCZOS) # fmt: off @@ -161,7 +162,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_box(self, mode) -> None: + def test_enlarge_box(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BOX) # fmt: off @@ -172,7 +173,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bilinear(self, mode) -> None: + def test_enlarge_bilinear(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.BILINEAR) # fmt: off @@ -183,7 +184,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_hamming(self, mode) -> None: + def test_enlarge_hamming(self, mode: str) -> None: case = self.make_case(mode, (2, 2), 0xE1) case = case.resize((4, 4), Image.Resampling.HAMMING) # fmt: off @@ -194,7 +195,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (4, 4))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_bicubic(self, mode) -> None: + def test_enlarge_bicubic(self, mode: str) -> None: case = self.make_case(mode, (4, 4), 0xE1) case = case.resize((8, 8), Image.Resampling.BICUBIC) # fmt: off @@ -207,7 +208,7 @@ class TestImagingCoreResampleAccuracy: self.check_case(channel, self.make_sample(data, (8, 8))) @pytest.mark.parametrize("mode", ("RGBX", "RGB", "La", "L")) - def test_enlarge_lanczos(self, mode) -> None: + def test_enlarge_lanczos(self, mode: str) -> None: case = self.make_case(mode, (6, 6), 0xE1) case = case.resize((12, 12), Image.Resampling.LANCZOS) data = ( @@ -230,7 +231,7 @@ class TestImagingCoreResampleAccuracy: class TestCoreResampleConsistency: - def make_case(self, mode, fill): + def make_case(self, mode: str, fill: tuple[int, int, int] | float): im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] @@ -265,7 +266,7 @@ class TestCoreResampleConsistency: class TestCoreResampleAlphaCorrect: - def make_levels_case(self, mode): + def make_levels_case(self, mode: str) -> Image.Image: i = Image.new(mode, (256, 16)) px = i.load() for y in range(i.size[1]): @@ -275,7 +276,7 @@ class TestCoreResampleAlphaCorrect: px[x, y] = tuple(pix) return i - def run_levels_case(self, i) -> None: + def run_levels_case(self, i: Image.Image) -> None: px = i.load() for y in range(i.size[1]): used_colors = {px[x, y][0] for x in range(i.size[0])} @@ -302,7 +303,9 @@ class TestCoreResampleAlphaCorrect: self.run_levels_case(case.resize((512, 32), Image.Resampling.BICUBIC)) self.run_levels_case(case.resize((512, 32), Image.Resampling.LANCZOS)) - def make_dirty_case(self, mode, clean_pixel, dirty_pixel): + def make_dirty_case( + self, mode: str, clean_pixel: tuple[int, ...], dirty_pixel: tuple[int, ...] + ) -> Image.Image: i = Image.new(mode, (64, 64), dirty_pixel) px = i.load() xdiv4 = i.size[0] // 4 @@ -312,7 +315,7 @@ class TestCoreResampleAlphaCorrect: px[x + xdiv4, y + ydiv4] = clean_pixel return i - def run_dirty_case(self, i, clean_pixel) -> None: + def run_dirty_case(self, i: Image.Image, clean_pixel: tuple[int, ...]) -> None: px = i.load() for y in range(i.size[1]): for x in range(i.size[0]): @@ -432,7 +435,7 @@ class TestCoreResampleBox: Image.Resampling.LANCZOS, ), ) - def test_wrong_arguments(self, resample) -> None: + def test_wrong_arguments(self, resample: Image.Resampling) -> None: im = hopper() im.resize((32, 32), resample, (0, 0, im.width, im.height)) im.resize((32, 32), resample, (20, 20, im.width, im.height)) @@ -459,8 +462,12 @@ class TestCoreResampleBox: with pytest.raises(ValueError, match="can't exceed"): im.resize((32, 32), resample, (0, 0, im.width, im.height + 1)) - def resize_tiled(self, im, dst_size, xtiles, ytiles): - def split_range(size, tiles): + def resize_tiled( + self, im: Image.Image, dst_size: tuple[int, int], xtiles: int, ytiles: int + ) -> Image.Image: + def split_range( + size: int, tiles: int + ) -> Generator[tuple[int, int], None, None]: scale = size / tiles for i in range(tiles): yield int(round(scale * i)), int(round(scale * (i + 1))) @@ -518,7 +525,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "resample", (Image.Resampling.NEAREST, Image.Resampling.BILINEAR) ) - def test_formats(self, mode, resample) -> None: + def test_formats(self, mode: str, resample: Image.Resampling) -> None: im = hopper(mode) box = (20, 20, im.size[0] - 20, im.size[1] - 20) with_box = im.resize((32, 32), resample, box) @@ -558,7 +565,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_horizontal(self, flt) -> None: + def test_skip_horizontal(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() @@ -581,7 +588,7 @@ class TestCoreResampleBox: @pytest.mark.parametrize( "flt", (Image.Resampling.NEAREST, Image.Resampling.BICUBIC) ) - def test_skip_vertical(self, flt) -> None: + def test_skip_vertical(self, flt: Image.Resampling) -> None: # Can skip resize for one dimension im = hopper() diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index ea6e80f1e..b65ea8740 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -5,11 +5,11 @@ import pytest from PIL import Image, ImageMath -def pixel(im): - if hasattr(im, "im"): - return f"{im.mode} {repr(im.getpixel((0, 0)))}" +def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans + else: + return f"{im.mode} {repr(im.getpixel((0, 0)))}" A = Image.new("L", (1, 1), 1) @@ -60,7 +60,7 @@ def test_ops() -> None: "(lambda: (lambda: exec('pass'))())()", ), ) -def test_prevent_exec(expression) -> None: +def test_prevent_exec(expression: str) -> None: with pytest.raises(ValueError): ImageMath.eval(expression) From 463c36821136652a05517a4db810a265d25c9b0c Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:02:34 +1100 Subject: [PATCH 16/29] Simplified code Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_imagemath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/test_imagemath.py b/Tests/test_imagemath.py index b65ea8740..a21e2307d 100644 --- a/Tests/test_imagemath.py +++ b/Tests/test_imagemath.py @@ -8,8 +8,8 @@ from PIL import Image, ImageMath def pixel(im: Image.Image | int) -> str | int: if isinstance(im, int): return int(im) # hack to deal with booleans - else: - return f"{im.mode} {repr(im.getpixel((0, 0)))}" + + return f"{im.mode} {repr(im.getpixel((0, 0)))}" A = Image.new("L", (1, 1), 1) From a276cf2c9fadf39cc5e663e44bc160c566d7c050 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Feb 2024 18:48:38 +1100 Subject: [PATCH 17/29] Use _typing alias --- src/PIL/ImageFont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 9f8394d63..7be2fdf04 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -33,10 +33,10 @@ import sys import warnings from enum import IntEnum from io import BytesIO -from pathlib import Path from typing import BinaryIO from . import Image +from ._typing import StrOrBytesPath from ._util import is_directory, is_path @@ -193,7 +193,7 @@ class FreeTypeFont: def __init__( self, - font: bytes | str | Path | BinaryIO | None = None, + font: StrOrBytesPath | BinaryIO | None = None, size: float = 10, index: int = 0, encoding: str = "", From a118a82c30acf6427653b129fab263fde3bdbbac Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 8 Feb 2024 18:35:37 +1100 Subject: [PATCH 18/29] Use os.path.realpath consistently when os.fspath is used --- src/PIL/Image.py | 2 +- src/PIL/ImageFont.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d7d0a1ae7..adb63b07f 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2385,7 +2385,7 @@ class Image: filename = "" open_fp = False if is_path(fp): - filename = os.fspath(fp) + filename = os.path.realpath(os.fspath(fp)) open_fp = True elif fp == sys.stdout: try: diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 7be2fdf04..256c581df 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -230,7 +230,7 @@ class FreeTypeFont: ) if is_path(font): - font = os.fspath(font) + font = os.path.realpath(os.fspath(font)) if sys.platform == "win32": font_bytes_path = font if isinstance(font, bytes) else font.encode() try: From 152a24e13abfe099d4cf75dc7982290feb200ad2 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 16:48:02 +1100 Subject: [PATCH 19/29] Simplified code --- src/PIL/Image.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index adb63b07f..231674f54 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3240,10 +3240,8 @@ def open(fp, mode="r", formats=None) -> Image: exclusive_fp = False filename = "" - if isinstance(fp, os.PathLike): + if is_path(fp): filename = os.path.realpath(os.fspath(fp)) - elif is_path(fp): - filename = fp if filename: fp = builtins.open(filename, "rb") From 19a6edeecce2f3605fcdb074c00ac0152c6bdf05 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 19:50:45 +1100 Subject: [PATCH 20/29] Added type hints --- pyproject.toml | 10 ---- src/PIL/DdsImagePlugin.py | 16 ++++-- src/PIL/ImImagePlugin.py | 4 +- src/PIL/Image.py | 86 +++++++++++++++++++---------- src/PIL/ImageQt.py | 12 +++- src/PIL/PdfParser.py | 11 +++- src/PIL/PyAccess.py | 1 + src/PIL/TiffImagePlugin.py | 109 +++++++++++++++++++++---------------- src/PIL/TiffTags.py | 4 +- src/PIL/_imaging.pyi | 5 ++ src/PIL/_tkinter_finder.py | 3 +- src/PIL/_webp.pyi | 5 ++ tox.ini | 5 ++ 13 files changed, 171 insertions(+), 100 deletions(-) create mode 100644 src/PIL/_imaging.pyi create mode 100644 src/PIL/_webp.pyi diff --git a/pyproject.toml b/pyproject.toml index 652ae3633..48c59f2a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,16 +141,6 @@ warn_redundant_casts = true warn_unreachable = true warn_unused_ignores = true exclude = [ - '^src/PIL/_tkinter_finder.py$', - '^src/PIL/DdsImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$', - '^src/PIL/Image.py$', - '^src/PIL/ImageQt.py$', - '^src/PIL/ImImagePlugin.py$', '^src/PIL/MicImagePlugin.py$', - '^src/PIL/PdfParser.py$', - '^src/PIL/PyAccess.py$', - '^src/PIL/TiffImagePlugin.py$', - '^src/PIL/TiffTags.py$', - '^src/PIL/WebPImagePlugin.py$', ] diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 3785174ef..be17f4223 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -270,13 +270,17 @@ class D3DFMT(IntEnum): # Backward compatibility layer module = sys.modules[__name__] for item in DDSD: + assert item.name is not None setattr(module, "DDSD_" + item.name, item.value) -for item in DDSCAPS: - setattr(module, "DDSCAPS_" + item.name, item.value) -for item in DDSCAPS2: - setattr(module, "DDSCAPS2_" + item.name, item.value) -for item in DDPF: - setattr(module, "DDPF_" + item.name, item.value) +for item1 in DDSCAPS: + assert item1.name is not None + setattr(module, "DDSCAPS_" + item1.name, item1.value) +for item2 in DDSCAPS2: + assert item2.name is not None + setattr(module, "DDSCAPS2_" + item2.name, item2.value) +for item3 in DDPF: + assert item3.name is not None + setattr(module, "DDPF_" + item3.name, item3.value) DDS_FOURCC = DDPF.FOURCC DDS_RGB = DDPF.RGB diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index 97d726a8a..4613e40b6 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]: for i in ["32S"]: OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}") -for i in range(2, 33): - OPEN[f"L*{i} image"] = ("F", f"F;{i}") +for j in range(2, 33): + OPEN[f"L*{j} image"] = ("F", f"F;{j}") # -------------------------------------------------------------------- diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 111d06012..d32a0fc19 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -26,6 +26,7 @@ from __future__ import annotations +import abc import atexit import builtins import io @@ -40,11 +41,8 @@ import warnings from collections.abc import Callable, MutableMapping from enum import IntEnum from pathlib import Path - -try: - from defusedxml import ElementTree -except ImportError: - ElementTree = None +from types import ModuleType +from typing import IO, TYPE_CHECKING, Any # VERSION was removed in Pillow 6.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0. @@ -60,6 +58,12 @@ from . import ( from ._binary import i32le, o32be, o32le from ._util import DeferredError, is_path +ElementTree: ModuleType | None +try: + from defusedxml import ElementTree +except ImportError: + ElementTree = None + logger = logging.getLogger(__name__) @@ -110,6 +114,7 @@ except ImportError as v: USE_CFFI_ACCESS = False +cffi: ModuleType | None try: import cffi except ImportError: @@ -211,14 +216,22 @@ if hasattr(core, "DEFAULT_STRATEGY"): # -------------------------------------------------------------------- # Registries -ID = [] -OPEN = {} -MIME = {} -SAVE = {} -SAVE_ALL = {} -EXTENSION = {} -DECODERS = {} -ENCODERS = {} +if TYPE_CHECKING: + from . import ImageFile # pragma: no cover +ID: list[str] = [] +OPEN: dict[ + str, + tuple[ + Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + Callable[[bytes], bool] | None, + ], +] = {} +MIME: dict[str, str] = {} +SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} +EXTENSION: dict[str, str] = {} +DECODERS: dict[str, object] = {} +ENCODERS: dict[str, object] = {} # -------------------------------------------------------------------- # Modes @@ -2383,12 +2396,12 @@ class Image: may have been created, and may contain partial data. """ - filename = "" + filename: str | bytes = "" open_fp = False if isinstance(fp, Path): filename = str(fp) open_fp = True - elif is_path(fp): + elif isinstance(fp, (str, bytes)): filename = fp open_fp = True elif fp == sys.stdout: @@ -2398,7 +2411,7 @@ class Image: pass if not filename and hasattr(fp, "name") and is_path(fp.name): # only set the name for metadata purposes - filename = fp.name + filename = os.path.realpath(os.fspath(fp.name)) # may mutate self! self._ensure_mutable() @@ -2409,7 +2422,8 @@ class Image: preinit() - ext = os.path.splitext(filename)[1].lower() + filename_ext = os.path.splitext(filename)[1].lower() + ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext if not format: if ext not in EXTENSION: @@ -2451,7 +2465,7 @@ class Image: if open_fp: fp.close() - def seek(self, frame) -> Image: + def seek(self, frame) -> None: """ Seeks to the given frame in this sequence file. If you seek beyond the end of the sequence, the method raises an @@ -2511,10 +2525,9 @@ class Image: self.load() if self.im.bands == 1: - ims = [self.copy()] + return (self.copy(),) else: - ims = map(self._new, self.im.split()) - return tuple(ims) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ @@ -2871,7 +2884,14 @@ class ImageTransformHandler: (for use with :py:meth:`~PIL.Image.Image.transform`) """ - pass + @abc.abstractmethod + def transform( + self, + size: tuple[int, int], + image: Image, + **options: dict[str, str | int | tuple[int, ...] | list[int]], + ) -> Image: + pass # pragma: no cover # -------------------------------------------------------------------- @@ -3243,11 +3263,9 @@ def open(fp, mode="r", formats=None) -> Image: raise TypeError(msg) exclusive_fp = False - filename = "" - if isinstance(fp, Path): - filename = str(fp.resolve()) - elif is_path(fp): - filename = fp + filename: str | bytes = "" + if is_path(fp): + filename = os.path.realpath(os.fspath(fp)) if filename: fp = builtins.open(filename, "rb") @@ -3421,7 +3439,11 @@ def merge(mode, bands): # Plugin registry -def register_open(id, factory, accept=None) -> None: +def register_open( + id, + factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], + accept: Callable[[bytes], bool] | None = None, +) -> None: """ Register an image file plugin. This function should not be used in application code. @@ -3631,7 +3653,13 @@ _apply_env_variables() atexit.register(core.clear_cache) -class Exif(MutableMapping): +if TYPE_CHECKING: + _ExifBase = MutableMapping[int, Any] # pragma: no cover +else: + _ExifBase = MutableMapping + + +class Exif(_ExifBase): """ This class provides read and write access to EXIF image data:: diff --git a/src/PIL/ImageQt.py b/src/PIL/ImageQt.py index 6377c7501..293ba4941 100644 --- a/src/PIL/ImageQt.py +++ b/src/PIL/ImageQt.py @@ -19,19 +19,26 @@ from __future__ import annotations import sys from io import BytesIO +from typing import Callable from . import Image from ._util import is_path +qt_version: str | None qt_versions = [ ["6", "PyQt6"], ["side6", "PySide6"], ] # If a version has already been imported, attempt it first -qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True) -for qt_version, qt_module in qt_versions: +qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True) +for version, qt_module in qt_versions: try: + QBuffer: type + QIODevice: type + QImage: type + QPixmap: type + qRgba: Callable[[int, int, int, int], int] if qt_module == "PyQt6": from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtGui import QImage, QPixmap, qRgba @@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions: except (ImportError, RuntimeError): continue qt_is_installed = True + qt_version = version break else: qt_is_installed = False diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 014460006..9aa8dde83 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -8,6 +8,7 @@ import os import re import time import zlib +from typing import TYPE_CHECKING, Any, List, Union # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set @@ -239,12 +240,18 @@ class PdfName: return bytes(result) -class PdfArray(list): +class PdfArray(List[Any]): def __bytes__(self): return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" -class PdfDict(collections.UserDict): +if TYPE_CHECKING: + _DictBase = collections.UserDict[Union[str, bytes], Any] # pragma: no cover +else: + _DictBase = collections.UserDict + + +class PdfDict(_DictBase): def __setattr__(self, key, value): if key == "data": collections.UserDict.__setattr__(self, key, value) diff --git a/src/PIL/PyAccess.py b/src/PIL/PyAccess.py index 07bb712d8..2c831913d 100644 --- a/src/PIL/PyAccess.py +++ b/src/PIL/PyAccess.py @@ -25,6 +25,7 @@ import sys from ._deprecate import deprecate +FFI: type try: from cffi import FFI diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index e20d4d5ea..af22d76cb 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -50,6 +50,7 @@ import warnings from collections.abc import MutableMapping from fractions import Fraction from numbers import Number, Rational +from typing import TYPE_CHECKING, Any, Callable from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 @@ -306,6 +307,13 @@ _load_dispatch = {} _write_dispatch = {} +def _delegate(op): + def delegate(self, *args): + return getattr(self._val, op)(*args) + + return delegate + + class IFDRational(Rational): """Implements a rational class where 0/0 is a legal value to match the in the wild use of exif rationals. @@ -391,12 +399,6 @@ class IFDRational(Rational): self._numerator = _numerator self._denominator = _denominator - def _delegate(op): - def delegate(self, *args): - return getattr(self._val, op)(*args) - - return delegate - """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'mod','rmod', 'pow','rpow', 'pos', 'neg', @@ -436,7 +438,50 @@ class IFDRational(Rational): __int__ = _delegate("__int__") -class ImageFileDirectory_v2(MutableMapping): +def _register_loader(idx, size): + def decorator(func): + from .TiffTags import TYPES + + if func.__name__.startswith("load_"): + TYPES[idx] = func.__name__[5:].replace("_", " ") + _load_dispatch[idx] = size, func # noqa: F821 + return func + + return decorator + + +def _register_writer(idx): + def decorator(func): + _write_dispatch[idx] = func # noqa: F821 + return func + + return decorator + + +def _register_basic(idx_fmt_name): + from .TiffTags import TYPES + + idx, fmt, name = idx_fmt_name + TYPES[idx] = name + size = struct.calcsize("=" + fmt) + _load_dispatch[idx] = ( # noqa: F821 + size, + lambda self, data, legacy_api=True: ( + self._unpack(f"{len(data) // size}{fmt}", data) + ), + ) + _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 + b"".join(self._pack(fmt, value) for value in values) + ) + + +if TYPE_CHECKING: + _IFDv2Base = MutableMapping[int, Any] # pragma: no cover +else: + _IFDv2Base = MutableMapping + + +class ImageFileDirectory_v2(_IFDv2Base): """This class represents a TIFF tag directory. To speed things up, we don't decode tags unless they're asked for. @@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping): """ + _load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {} + _write_dispatch: dict[int, Callable[..., Any]] = {} + def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): """Initialize an ImageFileDirectory. @@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping): prefix = property(lambda self: self._prefix) offset = property(lambda self: self._offset) - legacy_api = property(lambda self: self._legacy_api) + + @property + def legacy_api(self): + return self._legacy_api @legacy_api.setter def legacy_api(self, value): @@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping): def _pack(self, fmt, *values): return struct.pack(self._endian + fmt, *values) - def _register_loader(idx, size): - def decorator(func): - from .TiffTags import TYPES - - if func.__name__.startswith("load_"): - TYPES[idx] = func.__name__[5:].replace("_", " ") - _load_dispatch[idx] = size, func # noqa: F821 - return func - - return decorator - - def _register_writer(idx): - def decorator(func): - _write_dispatch[idx] = func # noqa: F821 - return func - - return decorator - - def _register_basic(idx_fmt_name): - from .TiffTags import TYPES - - idx, fmt, name = idx_fmt_name - TYPES[idx] = name - size = struct.calcsize("=" + fmt) - _load_dispatch[idx] = ( # noqa: F821 - size, - lambda self, data, legacy_api=True: ( - self._unpack(f"{len(data) // size}{fmt}", data) - ), - ) - _write_dispatch[idx] = lambda self, *values: ( # noqa: F821 - b"".join(self._pack(fmt, value) for value in values) - ) - list( map( _register_basic, @@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2): tagdata = property(lambda self: self._tagdata) # defined in ImageFileDirectory_v2 - tagtype: dict + tagtype: dict[int, int] """Dictionary of tag types""" @classmethod @@ -1835,11 +1852,11 @@ def _save(im, fp, filename): tags = list(atts.items()) tags.sort() a = (rawmode, compression, _fp, filename, tags, types) - e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) - e.setimage(im.im, (0, 0) + im.size) + encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig) + encoder.setimage(im.im, (0, 0) + im.size) while True: # undone, change to self.decodermaxblock: - errcode, data = e.encode(16 * 1024)[1:] + errcode, data = encoder.encode(16 * 1024)[1:] if not _fp: fp.write(data) if errcode: diff --git a/src/PIL/TiffTags.py b/src/PIL/TiffTags.py index 88ff2f4fc..b94193931 100644 --- a/src/PIL/TiffTags.py +++ b/src/PIL/TiffTags.py @@ -22,7 +22,7 @@ from collections import namedtuple class TagInfo(namedtuple("_TagInfo", "value name type length enum")): - __slots__ = [] + __slots__: list[str] = [] def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): return super().__new__(cls, value, name, type, length, enum or {}) @@ -437,7 +437,7 @@ _populate() ## # Map type numbers to type names -- defined in ImageFileDirectory. -TYPES = {} +TYPES: dict[int, str] = {} # # These tags are handled by default in libtiff, without diff --git a/src/PIL/_imaging.pyi b/src/PIL/_imaging.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_imaging.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/src/PIL/_tkinter_finder.py b/src/PIL/_tkinter_finder.py index 71c0ad465..beddfb062 100644 --- a/src/PIL/_tkinter_finder.py +++ b/src/PIL/_tkinter_finder.py @@ -5,7 +5,8 @@ from __future__ import annotations import sys import tkinter -from tkinter import _tkinter as tk + +tk = getattr(tkinter, "_tkinter") try: if hasattr(sys, "pypy_find_executable"): diff --git a/src/PIL/_webp.pyi b/src/PIL/_webp.pyi new file mode 100644 index 000000000..b0235555d --- /dev/null +++ b/src/PIL/_webp.pyi @@ -0,0 +1,5 @@ +from __future__ import annotations + +from typing import Any + +def __getattr__(name: str) -> Any: ... diff --git a/tox.ini b/tox.ini index fb6746ce7..8c818df7a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,9 +33,14 @@ commands = [testenv:mypy] skip_install = true deps = + IceSpringPySideStubs-PyQt6 + IceSpringPySideStubs-PySide6 ipython mypy==1.7.1 numpy + packaging + types-cffi + types-defusedxml extras = typing commands = From 517b797132a65a8a873d0c22008d760d2a706ff6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 20:47:32 +1100 Subject: [PATCH 21/29] Removed FileDescriptor --- src/PIL/GdImageFile.py | 6 ++---- src/PIL/_typing.py | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py index 315ac6d6c..88b87a22c 100644 --- a/src/PIL/GdImageFile.py +++ b/src/PIL/GdImageFile.py @@ -32,7 +32,7 @@ from typing import IO from . import ImageFile, ImagePalette, UnidentifiedImageError from ._binary import i16be as i16 from ._binary import i32be as i32 -from ._typing import FileDescriptor, StrOrBytesPath +from ._typing import StrOrBytesPath class GdImageFile(ImageFile.ImageFile): @@ -81,9 +81,7 @@ class GdImageFile(ImageFile.ImageFile): ] -def open( - fp: StrOrBytesPath | FileDescriptor | IO[bytes], mode: str = "r" -) -> GdImageFile: +def open(fp: StrOrBytesPath | IO[bytes], mode: str = "r") -> GdImageFile: """ Load texture from a GD image file. diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index 346702037..7075e8672 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -27,8 +27,7 @@ class SupportsRead(Protocol[_T_co]): def read(self, __length: int = ...) -> _T_co: ... -FileDescriptor = int StrOrBytesPath = Union[str, bytes, "os.PathLike[str]", "os.PathLike[bytes]"] -__all__ = ["FileDescriptor", "TypeGuard", "StrOrBytesPath", "SupportsRead"] +__all__ = ["TypeGuard", "StrOrBytesPath", "SupportsRead"] From 68db96981c0819efc51bea995915eaa389a292e7 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:50:48 +1100 Subject: [PATCH 22/29] Removed else Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index d32a0fc19..c3ab62174 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2526,8 +2526,7 @@ class Image: self.load() if self.im.bands == 1: return (self.copy(),) - else: - return tuple(map(self._new, self.im.split())) + return tuple(map(self._new, self.im.split())) def getchannel(self, channel): """ From d02a778efd443db9f69233763f187e14eebde6db Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Sat, 10 Feb 2024 21:57:59 +1100 Subject: [PATCH 23/29] Removed no cover pragmas Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 6 +++--- src/PIL/PdfParser.py | 2 +- src/PIL/TiffImagePlugin.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index c3ab62174..d9d708d5d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -217,7 +217,7 @@ if hasattr(core, "DEFAULT_STRATEGY"): # Registries if TYPE_CHECKING: - from . import ImageFile # pragma: no cover + from . import ImageFile ID: list[str] = [] OPEN: dict[ str, @@ -2890,7 +2890,7 @@ class ImageTransformHandler: image: Image, **options: dict[str, str | int | tuple[int, ...] | list[int]], ) -> Image: - pass # pragma: no cover + pass # -------------------------------------------------------------------- @@ -3653,7 +3653,7 @@ atexit.register(core.clear_cache) if TYPE_CHECKING: - _ExifBase = MutableMapping[int, Any] # pragma: no cover + _ExifBase = MutableMapping[int, Any] else: _ExifBase = MutableMapping diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 9aa8dde83..4c5101738 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -246,7 +246,7 @@ class PdfArray(List[Any]): if TYPE_CHECKING: - _DictBase = collections.UserDict[Union[str, bytes], Any] # pragma: no cover + _DictBase = collections.UserDict[Union[str, bytes], Any] else: _DictBase = collections.UserDict diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index af22d76cb..3ba4de9d1 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -476,7 +476,7 @@ def _register_basic(idx_fmt_name): if TYPE_CHECKING: - _IFDv2Base = MutableMapping[int, Any] # pragma: no cover + _IFDv2Base = MutableMapping[int, Any] else: _IFDv2Base = MutableMapping From 8ef0ffc2b849245bde6f96b58b4af48bf498bda7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:37:42 +1100 Subject: [PATCH 24/29] Removed no cover pragma --- src/PIL/ImageShow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index c03122c11..d90545e92 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -184,7 +184,7 @@ class UnixViewer(Viewer): @abc.abstractmethod def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: - pass # pragma: no cover + pass def get_command(self, file: str, **options: Any) -> str: command = self.get_command_ex(file, **options)[0] From e614bbfe501811bcb4a080ba9f07d745ba3ad231 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:39:18 +1100 Subject: [PATCH 25/29] Exclude code only for type checking --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 46df3f90d..115286b74 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,6 +10,7 @@ exclude_also = if DEBUG: # Don't complain about compatibility code for missing optional dependencies except ImportError + if TYPE_CHECKING: [run] omit = From 112a5a4813f34235530c2ac382108a5cf722789a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 10 Feb 2024 22:40:24 +1100 Subject: [PATCH 26/29] Exclude abstract methods --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 115286b74..ca5f114c6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,6 +11,7 @@ exclude_also = # Don't complain about compatibility code for missing optional dependencies except ImportError if TYPE_CHECKING: + @abc.abstractmethod [run] omit = From 3977124908b934a9b037d1e8ba5549393bed9dda Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Sat, 10 Feb 2024 14:54:20 +0200 Subject: [PATCH 27/29] Update docs/reference/internal_modules.rst Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- docs/reference/internal_modules.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/reference/internal_modules.rst b/docs/reference/internal_modules.rst index c3cc70060..899e4966f 100644 --- a/docs/reference/internal_modules.rst +++ b/docs/reference/internal_modules.rst @@ -33,10 +33,6 @@ Internal Modules Provides a convenient way to import type hints that are not available on some Python versions. -.. py:class:: FileDescriptor - - Typing alias. - .. py:class:: StrOrBytesPath Typing alias. From 3f6422b512ff39cffaa5a37915c970d7683b6d62 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Mon, 12 Feb 2024 09:28:53 +1100 Subject: [PATCH 28/29] Added type hints --- Tests/helper.py | 2 +- Tests/test_image_access.py | 47 ++++++++++++++++++-------------- Tests/test_image_array.py | 10 ++++--- Tests/test_image_draft.py | 7 ++++- Tests/test_image_entropy.py | 2 +- Tests/test_image_filter.py | 27 ++++++++++++------ Tests/test_image_getextrema.py | 2 +- Tests/test_image_getpalette.py | 2 +- Tests/test_image_paste.py | 14 +++++----- Tests/test_image_putdata.py | 6 ++-- Tests/test_image_putpalette.py | 4 +-- Tests/test_image_resample.py | 8 ++++-- Tests/test_image_rotate.py | 14 +++++++--- Tests/test_image_thumbnail.py | 2 +- Tests/test_image_transform.py | 50 ++++++++++++++++++++++++---------- 15 files changed, 124 insertions(+), 73 deletions(-) diff --git a/Tests/helper.py b/Tests/helper.py index 3e2a40e02..b98883946 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -244,7 +244,7 @@ def fromstring(data: bytes) -> Image.Image: return Image.open(BytesIO(data)) -def tostring(im: Image.Image, string_format: str, **options: dict[str, Any]) -> bytes: +def tostring(im: Image.Image, string_format: str, **options: Any) -> bytes: out = BytesIO() im.save(out, string_format, **options) return out.getvalue() diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index e4cb2dad1..3bdaea750 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -4,6 +4,7 @@ import os import subprocess import sys import sysconfig +from types import ModuleType import pytest @@ -23,6 +24,7 @@ else: except ImportError: cffi = None +numpy: ModuleType | None try: import numpy except ImportError: @@ -71,9 +73,10 @@ class TestImagePutPixel(AccessTest): pix1 = im1.load() pix2 = im2.load() - for x, y in ((0, "0"), ("0", 0)): - with pytest.raises(TypeError): - pix1[x, y] + with pytest.raises(TypeError): + pix1[0, "0"] + with pytest.raises(TypeError): + pix1["0", 0] for y in range(im1.size[1]): for x in range(im1.size[0]): @@ -123,12 +126,13 @@ class TestImagePutPixel(AccessTest): im = hopper() pix = im.load() + assert numpy is not None assert pix[numpy.int32(1), numpy.int32(2)] == (18, 20, 59) class TestImageGetPixel(AccessTest): @staticmethod - def color(mode): + def color(mode: str) -> int | tuple[int, ...]: bands = Image.getmodebands(mode) if bands == 1: return 1 @@ -138,12 +142,13 @@ class TestImageGetPixel(AccessTest): return (16, 32, 49) return tuple(range(1, bands + 1)) - def check(self, mode, expected_color=None) -> None: + def check(self, mode: str, expected_color_int: int | None = None) -> None: if self._need_cffi_access and mode.startswith("BGR;"): pytest.skip("Support not added to deprecated module for BGR;* modes") - if not expected_color: - expected_color = self.color(mode) + expected_color = ( + expected_color_int if expected_color_int is not None else self.color(mode) + ) # check putpixel im = Image.new(mode, (1, 1), None) @@ -222,7 +227,7 @@ class TestImageGetPixel(AccessTest): "YCbCr", ), ) - def test_basic(self, mode) -> None: + def test_basic(self, mode: str) -> None: self.check(mode) def test_list(self) -> None: @@ -231,14 +236,14 @@ class TestImageGetPixel(AccessTest): @pytest.mark.parametrize("mode", ("I;16", "I;16B")) @pytest.mark.parametrize("expected_color", (2**15 - 1, 2**15, 2**15 + 1, 2**16 - 1)) - def test_signedness(self, mode, expected_color) -> None: + def test_signedness(self, mode: str, expected_color: int) -> None: # see https://github.com/python-pillow/Pillow/issues/452 # pixelaccess is using signed int* instead of uint* self.check(mode, expected_color) @pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("color", ((255, 0, 0), (255, 0, 0, 255))) - def test_p_putpixel_rgb_rgba(self, mode, color) -> None: + def test_p_putpixel_rgb_rgba(self, mode: str, color: tuple[int, ...]) -> None: im = Image.new(mode, (1, 1)) im.putpixel((0, 0), color) @@ -262,7 +267,7 @@ class TestCffiGetPixel(TestImageGetPixel): class TestCffi(AccessTest): _need_cffi_access = True - def _test_get_access(self, im) -> None: + def _test_get_access(self, im: Image.Image) -> None: """Do we get the same thing as the old pixel access Using private interfaces, forcing a capi access and @@ -299,7 +304,7 @@ class TestCffi(AccessTest): # im = Image.new('I;32B', (10, 10), 2**10) # self._test_get_access(im) - def _test_set_access(self, im, color) -> None: + def _test_set_access(self, im: Image.Image, color: tuple[int, ...] | float) -> None: """Are we writing the correct bits into the image? Using private interfaces, forcing a capi access and @@ -359,7 +364,7 @@ class TestCffi(AccessTest): assert px[i, 0] == 0 @pytest.mark.parametrize("mode", ("P", "PA")) - def test_p_putpixel_rgb_rgba(self, mode) -> None: + def test_p_putpixel_rgb_rgba(self, mode: str) -> None: for color in ((255, 0, 0), (255, 0, 0, 127 if mode == "PA" else 255)): im = Image.new(mode, (1, 1)) with pytest.warns(DeprecationWarning): @@ -377,7 +382,7 @@ class TestImagePutPixelError(AccessTest): INVALID_TYPES = ["foo", 1.0, None] @pytest.mark.parametrize("mode", IMAGE_MODES1) - def test_putpixel_type_error1(self, mode) -> None: + def test_putpixel_type_error1(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises(TypeError, match="color must be int or tuple"): @@ -400,14 +405,16 @@ class TestImagePutPixelError(AccessTest): ), ), ) - def test_putpixel_invalid_number_of_bands(self, mode, band_numbers, match) -> None: + def test_putpixel_invalid_number_of_bands( + self, mode: str, band_numbers: tuple[int, ...], match: str + ) -> None: im = hopper(mode) for band_number in band_numbers: with pytest.raises(TypeError, match=match): im.putpixel((0, 0), (0,) * band_number) @pytest.mark.parametrize("mode", IMAGE_MODES2) - def test_putpixel_type_error2(self, mode) -> None: + def test_putpixel_type_error2(self, mode: str) -> None: im = hopper(mode) for v in self.INVALID_TYPES: with pytest.raises( @@ -416,7 +423,7 @@ class TestImagePutPixelError(AccessTest): im.putpixel((0, 0), v) @pytest.mark.parametrize("mode", IMAGE_MODES1 + IMAGE_MODES2) - def test_putpixel_overflow_error(self, mode) -> None: + def test_putpixel_overflow_error(self, mode: str) -> None: im = hopper(mode) with pytest.raises(OverflowError): im.putpixel((0, 0), 2**80) @@ -428,7 +435,7 @@ class TestEmbeddable: def test_embeddable(self) -> None: import ctypes - from setuptools.command.build_ext import new_compiler + from setuptools.command import build_ext with open("embed_pil.c", "w", encoding="utf-8") as fh: fh.write( @@ -457,7 +464,7 @@ int main(int argc, char* argv[]) % sys.prefix.replace("\\", "\\\\") ) - compiler = new_compiler() + compiler = getattr(build_ext, "new_compiler")() compiler.add_include_dir(sysconfig.get_config_var("INCLUDEPY")) libdir = sysconfig.get_config_var("LIBDIR") or sysconfig.get_config_var( @@ -471,7 +478,7 @@ int main(int argc, char* argv[]) env["PATH"] = sys.prefix + ";" + env["PATH"] # do not display the Windows Error Reporting dialog - ctypes.windll.kernel32.SetErrorMode(0x0002) + getattr(ctypes, "windll").kernel32.SetErrorMode(0x0002) process = subprocess.Popen(["embed_pil.exe"], env=env) process.communicate() diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py index 0125ab56a..cf85ee4fa 100644 --- a/Tests/test_image_array.py +++ b/Tests/test_image_array.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import Any + import pytest from packaging.version import parse as parse_version @@ -13,7 +15,7 @@ im = hopper().resize((128, 100)) def test_toarray() -> None: - def test(mode): + def test(mode: str) -> tuple[tuple[int, ...], str, int]: ai = numpy.array(im.convert(mode)) return ai.shape, ai.dtype.str, ai.nbytes @@ -50,14 +52,14 @@ def test_fromarray() -> None: class Wrapper: """Class with API matching Image.fromarray""" - def __init__(self, img, arr_params) -> None: + def __init__(self, img: Image.Image, arr_params: dict[str, Any]) -> None: self.img = img self.__array_interface__ = arr_params - def tobytes(self): + def tobytes(self) -> bytes: return self.img.tobytes() - def test(mode): + def test(mode: str) -> tuple[str, tuple[int, int], bool]: i = im.convert(mode) a = numpy.array(i) # Make wrapper instance for image, new array interface diff --git a/Tests/test_image_draft.py b/Tests/test_image_draft.py index 54474311a..1ce1a7cd8 100644 --- a/Tests/test_image_draft.py +++ b/Tests/test_image_draft.py @@ -7,7 +7,12 @@ from .helper import fromstring, skip_unless_feature, tostring pytestmark = skip_unless_feature("jpg") -def draft_roundtrip(in_mode, in_size, req_mode, req_size): +def draft_roundtrip( + in_mode: str, + in_size: tuple[int, int], + req_mode: str | None, + req_size: tuple[int, int] | None, +) -> Image.Image: im = Image.new(in_mode, in_size) data = tostring(im, "JPEG") im = fromstring(data) diff --git a/Tests/test_image_entropy.py b/Tests/test_image_entropy.py index 01107ae6b..c1dbb879b 100644 --- a/Tests/test_image_entropy.py +++ b/Tests/test_image_entropy.py @@ -4,7 +4,7 @@ from .helper import hopper def test_entropy() -> None: - def entropy(mode): + def entropy(mode: str) -> float: return hopper(mode).entropy() assert round(abs(entropy("1") - 0.9138803254693582), 7) == 0 diff --git a/Tests/test_image_filter.py b/Tests/test_image_filter.py index 2b6787933..6a10ae453 100644 --- a/Tests/test_image_filter.py +++ b/Tests/test_image_filter.py @@ -36,7 +36,7 @@ from .helper import assert_image_equal, hopper ), ) @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity(filter_to_apply, mode) -> None: +def test_sanity(filter_to_apply: ImageFilter.Filter, mode: str) -> None: im = hopper(mode) if mode != "I" or isinstance(filter_to_apply, ImageFilter.BuiltinFilter): out = im.filter(filter_to_apply) @@ -45,7 +45,7 @@ def test_sanity(filter_to_apply, mode) -> None: @pytest.mark.parametrize("mode", ("L", "I", "RGB", "CMYK")) -def test_sanity_error(mode) -> None: +def test_sanity_error(mode: str) -> None: with pytest.raises(TypeError): im = hopper(mode) im.filter("hello") @@ -53,7 +53,7 @@ def test_sanity_error(mode) -> None: # crashes on small images @pytest.mark.parametrize("size", ((1, 1), (2, 2), (3, 3))) -def test_crash(size) -> None: +def test_crash(size: tuple[int, int]) -> None: im = Image.new("RGB", size) im.filter(ImageFilter.SMOOTH) @@ -67,7 +67,10 @@ def test_crash(size) -> None: ("RGB", ((4, 0, 0), (0, 0, 0))), ), ) -def test_modefilter(mode, expected) -> None: +def test_modefilter( + mode: str, + expected: tuple[int, int] | tuple[tuple[int, int, int], tuple[int, int, int]], +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -90,7 +93,13 @@ def test_modefilter(mode, expected) -> None: ("F", (0.0, 4.0, 8.0)), ), ) -def test_rankfilter(mode, expected) -> None: +def test_rankfilter( + mode: str, + expected: ( + tuple[float, float, float] + | tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] + ), +) -> None: im = Image.new(mode, (3, 3), None) im.putdata(list(range(9))) # image is: @@ -106,7 +115,7 @@ def test_rankfilter(mode, expected) -> None: @pytest.mark.parametrize( "filter", (ImageFilter.MinFilter, ImageFilter.MedianFilter, ImageFilter.MaxFilter) ) -def test_rankfilter_error(filter) -> None: +def test_rankfilter_error(filter: ImageFilter.RankFilter) -> None: with pytest.raises(ValueError): im = Image.new("P", (3, 3), None) im.putdata(list(range(9))) @@ -137,7 +146,7 @@ def test_kernel_not_enough_coefficients() -> None: @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_3x3(mode) -> None: +def test_consistency_3x3(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -163,7 +172,7 @@ def test_consistency_3x3(mode) -> None: @pytest.mark.parametrize("mode", ("L", "LA", "I", "RGB", "CMYK")) -def test_consistency_5x5(mode) -> None: +def test_consistency_5x5(mode: str) -> None: with Image.open("Tests/images/hopper.bmp") as source: reference_name = "hopper_emboss_more" reference_name += "_I.png" if mode == "I" else ".bmp" @@ -199,7 +208,7 @@ def test_consistency_5x5(mode) -> None: (2, -2), ), ) -def test_invalid_box_blur_filter(radius) -> None: +def test_invalid_box_blur_filter(radius: int | tuple[int, int]) -> None: with pytest.raises(ValueError): ImageFilter.BoxBlur(radius) diff --git a/Tests/test_image_getextrema.py b/Tests/test_image_getextrema.py index 0107fdcc4..a5b974459 100644 --- a/Tests/test_image_getextrema.py +++ b/Tests/test_image_getextrema.py @@ -6,7 +6,7 @@ from .helper import hopper def test_extrema() -> None: - def extrema(mode): + def extrema(mode: str) -> tuple[int, int] | tuple[tuple[int, int], ...]: return hopper(mode).getextrema() assert extrema("1") == (0, 255) diff --git a/Tests/test_image_getpalette.py b/Tests/test_image_getpalette.py index e7304c98f..6a8f157fc 100644 --- a/Tests/test_image_getpalette.py +++ b/Tests/test_image_getpalette.py @@ -6,7 +6,7 @@ from .helper import hopper def test_palette() -> None: - def palette(mode): + def palette(mode: str) -> list[int] | None: p = hopper(mode).getpalette() if p: return p[:10] diff --git a/Tests/test_image_paste.py b/Tests/test_image_paste.py index c4d7a5dd2..ce7345572 100644 --- a/Tests/test_image_paste.py +++ b/Tests/test_image_paste.py @@ -46,7 +46,7 @@ class TestImagingPaste: self.assert_9points_image(im, expected) @CachedProperty - def mask_1(self): + def mask_1(self) -> Image.Image: mask = Image.new("1", (self.size, self.size)) px = mask.load() for y in range(mask.height): @@ -55,11 +55,11 @@ class TestImagingPaste: return mask @CachedProperty - def mask_L(self): + def mask_L(self) -> Image.Image: return self.gradient_L.transpose(Image.Transpose.ROTATE_270) @CachedProperty - def gradient_L(self): + def gradient_L(self) -> Image.Image: gradient = Image.new("L", (self.size, self.size)) px = gradient.load() for y in range(gradient.height): @@ -68,7 +68,7 @@ class TestImagingPaste: return gradient @CachedProperty - def gradient_RGB(self): + def gradient_RGB(self) -> Image.Image: return Image.merge( "RGB", [ @@ -79,7 +79,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_LA(self): + def gradient_LA(self) -> Image.Image: return Image.merge( "LA", [ @@ -89,7 +89,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_RGBA(self): + def gradient_RGBA(self) -> Image.Image: return Image.merge( "RGBA", [ @@ -101,7 +101,7 @@ class TestImagingPaste: ) @CachedProperty - def gradient_RGBa(self): + def gradient_RGBa(self) -> Image.Image: return Image.merge( "RGBa", [ diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py index 103019916..73145faac 100644 --- a/Tests/test_image_putdata.py +++ b/Tests/test_image_putdata.py @@ -31,7 +31,7 @@ def test_sanity() -> None: def test_long_integers() -> None: # see bug-200802-systemerror - def put(value): + def put(value: int) -> tuple[int, int, int, int]: im = Image.new("RGBA", (1, 1)) im.putdata([value]) return im.getpixel((0, 0)) @@ -58,7 +58,7 @@ def test_mode_with_L_with_float() -> None: @pytest.mark.parametrize("mode", ("I", "I;16", "I;16L", "I;16B")) -def test_mode_i(mode) -> None: +def test_mode_i(mode: str) -> None: src = hopper("L") data = list(src.getdata()) im = Image.new(mode, src.size, 0) @@ -79,7 +79,7 @@ def test_mode_F() -> None: @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24")) -def test_mode_BGR(mode) -> None: +def test_mode_BGR(mode: str) -> None: data = [(16, 32, 49), (32, 32, 98)] im = Image.new(mode, (1, 2)) im.putdata(data) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index ffe2551d2..cc7cf58f0 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -8,7 +8,7 @@ from .helper import assert_image_equal, assert_image_equal_tofile, hopper def test_putpalette() -> None: - def palette(mode): + def palette(mode: str) -> str | tuple[str, list[int]]: im = hopper(mode).copy() im.putpalette(list(range(256)) * 3) p = im.getpalette() @@ -81,7 +81,7 @@ def test_putpalette_with_alpha_values() -> None: ("RGBAX", (1, 2, 3, 4, 0)), ), ) -def test_rgba_palette(mode, palette) -> None: +def test_rgba_palette(mode: str, palette: tuple[int, ...]) -> None: im = Image.new("P", (1, 1)) im.putpalette(palette, mode) assert im.getpalette() == [1, 2, 3] diff --git a/Tests/test_image_resample.py b/Tests/test_image_resample.py index f3ec12c05..7090ff9cd 100644 --- a/Tests/test_image_resample.py +++ b/Tests/test_image_resample.py @@ -231,11 +231,13 @@ class TestImagingCoreResampleAccuracy: class TestCoreResampleConsistency: - def make_case(self, mode: str, fill: tuple[int, int, int] | float): + def make_case( + self, mode: str, fill: tuple[int, int, int] | float + ) -> tuple[Image.Image, tuple[int, ...]]: im = Image.new(mode, (512, 9), fill) return im.resize((9, 512), Image.Resampling.LANCZOS), im.load()[0, 0] - def run_case(self, case) -> None: + def run_case(self, case: tuple[Image.Image, Image.Image]) -> None: channel, color = case px = channel.load() for x in range(channel.size[0]): @@ -353,7 +355,7 @@ class TestCoreResampleAlphaCorrect: class TestCoreResamplePasses: @contextmanager - def count(self, diff): + def count(self, diff: int) -> Generator[None, None, None]: count = Image.core.get_stats()["new_count"] yield assert Image.core.get_stats()["new_count"] - count == diff diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 51e0f5854..c10c96da6 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -12,7 +12,13 @@ from .helper import ( ) -def rotate(im, mode, angle, center=None, translate=None) -> None: +def rotate( + im: Image.Image, + mode: str, + angle: int, + center: tuple[int, int] | None = None, + translate: tuple[int, int] | None = None, +) -> None: out = im.rotate(angle, center=center, translate=translate) assert out.mode == mode assert out.size == im.size # default rotate clips output @@ -27,13 +33,13 @@ def rotate(im, mode, angle, center=None, translate=None) -> None: @pytest.mark.parametrize("mode", ("1", "P", "L", "RGB", "I", "F")) -def test_mode(mode) -> None: +def test_mode(mode: str) -> None: im = hopper(mode) rotate(im, mode, 45) @pytest.mark.parametrize("angle", (0, 90, 180, 270)) -def test_angle(angle) -> None: +def test_angle(angle: int) -> None: with Image.open("Tests/images/test-card.png") as im: rotate(im, im.mode, angle) @@ -42,7 +48,7 @@ def test_angle(angle) -> None: @pytest.mark.parametrize("angle", (0, 45, 90, 180, 270)) -def test_zero(angle) -> None: +def test_zero(angle: int) -> None: im = Image.new("RGB", (0, 0)) rotate(im, im.mode, angle) diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index 6aeeea2ed..2ca1d2cfc 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -111,7 +111,7 @@ def test_load_first_unless_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: draft = im.draft - def im_draft(mode, size): + def im_draft(mode: str, size: tuple[int, int]): result = draft(mode, size) assert result is not None diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 1067dd563..638d12247 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -1,6 +1,7 @@ from __future__ import annotations import math +from typing import Callable import pytest @@ -91,7 +92,7 @@ class TestImageTransform: ("LA", (76, 0)), ), ) - def test_fill(self, mode, expected_pixel) -> None: + def test_fill(self, mode: str, expected_pixel: tuple[int, ...]) -> None: im = hopper(mode) (w, h) = im.size transformed = im.transform( @@ -142,7 +143,9 @@ class TestImageTransform: assert_image_equal(blank, transformed.crop((w // 2, 0, w, h // 2))) assert_image_equal(blank, transformed.crop((0, h // 2, w // 2, h))) - def _test_alpha_premult(self, op) -> None: + def _test_alpha_premult( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image] + ) -> None: # create image with half white, half black, # with the black half transparent. # do op, @@ -159,13 +162,13 @@ class TestImageTransform: assert 40 * 10 == hist[-1] def test_alpha_premult_resize(self) -> None: - def op(im, sz): + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.BILINEAR) self._test_alpha_premult(op) def test_alpha_premult_transform(self) -> None: - def op(im, sz): + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.BILINEAR @@ -173,7 +176,9 @@ class TestImageTransform: self._test_alpha_premult(op) - def _test_nearest(self, op, mode) -> None: + def _test_nearest( + self, op: Callable[[Image.Image, tuple[int, int]], Image.Image], mode: str + ) -> None: # create white image with half transparent, # do op, # the image should remain white with half transparent @@ -196,15 +201,15 @@ class TestImageTransform: ) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_resize(self, mode) -> None: - def op(im, sz): + def test_nearest_resize(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: return im.resize(sz, Image.Resampling.NEAREST) self._test_nearest(op, mode) @pytest.mark.parametrize("mode", ("RGBA", "LA")) - def test_nearest_transform(self, mode) -> None: - def op(im, sz): + def test_nearest_transform(self, mode: str) -> None: + def op(im: Image.Image, sz: tuple[int, int]) -> Image.Image: (w, h) = im.size return im.transform( sz, Image.Transform.EXTENT, (0, 0, w, h), Image.Resampling.NEAREST @@ -227,7 +232,9 @@ class TestImageTransform: # Running by default, but I'd totally understand not doing it in # the future - pattern = [Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65)] + pattern: list[Image.Image] | None = [ + Image.new("RGBA", (1024, 1024), (a, a, a, a)) for a in range(1, 65) + ] # Yeah. Watch some JIT optimize this out. pattern = None # noqa: F841 @@ -240,7 +247,7 @@ class TestImageTransform: im.transform((100, 100), None) @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) - def test_unknown_resampling_filter(self, resample) -> None: + def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): @@ -250,7 +257,7 @@ class TestImageTransform: class TestImageTransformAffine: transform = Image.Transform.AFFINE - def _test_image(self): + def _test_image(self) -> Image.Image: im = hopper("RGB") return im.crop((10, 20, im.width - 10, im.height - 20)) @@ -263,7 +270,7 @@ class TestImageTransformAffine: (270, Image.Transpose.ROTATE_270), ), ) - def test_rotate(self, deg, transpose) -> None: + def test_rotate(self, deg: int, transpose: Image.Transpose | None) -> None: im = self._test_image() angle = -math.radians(deg) @@ -313,7 +320,13 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_resize(self, scale, epsilon_scale, resample, epsilon) -> None: + def test_resize( + self, + scale: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: int, + ) -> None: im = self._test_image() size_up = int(round(im.width * scale)), int(round(im.height * scale)) @@ -342,7 +355,14 @@ class TestImageTransformAffine: (Image.Resampling.BICUBIC, 1), ), ) - def test_translate(self, x, y, epsilon_scale, resample, epsilon) -> None: + def test_translate( + self, + x: float, + y: float, + epsilon_scale: float, + resample: Image.Resampling, + epsilon: float, + ) -> None: im = self._test_image() size_up = int(round(im.width + x)), int(round(im.height + y)) From ea0240bf2d414ff297c7959f904c252bae696ff3 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Mon, 12 Feb 2024 19:12:08 +1100 Subject: [PATCH 29/29] Use is None Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Tests/test_image_access.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py index 3bdaea750..380b89de8 100644 --- a/Tests/test_image_access.py +++ b/Tests/test_image_access.py @@ -147,7 +147,7 @@ class TestImageGetPixel(AccessTest): pytest.skip("Support not added to deprecated module for BGR;* modes") expected_color = ( - expected_color_int if expected_color_int is not None else self.color(mode) + self.color(mode) if expected_color_int is None else expected_color_int ) # check putpixel