Updated type hints

This commit is contained in:
Andrew Murray 2024-06-22 10:09:11 +10:00
parent 4b258be3bb
commit cc83cc8ec8
27 changed files with 98 additions and 76 deletions

View File

@ -18,7 +18,7 @@ from typing import Any, Callable, Sequence
import pytest import pytest
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
from PIL import Image, ImageMath, features from PIL import Image, ImageFile, ImageMath, features
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -240,7 +240,7 @@ class PillowLeakTestCase:
# helpers # helpers
def fromstring(data: bytes) -> Image.Image: def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data)) return Image.open(BytesIO(data))

View File

@ -1033,8 +1033,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None: def test_repr_jpeg(self) -> None:
im = hopper() im = hopper()
b = im._repr_jpeg_()
assert b is not None
with Image.open(BytesIO(im._repr_jpeg_())) as repr_jpeg: with Image.open(BytesIO(b)) as repr_jpeg:
assert repr_jpeg.format == "JPEG" assert repr_jpeg.format == "JPEG"
assert_image_similar(im, repr_jpeg, 17) assert_image_similar(im, repr_jpeg, 17)

View File

@ -535,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None: def test_repr_png(self) -> None:
im = hopper() im = hopper()
b = im._repr_png_()
assert b is not None
with Image.open(BytesIO(im._repr_png_())) as repr_png: with Image.open(BytesIO(b)) as repr_png:
assert repr_png.format == "PNG" assert repr_png.format == "PNG"
assert_image_equal(im, repr_png) assert_image_equal(im, repr_png)
@ -768,14 +770,10 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None: def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: class MyStdOut:
buffer = BytesIO()
class MyStdOut: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout sys.stdout = mystdout
@ -785,7 +783,7 @@ class TestFilePng:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_PNG_FILE) assert_image_equal_tofile(reloaded, TEST_PNG_FILE)

View File

@ -368,14 +368,10 @@ def test_mimetypes(tmp_path: Path) -> None:
def test_save_stdout(buffer: bool) -> None: def test_save_stdout(buffer: bool) -> None:
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: class MyStdOut:
buffer = BytesIO()
class MyStdOut: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout sys.stdout = mystdout
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded: with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE) assert_image_equal_tofile(reloaded, TEST_FILE)

View File

@ -120,7 +120,7 @@ class TestFileTiff:
def test_set_legacy_api(self) -> None: def test_set_legacy_api(self) -> None:
ifd = TiffImagePlugin.ImageFileDirectory_v2() ifd = TiffImagePlugin.ImageFileDirectory_v2()
with pytest.raises(Exception) as e: with pytest.raises(Exception) as e:
ifd.legacy_api = None ifd.legacy_api = False
assert str(e.value) == "Not allowing setting of legacy api" assert str(e.value) == "Not allowing setting of legacy api"
def test_xyres_tiff(self) -> None: def test_xyres_tiff(self) -> None:

View File

@ -34,7 +34,7 @@ class TestDefaultFontLeak(TestTTypeFontLeak):
def test_leak(self) -> None: def test_leak(self) -> None:
if features.check_module("freetype2"): if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError) ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
try: try:
default_font = ImageFont.load_default() default_font = ImageFont.load_default()
finally: finally:

View File

@ -393,13 +393,13 @@ class TestImage:
# errors # errors
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, "invalid source") source.alpha_composite(over, "invalid destination") # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), "invalid destination") source.alpha_composite(over, (0, 0), "invalid source") # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, 0) source.alpha_composite(over, 0) # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), 0) source.alpha_composite(over, (0, 0), 0) # type: ignore[arg-type]
with pytest.raises(ValueError): with pytest.raises(ValueError):
source.alpha_composite(over, (0, 0), (0, -1)) source.alpha_composite(over, (0, 0), (0, -1))

View File

