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
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__)
@ -240,7 +240,7 @@ class PillowLeakTestCase:
# helpers
def fromstring(data: bytes) -> Image.Image:
def fromstring(data: bytes) -> ImageFile.ImageFile:
return Image.open(BytesIO(data))

View File

@ -1033,8 +1033,10 @@ class TestFileJpeg:
def test_repr_jpeg(self) -> None:
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_image_similar(im, repr_jpeg, 17)

View File

@ -535,8 +535,10 @@ class TestFilePng:
def test_repr_png(self) -> None:
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_image_equal(im, repr_png)
@ -768,14 +770,10 @@ class TestFilePng:
def test_save_stdout(self, buffer: bool) -> None:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
@ -785,7 +783,7 @@ class TestFilePng:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
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:
old_stdout = sys.stdout
if buffer:
class MyStdOut:
buffer = BytesIO()
class MyStdOut:
buffer = BytesIO()
mystdout = MyStdOut()
else:
mystdout = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO()
sys.stdout = mystdout
@ -385,7 +381,7 @@ def test_save_stdout(buffer: bool) -> None:
# Reset stdout
sys.stdout = old_stdout
if buffer:
if isinstance(mystdout, MyStdOut):
mystdout = mystdout.buffer
with Image.open(mystdout) as reloaded:
assert_image_equal_tofile(reloaded, TEST_FILE)

View File

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

View File

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

View File

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

View File

@ -16,7 +16,9 @@ def draft_roundtrip(
im = Image.new(in_mode, in_size)
data = tostring(im, "JPEG")
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
assert box[:2] == (0, 0)
assert (im.width - scale) < box[2] <= im.width

View File

@ -338,3 +338,8 @@ class TestImagingPaste:
im.copy().paste(im2)
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:
im = hopper("F")
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:
im = Image.new("L", (1, 1))
with pytest.raises(TypeError):
im.putdata([[0]]) # type: ignore[list-item]
im.putdata([[0]])
with pytest.raises(TypeError):
im.putdata([[0]], 2) # type: ignore[list-item]
im.putdata([[0]], 2)
with pytest.raises(TypeError):
im = Image.new("I", (1, 1))
im.putdata([[0]]) # type: ignore[list-item]
im.putdata([[0]])
with pytest.raises(TypeError):
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))
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"):
im.resize((32, 32), resample, (-20, 20, 100, 100))

View File

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

View File

@ -1581,7 +1581,7 @@ def test_compute_regular_polygon_vertices_input_error_handling(
error_message: str,
) -> None:
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

View File

@ -51,9 +51,10 @@ def test_sanity() -> None:
pen = ImageDraw2.Pen("blue", width=7)
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)
draw.line(list(range(10)), pen)
draw2.line(list(range(10)), pen)
@pytest.mark.parametrize("bbox", BBOX)

View File

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

View File

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

View File

@ -14,7 +14,7 @@ original_core = ImageFont.core
def setup_module() -> None:
if features.check_module("freetype2"):
ImageFont.core = _util.DeferredError(ImportError)
ImageFont.core = _util.DeferredError(ImportError("Disabled for testing"))
def teardown_module() -> None:
@ -76,3 +76,8 @@ def test_oom() -> None:
font = ImageFont.ImageFont()
font._load_pilfont_data(fp, Image.new("L", (1, 1)))
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
with pytest.raises(ValueError):
palette.getcolor("unknown")
palette.getcolor("unknown") # type: ignore[arg-type]
def test_getcolor_rgba_color_rgb_palette() -> None:

View File

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

View File

@ -1168,7 +1168,7 @@ class Image:
def quantize(
self,
colors: int = 256,
method: Quantize | None = None,
method: int | None = None,
kmeans: int = 0,
palette=None,
dither: Dither = Dither.FLOYDSTEINBERG,
@ -1309,7 +1309,7 @@ class Image:
return im.crop((x0, y0, x1, y1))
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:
"""
Configures the image file loader so it returns a version of the
@ -1744,7 +1744,7 @@ class Image:
def paste(
self,
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,
) -> None:
"""
@ -1786,10 +1786,14 @@ class 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
mask = box
box = None
assert not isinstance(box, Image)
if box is None:
box = (0, 0)
@ -1995,7 +1999,10 @@ class Image:
self.im.putband(alpha.im, band)
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:
"""
Copies pixel data from a flattened sequence object into the image. The
@ -2656,7 +2663,7 @@ class Image:
self,
size: tuple[float, float],
resample: Resampling = Resampling.BICUBIC,
reducing_gap: float = 2.0,
reducing_gap: float | None = 2.0,
) -> None:
"""
Make this image into a thumbnail. This method modifies the
@ -2717,11 +2724,12 @@ class Image:
return x, y
box = None
final_size: tuple[int, int]
if reducing_gap is not None:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
size = preserved_size
final_size = preserved_size
res = self.draft(
None, (int(size[0] * reducing_gap), int(size[1] * reducing_gap))
@ -2735,13 +2743,13 @@ class Image:
preserved_size = preserve_aspect_ratio()
if preserved_size is None:
return
size = preserved_size
final_size = preserved_size
if self.size != size:
im = self.resize(size, resample, box=box, reducing_gap=reducing_gap)
if self.size != final_size:
im = self.resize(final_size, resample, box=box, reducing_gap=reducing_gap)
self.im = im.im
self._size = size
self._size = final_size
self._mode = self.im.mode
self.readonly = 0

View File

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

View File

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

View File

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

View File

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

View File

@ -428,7 +428,7 @@ class JpegImageFile(ImageFile.ImageFile):
return s
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:
if len(self.tile) != 1:
return None

View File

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