@ -16,7 +16,9 @@ def draft_roundtrip(
im = Image.new(in_mode, in_size) im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG") data = tostring(im, "JPEG")
im = fromstring(data) im = fromstring(data)
mode, box = im.draft(req_mode, req_size) result = im.draft(req_mode, req_size)
assert result is not None
box = result[1]
scale, _ = im.decoderconfig scale, _ = im.decoderconfig
assert box[:2] == (0, 0) assert box[:2] == (0, 0)
assert (im.width - scale) < box[2] <= im.width assert (im.width - scale) < box[2] <= im.width

View File

@ -338,3 +338,8 @@ class TestImagingPaste:
im.copy().paste(im2) im.copy().paste(im2)
im.copy().paste(im2, (0, 0)) im.copy().paste(im2, (0, 0))
def test_incorrect_abbreviated_form(self) -> None:
im = Image.new("L", (1, 1))
with pytest.raises(ValueError):
im.paste(im, im, im)

View File

@ -61,4 +61,4 @@ def test_f_lut() -> None:
def test_f_mode() -> None: def test_f_mode() -> None:
im = hopper("F") im = hopper("F")
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.point(None) im.point([])

View File

@ -113,13 +113,13 @@ def test_array_F() -> None:
def test_not_flattened() -> None: def test_not_flattened() -> None:
im = Image.new("L", (1, 1)) im = Image.new("L", (1, 1))
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.putdata([[0]]) # type: ignore[list-item] im.putdata([[0]])
with pytest.raises(TypeError): with pytest.raises(TypeError):
im.putdata([[0]], 2) # type: ignore[list-item] im.putdata([[0]], 2)
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = Image.new("I", (1, 1)) im = Image.new("I", (1, 1))
im.putdata([[0]]) # type: ignore[list-item] im.putdata([[0]])
with pytest.raises(TypeError): with pytest.raises(TypeError):
im = Image.new("F", (1, 1)) im = Image.new("F", (1, 1))
im.putdata([[0]]) # type: ignore[list-item] im.putdata([[0]])

View File

@ -445,7 +445,7 @@ class TestCoreResampleBox:
im.resize((32, 32), resample, (20, 20, 100, 20)) im.resize((32, 32), resample, (20, 20, 100, 20))
with pytest.raises(TypeError, match="must be sequence of length 4"): with pytest.raises(TypeError, match="must be sequence of length 4"):
im.resize((32, 32), resample, (im.width, im.height)) im.resize((32, 32), resample, (im.width, im.height)) # type: ignore[arg-type]
with pytest.raises(ValueError, match="can't be negative"): with pytest.raises(ValueError, match="can't be negative"):
im.resize((32, 32), resample, (-20, 20, 100, 100)) im.resize((32, 32), resample, (-20, 20, 100, 100))

View File

@ -103,7 +103,7 @@ def test_sanity() -> None:
def test_flags() -> None: def test_flags() -> None:
assert ImageCms.Flags.NONE == 0 assert ImageCms.Flags.NONE.value == 0
assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(0) == ImageCms.Flags.NONE
assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE assert ImageCms.Flags.GRIDPOINTS(256) == ImageCms.Flags.NONE
@ -569,9 +569,9 @@ def assert_aux_channel_preserved(
for delta in nine_grid_deltas: for delta in nine_grid_deltas:
channel_data.paste( channel_data.paste(
channel_pattern, channel_pattern,
tuple( (
paste_offset[c] + delta[c] * channel_pattern.size[c] paste_offset[0] + delta[0] * channel_pattern.size[0],
for c in range(2) paste_offset[1] + delta[1] * channel_pattern.size[1],
), ),
) )
chans.append(channel_data) chans.append(channel_data)
@ -642,7 +642,8 @@ def test_auxiliary_channels_isolated() -> None:
# convert with and without AUX data, test colors are equal # convert with and without AUX data, test colors are equal
src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1]) src_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], src_format[1])
source_profile = ImageCms.createProfile(src_colorSpace) source_profile = ImageCms.createProfile(src_colorSpace)
destination_profile = ImageCms.createProfile(dst_format[1]) dst_colorSpace = cast(Literal["LAB", "XYZ", "sRGB"], dst_format[1])
destination_profile = ImageCms.createProfile(dst_colorSpace)
source_image = src_format[3] source_image = src_format[3]
test_transform = ImageCms.buildTransform( test_transform = ImageCms.buildTransform(
source_profile, source_profile,

View File

@ -1581,7 +1581,7 @@ def test_compute_regular_polygon_vertices_input_error_handling(
error_message: str, error_message: str,
) -> None: ) -> None:
with pytest.raises(expected_error) as e: with pytest.raises(expected_error) as e:
ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type]
assert str(e.value) == error_message assert str(e.value) == error_message

View File

@ -51,9 +51,10 @@ def test_sanity() -> None:
pen = ImageDraw2.Pen("blue", width=7) pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen) draw.line(list(range(10)), pen)
draw, handler = ImageDraw.getdraw(im) draw2, handler = ImageDraw.getdraw(im)
assert draw2 is not None
pen = ImageDraw2.Pen("blue", width=7) pen = ImageDraw2.Pen("blue", width=7)
draw.line(list(range(10)), pen) draw2.line(list(range(10)), pen)
@pytest.mark.parametrize("bbox", BBOX) @pytest.mark.parametrize("bbox", BBOX)

View File

@ -381,7 +381,7 @@ class TestPyEncoder(CodecsTest):
def test_encode(self) -> None: def test_encode(self) -> None:
encoder = ImageFile.PyEncoder(None) encoder = ImageFile.PyEncoder(None)
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode(None) encoder.encode(0)
bytes_consumed, errcode = encoder.encode_to_pyfd() bytes_consumed, errcode = encoder.encode_to_pyfd()
assert bytes_consumed == 0 assert bytes_consumed == 0

View File

@ -209,7 +209,7 @@ def test_getlength(
assert length == length_raqm assert length == length_raqm
def test_float_size() -> None: def test_float_size(layout_engine: ImageFont.Layout) -> None:
lengths = [] lengths = []
for size in (48, 48.5, 49): for size in (48, 48.5, 49):
f = ImageFont.truetype( f = ImageFont.truetype(
@ -494,8 +494,8 @@ def test_default_font() -> None:
assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png") assert_image_equal_tofile(im, "Tests/images/default_font_freetype.png")
@pytest.mark.parametrize("mode", (None, "1", "RGBA")) @pytest.mark.parametrize("mode", ("", "1", "RGBA"))
def test_getbbox(font: ImageFont.FreeTypeFont, mode: str | None) -> None: def test_getbbox(font: ImageFont.FreeTypeFont, mode: str) -> None:
assert (0, 4, 12, 16) == font.getbbox("A", mode) assert (0, 4, 12, 16) == font.getbbox("A", mode)

View File

@ -14,7 +14,7 @@ original_core = ImageFont.core
def setup_module() -> None: def setup_module() -> None:
if features.check_module("freetype2"): if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError) ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
def teardown_module() -> None: def teardown_module() -> None:
@ -76,3 +76,8 @@ def test_oom() -> None:
font = ImageFont.ImageFont() font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (1, 1))) font._load_pilfont_data(fp, Image.new("L", (1, 1)))
font.getmask("A" * 1_000_000) font.getmask("A" * 1_000_000)
def test_freetypefont_without_freetype() -> None:
with pytest.raises(ImportError):
ImageFont.truetype("Tests/fonts/FreeMono.ttf")

View File

@ -45,7 +45,7 @@ def test_getcolor() -> None:
# Test unknown color specifier # Test unknown color specifier
with pytest.raises(ValueError): with pytest.raises(ValueError):
palette.getcolor("unknown") palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba_color_rgb_palette() -> None: def test_getcolor_rgba_color_rgb_palette() -> None:

View File

@ -54,14 +54,10 @@ def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout # Temporarily redirect stdout
old_stdout = sys.stdout old_stdout = sys.stdout
if buffer: class MyStdOut:
buffer = BytesIO()
class MyStdOut: mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
sys.stdout = mystdout sys.stdout = mystdout
@ -71,6 +67,6 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if buffer: if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer mystdout = mystdout.buffer
assert mystdout.getvalue() != b"" assert mystdout.getvalue() != b""

View File

@ -1168,7 +1168,7 @@ class Image:
def quantize( def quantize(
self, self,
colors: int = 256, colors: int = 256,
method: Quantize | None = None, method: int | None = None,
kmeans: int = 0, kmeans: int = 0,
palette=None, palette=None,
dither: Dither = Dither.FLOYDSTEINBERG, dither: Dither = Dither.FLOYDSTEINBERG,
@ -1309,7 +1309,7 @@ class Image:
return im.crop((x0, y0, x1, y1)) return im.crop((x0, y0, x1, y1))
def draft( def draft(
self, mode: str | None, size: tuple[int, int] self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None: ) -> tuple[str, tuple[int, int, float, float]] | None:
""" """
Configures the image file loader so it returns a version of the Configures the image file loader so it returns a version of the
@ -1744,7 +1744,7 @@ class Image:
def paste( def paste(
self, self,
im: Image | str | float | tuple[float, ...], im: Image | str | float | tuple[float, ...],
box: tuple[int, int, int, int] | tuple[int, int] | None = None, box: Image | tuple[int, int, int, int] | tuple[int, int] | None = None,
mask: Image | None = None, mask: Image | None = None,
) -> None: ) -> None:
""" """
@ -1786,10 +1786,14 @@ class Image:
:param mask: An optional mask image. :param mask: An optional mask image.
""" """
if isImageType(box) and mask is None: if isImageType(box):
if mask is not None:
msg = "If using second argument as mask, third argument must be None"
raise ValueError(msg)
# abbreviated paste(im, mask) syntax # abbreviated paste(im, mask) syntax
mask = box mask = box
box = None box = None
assert not isinstance(box, Image)
if box is None: if box is None:
box = (0, 0) box = (0, 0)
@ -1995,7 +1999,10 @@ class Image:
self.im.putband(alpha.im, band) self.im.putband(alpha.im, band)
def putdata( def putdata(
self, data: Sequence[float], scale: float = 1.0, offset: float = 0.0 self,
data: Sequence[float] | Sequence[Sequence[int]],
scale: float = 1.0,
offset: float = 0.0,
) -> None: ) -> None:
""" """
Copies pixel data from a flattened sequence object into the image. The Copies pixel data from a flattened sequence object into the image. The
@ -2656,7 +2663,7 @@ class Image:
self, self,
size: tuple[float, float], size: tuple[float, float],
resample: Resampling = Resampling.BICUBIC, resample: Resampling = Resampling.BICUBIC,
reducing_gap: float = 2.0, reducing_gap: float | None = 2.0,
) -> None: ) -> None:
""" """
Make this image into a thumbnail. This method modifies the Make this image into a thumbnail. This method modifies the
@ -2717,11 +2724,12 @@ class Image:
return x, y return x, y
box = None box = None
final_size: tuple[int, int]
if reducing_gap is not None: if reducing_gap is not None:
preserved_size = preserve_aspect_ratio() preserved_size = preserve_aspect_ratio()
if preserved_size is None: if preserved_size is None:
return return
size = preserved_size final_size = preserved_size
res = self.draft( res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap)) None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
@ -2735,13 +2743,13 @@ class Image:
preserved_size = preserve_aspect_ratio() preserved_size = preserve_aspect_ratio()
if preserved_size is None: if preserved_size is None:
return return
size = preserved_size final_size = preserved_size
if self.size != size: if self.size != final_size:
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap) im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
self.im = im.im self.im = im.im
self._size = size self._size = final_size
self._mode = self.im.mode self._mode = self.im.mode
self.readonly = 0 self.readonly = 0

View File

@ -62,7 +62,9 @@ directly.
class ImageDraw: class ImageDraw:
font = None font: (
ImageFont.ImageFont | ImageFont.FreeTypeFont | ImageFont.TransposedFont | None
) = None
def __init__(self, im: Image.Image, mode: str | None = None) -> None: def __init__(self, im: Image.Image, mode: str | None = None) -> None:
""" """

View File

@ -33,11 +33,12 @@ import sys
import warnings import warnings
from enum import IntEnum from enum import IntEnum
from io import BytesIO from io import BytesIO
from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, BinaryIO from typing import IO, TYPE_CHECKING, Any, BinaryIO
from . import Image from . import Image
from ._typing import StrOrBytesPath from ._typing import StrOrBytesPath
from ._util import is_path from ._util import DeferredError, is_path
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ImageFile from . import ImageFile
@ -53,11 +54,10 @@ class Layout(IntEnum):
MAX_STRING_LENGTH = 1_000_000 MAX_STRING_LENGTH = 1_000_000
core: ModuleType | DeferredError
try: try:
from . import _imagingft as core from . import _imagingft as core
except ImportError as ex: except ImportError as ex:
from ._util import DeferredError
core = DeferredError.new(ex) core = DeferredError.new(ex)
@ -199,6 +199,7 @@ class FreeTypeFont:
"""FreeType font wrapper (requires _imagingft service)""" """FreeType font wrapper (requires _imagingft service)"""
font: Font font: Font
font_bytes: bytes
def __init__( def __init__(
self, self,
@ -210,6 +211,9 @@ class FreeTypeFont:
) -> None: ) -> None:
# FIXME: use service provider instead # FIXME: use service provider instead
if isinstance(core, DeferredError):
raise core.ex
if size <= 0: if size <= 0:
msg = "font size must be greater than 0" msg = "font size must be greater than 0"
raise ValueError(msg) raise ValueError(msg)
@ -903,7 +907,7 @@ def load_default(size: float | None = None) -> FreeTypeFont | ImageFont:
:return: A font object. :return: A font object.
""" """
f: FreeTypeFont | ImageFont f: FreeTypeFont | ImageFont
if core.__class__.__name__ == "module" or size is not None: if isinstance(core, ModuleType) or size is not None:
f = truetype( f = truetype(
BytesIO( BytesIO(
base64.b64decode( base64.b64decode(

View File

@ -196,7 +196,7 @@ if qt_is_installed:
self.setColorTable(im_data["colortable"]) self.setColorTable(im_data["colortable"])
def toqimage(im): def toqimage(im) -> ImageQt:
return ImageQt(im) return ImageQt(im)

View File

@ -24,7 +24,7 @@ class Transform(Image.ImageTransformHandler):
method: Image.Transform method: Image.Transform
def __init__(self, data: Sequence[int]) -> None: def __init__(self, data: Sequence[Any]) -> None:
self.data = data self.data = data
def getdata(self) -> tuple[Image.Transform, Sequence[int]]: def getdata(self) -> tuple[Image.Transform, Sequence[int]]:

View File

@ -428,7 +428,7 @@ class JpegImageFile(ImageFile.ImageFile):
return s return s
def draft( def draft(
self, mode: str | None, size: tuple[int, int] self, mode: str | None, size: tuple[int, int] | None
) -> tuple[str, tuple[int, int, float, float]] | None: ) -> tuple[str, tuple[int, int, float, float]] | None:
if len(self.tile) != 1: if len(self.tile) != 1:
return None return None

View File

@ -89,7 +89,7 @@ DOUBLE = 12
IFD = 13 IFD = 13
LONG8 = 16 LONG8 = 16
TAGS_V2 = { _tags_v2 = {
254: ("NewSubfileType", LONG, 1), 254: ("NewSubfileType", LONG, 1),
255: ("SubfileType", SHORT, 1), 255: ("SubfileType", SHORT, 1),
256: ("ImageWidth", LONG, 1), 256: ("ImageWidth", LONG, 1),
@ -425,9 +425,11 @@ TAGS = {
50784: "Alias Layer Metadata", 50784: "Alias Layer Metadata",
} }
TAGS_V2: dict[int, TagInfo] = {}
def _populate(): def _populate():
for k, v in TAGS_V2.items(): for k, v in _tags_v2.items():
# Populate legacy structure. # Populate legacy structure.
TAGS[k] = v[0] TAGS[k] = v[0]
if len(v) == 4: if len(v) == 4: