diff --git a/Tests/helper.py b/Tests/helper.py index d6a93a803..577e32d37 100644 --- a/Tests/helper.py +++ b/Tests/helper.py @@ -98,6 +98,7 @@ def assert_image_equal(a: Image.Image, b: Image.Image, msg: str | None = None) - def assert_image_equal_tofile( a: Image.Image, filename: str, msg: str | None = None, mode: str | None = None ) -> None: + img: Image.Image with Image.open(filename) as img: if mode: img = img.convert(mode) @@ -142,6 +143,7 @@ def assert_image_similar_tofile( msg: str | None = None, mode: str | None = None, ) -> None: + img: Image.Image with Image.open(filename) as img: if mode: img = img.convert(mode) diff --git a/Tests/test_bmp_reference.py b/Tests/test_bmp_reference.py index 7f8487921..3c57ff8aa 100644 --- a/Tests/test_bmp_reference.py +++ b/Tests/test_bmp_reference.py @@ -92,8 +92,11 @@ def test_good() -> None: for f in get_files("g"): try: + im: Image.Image with Image.open(f) as im: im.load() + + compare: Image.Image with Image.open(get_compare(f)) as compare: compare.load() if im.mode == "P": diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py index e95850212..1e11554c8 100644 --- a/Tests/test_file_apng.py +++ b/Tests/test_file_apng.py @@ -12,6 +12,7 @@ from PIL import Image, ImageSequence, PngImagePlugin # (referenced from https://wiki.mozilla.org/APNG_Specification) def test_apng_basic() -> None: with Image.open("Tests/images/apng/single_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated assert im.n_frames == 1 assert im.get_format_mimetype() == "image/apng" @@ -20,6 +21,7 @@ def test_apng_basic() -> None: assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/single_frame_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.is_animated assert im.n_frames == 2 assert im.get_format_mimetype() == "image/apng" @@ -49,6 +51,7 @@ def test_apng_basic() -> None: ) def test_apng_fdat(filename: str) -> None: with Image.open(filename) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -56,31 +59,37 @@ def test_apng_fdat(filename: str) -> None: def test_apng_dispose() -> None: with Image.open("Tests/images/apng/dispose_op_none.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_final.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_previous_first.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) @@ -88,21 +97,25 @@ def test_apng_dispose() -> None: def test_apng_dispose_region() -> None: with Image.open("Tests/images/apng/dispose_op_none_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/dispose_op_background_before_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_background_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 255, 255) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/dispose_op_previous_region.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -129,6 +142,7 @@ def test_apng_dispose_op_previous_frame() -> None: # ], # ) with Image.open("Tests/images/apng/dispose_op_previous_frame.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (255, 0, 0, 255) @@ -142,26 +156,31 @@ def test_apng_dispose_op_background_p_mode() -> None: def test_apng_blend() -> None: with Image.open("Tests/images/apng/blend_op_source_solid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_source_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 0, 0) assert im.getpixel((64, 32)) == (0, 0, 0, 0) with Image.open("Tests/images/apng/blend_op_source_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 2) assert im.getpixel((64, 32)) == (0, 255, 0, 2) with Image.open("Tests/images/apng/blend_op_over.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) with Image.open("Tests/images/apng/blend_op_over_near_transparent.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 97) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -175,6 +194,7 @@ def test_apng_blend_transparency() -> None: def test_apng_chunk_order() -> None: with Image.open("Tests/images/apng/fctl_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 255, 0, 255) assert im.getpixel((64, 32)) == (0, 255, 0, 255) @@ -230,66 +250,78 @@ def test_apng_num_plays() -> None: def test_apng_mode() -> None: with Image.open("Tests/images/apng/mode_16bit.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "RGBA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (0, 0, 128, 191) assert im.getpixel((64, 32)) == (0, 0, 128, 191) with Image.open("Tests/images/apng/mode_grayscale.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "L" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == 128 assert im.getpixel((64, 32)) == 255 with Image.open("Tests/images/apng/mode_grayscale_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "LA" im.seek(im.n_frames - 1) assert im.getpixel((0, 0)) == (128, 191) assert im.getpixel((64, 32)) == (128, 191) with Image.open("Tests/images/apng/mode_palette.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) - im = im.convert("RGB") - assert im.getpixel((0, 0)) == (0, 255, 0) - assert im.getpixel((64, 32)) == (0, 255, 0) + rgb_im = im.convert("RGB") + assert rgb_im.getpixel((0, 0)) == (0, 255, 0) + assert rgb_im.getpixel((64, 32)) == (0, 255, 0) with Image.open("Tests/images/apng/mode_palette_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) - im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (255, 0, 0, 0) - assert im.getpixel((64, 32)) == (255, 0, 0, 0) + rgb_im = im.convert("RGBA") + assert rgb_im.getpixel((0, 0)) == (255, 0, 0, 0) + assert rgb_im.getpixel((64, 32)) == (255, 0, 0, 0) with Image.open("Tests/images/apng/mode_palette_1bit_alpha.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.mode == "P" im.seek(im.n_frames - 1) - im = im.convert("RGBA") - assert im.getpixel((0, 0)) == (0, 0, 255, 128) - assert im.getpixel((64, 32)) == (0, 0, 255, 128) + rgb_im = im.convert("RGBA") + assert rgb_im.getpixel((0, 0)) == (0, 0, 255, 128) + assert rgb_im.getpixel((64, 32)) == (0, 0, 255, 128) def test_apng_chunk_errors() -> None: with Image.open("Tests/images/apng/chunk_no_actl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.warns(UserWarning): with Image.open("Tests/images/apng/chunk_multi_actl.png") as im: im.load() + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with Image.open("Tests/images/apng/chunk_no_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_repeat_fctl.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) with Image.open("Tests/images/apng/chunk_no_fdat.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(SyntaxError): im.seek(im.n_frames - 1) @@ -297,18 +329,21 @@ def test_apng_chunk_errors() -> None: def test_apng_syntax_errors() -> None: with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated with pytest.raises(OSError): im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() # we can handle this case gracefully exception = None with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) try: im.seek(im.n_frames - 1) except Exception as e: @@ -317,11 +352,13 @@ def test_apng_syntax_errors() -> None: with pytest.raises(OSError): with Image.open("Tests/images/apng/syntax_num_frames_high.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() with pytest.warns(UserWarning): with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert not im.is_animated im.load() @@ -341,6 +378,7 @@ def test_apng_syntax_errors() -> 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: + assert isinstance(im, PngImagePlugin.PngImageFile) im.seek(im.n_frames - 1) im.load() @@ -351,6 +389,8 @@ def test_apng_save(tmp_path: Path) -> None: im.save(test_file, save_all=True) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + im.load() assert not im.is_animated assert im.n_frames == 1 @@ -366,6 +406,8 @@ def test_apng_save(tmp_path: Path) -> None: ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + im.load() assert im.is_animated assert im.n_frames == 2 @@ -405,6 +447,8 @@ def test_apng_save_split_fdat(tmp_path: Path) -> None: append_images=frames, ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + exception = None try: im.seek(im.n_frames - 1) @@ -452,6 +496,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: test_file, save_all=True, append_images=[frame, frame], duration=[500, 100, 150] ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 1 assert "duration" not in im.info @@ -463,6 +508,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: duration=[500, 100, 150], ) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 @@ -473,6 +519,7 @@ def test_apng_save_duration_loop(tmp_path: Path) -> None: frame.info["duration"] = 300 frame.save(test_file, save_all=True, append_images=[frame, different_frame]) with Image.open(test_file) as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.n_frames == 2 assert im.info["duration"] == 600 diff --git a/Tests/test_file_bmp.py b/Tests/test_file_bmp.py index 2ff4160bd..4aafd5eb8 100644 --- a/Tests/test_file_bmp.py +++ b/Tests/test_file_bmp.py @@ -166,9 +166,9 @@ def test_save_dib(tmp_path: Path) -> None: def test_rgba_bitfields() -> None: # This test image has been manually hexedited # to change the bitfield compression in the header from XBGR to RGBA - with Image.open("Tests/images/rgb32bf-rgba.bmp") as im: + with Image.open("Tests/images/rgb32bf-rgba.bmp") as bmp_im: # So before the comparing the image, swap the channels - b, g, r = im.split()[1:] + b, g, r = bmp_im.split()[1:] im = Image.merge("RGB", (r, g, b)) assert_image_equal_tofile(im, "Tests/images/bmp/q/rgb32bf-xbgr.bmp") diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 77ee5b0ea..983eb381c 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -61,6 +61,7 @@ def test_handler(tmp_path: Path) -> None: def load(self, im: ImageFile.StubImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_container.py b/Tests/test_file_container.py index 237045acc..597ab5083 100644 --- a/Tests/test_file_container.py +++ b/Tests/test_file_container.py @@ -4,8 +4,6 @@ import pytest from PIL import ContainerIO, Image -from .helper import hopper - TEST_FILE = "Tests/images/dummy.container" @@ -15,15 +13,15 @@ def test_sanity() -> None: def test_isatty() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.isatty() is False def test_seekable() -> None: - with hopper() as im: - container = ContainerIO.ContainerIO(im, 0, 0) + with open(TEST_FILE, "rb") as fh: + container = ContainerIO.ContainerIO(fh, 0, 0) assert container.seekable() is True diff --git a/Tests/test_file_cur.py b/Tests/test_file_cur.py index dbf1b866d..ff82e2983 100644 --- a/Tests/test_file_cur.py +++ b/Tests/test_file_cur.py @@ -26,6 +26,7 @@ def test_invalid_file() -> None: no_cursors_file = "Tests/images/no_cursors.cur" cur = CurImagePlugin.CurImageFile(TEST_FILE) + assert cur.fp is not None cur.fp.close() with open(no_cursors_file, "rb") as cur.fp: with pytest.raises(TypeError): diff --git a/Tests/test_file_dcx.py b/Tests/test_file_dcx.py index 65337cad9..0383bb746 100644 --- a/Tests/test_file_dcx.py +++ b/Tests/test_file_dcx.py @@ -65,12 +65,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, DcxImagePlugin.DcxImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 9a826ebe8..d81b3f8f3 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -50,6 +50,7 @@ TEST_FILE_UNCOMPRESSED_RGB_WITH_ALPHA = "Tests/images/uncompressed_rgb.dds" ) def test_sanity_dxt1_bc1(image_path: str) -> None: """Check DXT1 and BC1 images can be opened""" + target: Image.Image with Image.open(TEST_FILE_DXT1.replace(".dds", ".png")) as target: target = target.convert("RGBA") with Image.open(image_path) as im: @@ -331,11 +332,13 @@ def test_dxt5_colorblock_alpha_issue_4142() -> None: with Image.open("Tests/images/dxt5-colorblock-alpha-issue-4142.dds") as im: px = im.getpixel((0, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 px = im.getpixel((1, 0)) + assert isinstance(px, tuple) assert px[0] != 0 assert px[1] != 0 assert px[2] != 0 diff --git a/Tests/test_file_eps.py b/Tests/test_file_eps.py index d54deb515..22519b813 100644 --- a/Tests/test_file_eps.py +++ b/Tests/test_file_eps.py @@ -85,6 +85,8 @@ simple_eps_file_with_long_binary_data = ( 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: + assert isinstance(image, EpsImagePlugin.EpsImageFile) + image.load(scale=scale) assert image.mode == "RGB" assert image.size == expected_size @@ -94,10 +96,12 @@ def test_sanity(filename: str, size: tuple[int, int], scale: int) -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_load() -> None: with Image.open(FILE1) as im: - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (255, 255, 255) + assert px[0, 0] == (255, 255, 255) def test_binary() -> None: @@ -215,6 +219,8 @@ def test_showpage() -> None: @pytest.mark.skipif(not HAS_GHOSTSCRIPT, reason="Ghostscript not available") def test_transparency() -> None: with Image.open("Tests/images/reqd_showpage.eps") as plot_image: + assert isinstance(plot_image, EpsImagePlugin.EpsImageFile) + plot_image.load(transparency=True) assert plot_image.mode == "RGBA" @@ -239,8 +245,8 @@ def test_bytesio_object() -> None: with Image.open(img_bytes) as img: img.load() - with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") + with Image.open(FILE1_COMPARE) as im: + image1_scale1_compare = im.convert("RGB") image1_scale1_compare.load() assert_image_similar(img, image1_scale1_compare, 5) @@ -265,16 +271,16 @@ def test_render_scale1() -> None: # Zero bounding box with Image.open(FILE1) as image1_scale1: image1_scale1.load() - with Image.open(FILE1_COMPARE) as image1_scale1_compare: - image1_scale1_compare = image1_scale1_compare.convert("RGB") + with Image.open(FILE1_COMPARE) as im: + image1_scale1_compare = im.convert("RGB") image1_scale1_compare.load() assert_image_similar(image1_scale1, image1_scale1_compare, 5) # Non-zero bounding box with Image.open(FILE2) as image2_scale1: image2_scale1.load() - with Image.open(FILE2_COMPARE) as image2_scale1_compare: - image2_scale1_compare = image2_scale1_compare.convert("RGB") + with Image.open(FILE2_COMPARE) as im: + image2_scale1_compare = im.convert("RGB") image2_scale1_compare.load() assert_image_similar(image2_scale1, image2_scale1_compare, 10) @@ -286,17 +292,19 @@ def test_render_scale2() -> None: # Zero bounding box with Image.open(FILE1) as image1_scale2: + assert isinstance(image1_scale2, EpsImagePlugin.EpsImageFile) image1_scale2.load(scale=2) - with Image.open(FILE1_COMPARE_SCALE2) as image1_scale2_compare: - image1_scale2_compare = image1_scale2_compare.convert("RGB") + with Image.open(FILE1_COMPARE_SCALE2) as im: + image1_scale2_compare = im.convert("RGB") image1_scale2_compare.load() assert_image_similar(image1_scale2, image1_scale2_compare, 5) # Non-zero bounding box with Image.open(FILE2) as image2_scale2: + assert isinstance(image2_scale2, EpsImagePlugin.EpsImageFile) image2_scale2.load(scale=2) - with Image.open(FILE2_COMPARE_SCALE2) as image2_scale2_compare: - image2_scale2_compare = image2_scale2_compare.convert("RGB") + with Image.open(FILE2_COMPARE_SCALE2) as im: + image2_scale2_compare = im.convert("RGB") image2_scale2_compare.load() assert_image_similar(image2_scale2, image2_scale2_compare, 10) @@ -304,9 +312,9 @@ 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: str) -> None: - with Image.open(filename) as im: + with Image.open(filename) as img: new_size = (100, 100) - im = im.resize(new_size) + im = img.resize(new_size) assert im.size == new_size diff --git a/Tests/test_file_fli.py b/Tests/test_file_fli.py index f86fb8d09..d99b76f62 100644 --- a/Tests/test_file_fli.py +++ b/Tests/test_file_fli.py @@ -21,6 +21,8 @@ animated_test_file_with_prefix_chunk = "Tests/images/2422.flc" def test_sanity() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + im.load() assert im.mode == "P" assert im.size == (128, 128) @@ -28,6 +30,8 @@ def test_sanity() -> None: assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -39,6 +43,8 @@ def test_prefix_chunk() -> None: ImageFile.LOAD_TRUNCATED_IMAGES = True try: with Image.open(animated_test_file_with_prefix_chunk) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) + assert im.mode == "P" assert im.size == (320, 200) assert im.format == "FLI" @@ -46,6 +52,7 @@ def test_prefix_chunk() -> None: assert im.is_animated palette = im.getpalette() + assert palette is not None assert palette[3:6] == [255, 255, 255] assert palette[381:384] == [204, 204, 12] assert palette[765:] == [252, 0, 0] @@ -110,16 +117,19 @@ def test_palette_chunk_second() -> None: def test_n_frames() -> None: with Image.open(static_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) assert im.n_frames == 384 assert im.is_animated def test_eoferror() -> None: with Image.open(animated_test_file) as im: + assert isinstance(im, FliImagePlugin.FliImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_fpx.py b/Tests/test_file_fpx.py index e32f30a01..8d8064692 100644 --- a/Tests/test_file_fpx.py +++ b/Tests/test_file_fpx.py @@ -22,10 +22,11 @@ def test_sanity() -> None: def test_close() -> None: with Image.open("Tests/images/input_bw_one_band.fpx") as im: - pass + assert isinstance(im, FpxImagePlugin.FpxImageFile) assert im.ole.fp.closed im = Image.open("Tests/images/input_bw_one_band.fpx") + assert isinstance(im, FpxImagePlugin.FpxImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_gbr.py b/Tests/test_file_gbr.py index be98b08f2..19ef2ddde 100644 --- a/Tests/test_file_gbr.py +++ b/Tests/test_file_gbr.py @@ -14,10 +14,12 @@ def test_gbr_file() -> None: def test_load() -> None: with Image.open("Tests/images/gbr.gbr") as im: - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + assert px[0, 0] == (0, 0, 0, 0) def test_multiple_load_operations() -> None: diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 571fe1b9a..e8ec518a6 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -81,12 +81,16 @@ def test_invalid_file() -> None: def test_l_mode_transparency() -> None: with Image.open("Tests/images/no_palette_with_transparency.gif") as im: assert im.mode == "L" - assert im.load()[0, 0] == 128 + px = im.load() + assert px is not None + assert px[0, 0] == 128 assert im.info["transparency"] == 255 im.seek(1) assert im.mode == "L" - assert im.load()[0, 0] == 128 + px = im.load() + assert px is not None + assert px[0, 0] == 128 def test_l_mode_after_rgb() -> None: @@ -221,6 +225,7 @@ def test_optimize_if_palette_can_be_reduced_by_half() -> None: out = BytesIO() im.save(out, "GIF", optimize=optimize) with Image.open(out) as reloaded: + assert reloaded.palette is not None assert len(reloaded.palette.palette) // 3 == colors @@ -277,6 +282,7 @@ def test_roundtrip_save_all(tmp_path: Path) -> None: im.save(out, save_all=True) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 5 @@ -302,10 +308,14 @@ def test_roundtrip_save_all_1(tmp_path: Path) -> None: ), ) def test_loading_multiple_palettes(path: str, mode: str) -> None: + im: Image.Image with Image.open(path) as im: assert im.mode == "P" + assert im.palette is not None first_frame_colors = im.palette.colors.keys() - original_color = im.convert("RGB").load()[0, 0] + px = im.convert("RGB").load() + assert px is not None + original_color = px[0, 0] im.seek(1) assert im.mode == mode @@ -313,10 +323,14 @@ def test_loading_multiple_palettes(path: str, mode: str) -> None: im = im.convert("RGB") # Check a color only from the old palette - assert im.load()[0, 0] == original_color + px = im.load() + assert px is not None + assert px[0, 0] == original_color # Check a color from the new palette - assert im.load()[24, 24] not in first_frame_colors + px = im.load() + assert px is not None + assert px[24, 24] not in first_frame_colors def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: @@ -334,7 +348,7 @@ def test_headers_saving_for_animated_gifs(tmp_path: Path) -> None: def test_palette_handling(tmp_path: Path) -> None: # see https://github.com/python-pillow/Pillow/issues/513 - + im: Image.Image with Image.open(TEST_GIF) as im: im = im.convert("RGB") @@ -358,8 +372,8 @@ def test_palette_434(tmp_path: Path) -> None: return reloaded - orig = "Tests/images/test.colors.gif" - with Image.open(orig) as im: + im: Image.Image + with Image.open("Tests/images/test.colors.gif") as im: with roundtrip(im) as reloaded: assert_image_similar(im, reloaded, 1) with roundtrip(im, optimize=True) as reloaded: @@ -374,6 +388,7 @@ def test_palette_434(tmp_path: Path) -> None: @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: + img: Image.Image with Image.open(TEST_GIF) as img: img = img.convert("RGB") @@ -386,6 +401,7 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None: @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_l_mode(tmp_path: Path) -> None: + img: Image.Image with Image.open(TEST_GIF) as img: img = img.convert("L") @@ -438,10 +454,12 @@ def test_seek_rewind() -> None: def test_n_frames(path: str, n_frames: int) -> None: # Test is_animated before n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated == (n_frames != 1) # Test is_animated after n_frames with Image.open(path) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) @@ -451,6 +469,7 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(1) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 5 assert_image_equal(im, expected) @@ -458,17 +477,20 @@ def test_no_change() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as im: im.seek(3) expected = im.copy() + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.is_animated assert_image_equal(im, expected) with Image.open("Tests/images/comment_after_only_frame.gif") as im: expected = Image.new("P", (1, 1)) + assert isinstance(im, GifImagePlugin.GifImageFile) assert not im.is_animated assert_image_equal(im, expected) def test_eoferror() -> None: with Image.open(TEST_GIF) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -483,11 +505,13 @@ def test_eoferror() -> None: def test_first_frame_transparency() -> None: with Image.open("Tests/images/first_frame_transparency.gif") as im: px = im.load() + assert px is not None assert px[0, 0] == im.info["transparency"] def test_dispose_none() -> None: with Image.open("Tests/images/dispose_none.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -511,6 +535,7 @@ def test_dispose_none_load_end() -> None: def test_dispose_background() -> None: with Image.open("Tests/images/dispose_bgnd.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -523,7 +548,10 @@ def test_dispose_background_transparency() -> None: with Image.open("Tests/images/dispose_bgnd_transparency.gif") as img: img.seek(2) px = img.load() - assert px[35, 30][3] == 0 + assert px is not None + value = px[35, 30] + assert isinstance(value, tuple) + assert value[3] == 0 @pytest.mark.parametrize( @@ -565,6 +593,7 @@ def test_transparent_dispose( def test_dispose_previous() -> None: with Image.open("Tests/images/dispose_prev.gif") as img: + assert isinstance(img, GifImagePlugin.GifImageFile) try: while True: img.seek(img.tell() + 1) @@ -602,6 +631,7 @@ def test_save_dispose(tmp_path: Path) -> None: for method in range(0, 4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for _ in range(2): img.seek(img.tell() + 1) assert img.disposal_method == method @@ -615,6 +645,7 @@ def test_save_dispose(tmp_path: Path) -> None: ) with Image.open(out) as img: + assert isinstance(img, GifImagePlugin.GifImageFile) for i in range(2): img.seek(img.tell() + 1) assert img.disposal_method == i + 1 @@ -737,6 +768,7 @@ def test_dispose2_background_frame(tmp_path: Path) -> None: im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2) with Image.open(out) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.n_frames == 3 @@ -903,6 +935,8 @@ def test_identical_frames(tmp_path: Path) -> None: out, save_all=True, append_images=im_list[1:], duration=duration_list ) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that the first three frames were combined assert reread.n_frames == 2 @@ -932,6 +966,8 @@ def test_identical_frames_to_single_frame( im_list[0].save(out, save_all=True, append_images=im_list[1:], duration=duration) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) + # Assert that all frames were combined assert reread.n_frames == 1 @@ -979,9 +1015,9 @@ def test_webp_background(tmp_path: Path) -> None: # Test opaque WebP background if features.check("webp"): - with Image.open("Tests/images/hopper.webp") as im: - assert im.info["background"] == (255, 255, 255, 255) - im.save(out) + with Image.open("Tests/images/hopper.webp") as img: + assert img.info["background"] == (255, 255, 255, 255) + img.save(out) # Test non-opaque WebP background im = Image.new("L", (100, 100), "#000") @@ -990,6 +1026,7 @@ def test_webp_background(tmp_path: Path) -> None: def test_comment(tmp_path: Path) -> None: + im: Image.Image with Image.open(TEST_GIF) as im: assert im.info["comment"] == b"File written by Adobe Photoshop\xa8 4.0" @@ -1118,6 +1155,7 @@ def test_append_images(tmp_path: Path) -> None: im.copy().save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending using a generator @@ -1127,6 +1165,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=im_generator(ims)) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 3 # Tests appending single and multiple frame images @@ -1135,6 +1174,7 @@ def test_append_images(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 10 @@ -1235,6 +1275,7 @@ def test_bbox(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=ims) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1247,6 +1288,7 @@ def test_bbox_alpha(tmp_path: Path) -> None: im.save(out, save_all=True, append_images=[im2]) with Image.open(out) as reread: + assert isinstance(reread, GifImagePlugin.GifImageFile) assert reread.n_frames == 2 @@ -1308,6 +1350,9 @@ def test_palette_save_all_P(tmp_path: Path) -> None: with Image.open(out) as im: # Assert that the frames are correct, and each frame has the same palette assert_image_equal(im.convert("RGB"), frames[0].convert("RGB")) + assert isinstance(im, GifImagePlugin.GifImageFile) + assert im.palette is not None + assert im.global_palette is not None assert im.palette.palette == im.global_palette.palette im.seek(1) @@ -1373,7 +1418,9 @@ def test_getdata() -> None: def test_lzw_bits() -> None: # see https://github.com/python-pillow/Pillow/issues/2811 with Image.open("Tests/images/issue_2811.gif") as im: - assert im.tile[0][3][0] == 11 # LZW bits + args = im.tile[0][3] + assert isinstance(args, tuple) + assert args[0] == 11 # LZW bits # codec error prepatch im.load() @@ -1398,6 +1445,7 @@ def test_extents( GifImagePlugin.LOADING_STRATEGY = loading_strategy try: with Image.open("Tests/images/" + test_file) as im: + assert isinstance(im, GifImagePlugin.GifImageFile) assert im.size == (100, 100) # Check that n_frames does not change the size @@ -1428,4 +1476,8 @@ def test_saving_rgba(tmp_path: Path) -> None: with Image.open(out) as reloaded: reloaded_rgba = reloaded.convert("RGBA") - assert reloaded_rgba.load()[0, 0][3] == 0 + px = reloaded_rgba.load() + assert px is not None + value = px[0, 0] + assert isinstance(value, tuple) + assert value[3] == 0 diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index aba473d24..65b1c506e 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -59,8 +59,9 @@ def test_handler(tmp_path: Path) -> None: def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im: Image.Image) -> Image.Image: + def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 8275bd0d8..b8a8ecce6 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -61,8 +61,9 @@ def test_handler(tmp_path: Path) -> None: def open(self, im: Image.Image) -> None: self.opened = True - def load(self, im: Image.Image) -> Image.Image: + def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True + assert im.fp is not None im.fp.close() return Image.new("RGB", (1, 1)) diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py index 488984aef..91fd81409 100644 --- a/Tests/test_file_icns.py +++ b/Tests/test_file_icns.py @@ -30,10 +30,14 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_FILE) as im: - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) # Test again now that it has already been loaded once - assert im.load()[0, 0] == (0, 0, 0, 0) + px = im.load() + assert px is not None + assert px[0, 0] == (0, 0, 0, 0) def test_save(tmp_path: Path) -> None: @@ -63,6 +67,7 @@ def test_save_append_images(tmp_path: Path) -> None: assert_image_similar_tofile(im, temp_file, 1) with Image.open(temp_file) as reread: + assert isinstance(reread, IcnsImagePlugin.IcnsImageFile) reread.size = (16, 16, 2) reread.load() assert_image_equal(reread, provided_im) @@ -84,6 +89,7 @@ def test_sizes() -> None: # Check that we can load all of the sizes, and that the final pixel # dimensions are as expected with Image.open(TEST_FILE) as im: + assert isinstance(im, IcnsImagePlugin.IcnsImageFile) for w, h, r in im.info["sizes"]: wr = w * r hr = h * r @@ -105,6 +111,7 @@ def test_older_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow2.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h, r) im2.load() assert im2.mode == "RGBA" @@ -122,6 +129,7 @@ def test_jp2_icon() -> None: wr = w * r hr = h * r with Image.open("Tests/images/pillow3.icns") as im2: + assert isinstance(im2, IcnsImagePlugin.IcnsImageFile) im2.size = (w, h, r) im2.load() assert im2.mode == "RGBA" diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py index 37770498a..c86ade12d 100644 --- a/Tests/test_file_ico.py +++ b/Tests/test_file_ico.py @@ -24,7 +24,9 @@ def test_sanity() -> None: def test_load() -> None: with Image.open(TEST_ICO_FILE) as im: - assert im.load()[0, 0] == (1, 1, 9, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (1, 1, 9, 255) def test_mask() -> None: @@ -75,6 +77,7 @@ def test_save_to_bytes() -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert im.mode == reloaded.mode @@ -92,6 +95,7 @@ def test_getpixel(tmp_path: Path) -> None: im.save(temp_file, "ico", sizes=[(32, 32), (64, 64)]) with Image.open(temp_file) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.load() reloaded.size = (32, 32) @@ -165,6 +169,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: # The other one output.seek(0) with Image.open(output) as reloaded: + assert isinstance(reloaded, IcoImagePlugin.IcoImageFile) reloaded.size = (32, 32) assert "RGBA" == reloaded.mode @@ -176,6 +181,7 @@ def test_save_to_bytes_bmp(mode: str) -> None: def test_incorrect_size() -> None: with Image.open(TEST_ICO_FILE) as im: + assert isinstance(im, IcoImagePlugin.IcoImageFile) with pytest.raises(ValueError): im.size = (1, 1) @@ -217,6 +223,7 @@ def test_save_append_images(tmp_path: Path) -> None: im.save(outfile, sizes=[(32, 32), (128, 128)], append_images=[provided_im]) with Image.open(outfile) as reread: + assert isinstance(reread, IcoImagePlugin.IcoImageFile) assert_image_equal(reread, hopper("RGBA")) reread.size = (32, 32) @@ -253,8 +260,7 @@ def test_truncated_mask() -> None: try: with Image.open(io.BytesIO(data)) as im: - with Image.open("Tests/images/hopper_mask.png") as expected: - assert im.mode == "1" + assert im.mode == "1" # 32 bpp output = io.BytesIO() diff --git a/Tests/test_file_im.py b/Tests/test_file_im.py index 036965bf5..787a36b26 100644 --- a/Tests/test_file_im.py +++ b/Tests/test_file_im.py @@ -64,12 +64,14 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) assert im.n_frames == 1 assert not im.is_animated def test_eoferror() -> None: with Image.open(TEST_IM) as im: + assert isinstance(im, ImImagePlugin.ImImageFile) n_frames = im.n_frames # Test seeking past the last frame diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index cde951395..d3164c235 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -91,6 +91,7 @@ class TestFileJpeg: def test_app(self) -> None: # Test APP/COM reader (@PIL135) with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im.applist[0] == ("APP0", b"JFIF\x00\x01\x01\x01\x00`\x00`\x00\x00") assert im.applist[1] == ( "COM", @@ -131,26 +132,30 @@ class TestFileJpeg: f = "Tests/images/pil_sample_cmyk.jpg" with Image.open(f) as im: # the source image has red pixels in the upper left corner. - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + value = im.getpixel((0, 0)) + assert isinstance(value, tuple) + c, m, y, k = (x / 255.0 for x in value) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 # the opposite corner is black - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + value = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(value, tuple) + c, m, y, k = (x / 255.0 for x in value) assert k > 0.9 # roundtrip, and check again im = self.roundtrip(im) - c, m, y, k = (x / 255.0 for x in im.getpixel((0, 0))) + value = im.getpixel((0, 0)) + assert isinstance(value, tuple) + c, m, y, k = (x / 255.0 for x in value) assert c == 0.0 assert m > 0.8 assert y > 0.8 assert k == 0.0 - c, m, y, k = ( - x / 255.0 for x in im.getpixel((im.size[0] - 1, im.size[1] - 1)) - ) + value = im.getpixel((im.size[0] - 1, im.size[1] - 1)) + assert isinstance(value, tuple) + c, m, y, k = (x / 255.0 for x in value) assert k > 0.9 def test_rgb(self) -> None: @@ -309,6 +314,8 @@ class TestFileJpeg: def test_exif_typeerror(self) -> None: with Image.open("Tests/images/exif_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -324,8 +331,10 @@ class TestFileJpeg: # Reading with Image.open("Tests/images/exif_gps.jpg") as im: - exif = im._getexif() - assert exif[gps_index] == expected_exif_gps + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[gps_index] == expected_exif_gps # Writing f = str(tmp_path / "temp.jpg") @@ -334,8 +343,10 @@ class TestFileJpeg: hopper().save(f, exif=exif) with Image.open(f) as reloaded: - exif = reloaded._getexif() - assert exif[gps_index] == expected_exif_gps + assert isinstance(reloaded, JpegImagePlugin.JpegImageFile) + exif_data = reloaded._getexif() + assert exif_data is not None + assert exif_data[gps_index] == expected_exif_gps def test_empty_exif_gps(self) -> None: with Image.open("Tests/images/empty_gps_ifd.jpg") as im: @@ -363,6 +374,7 @@ class TestFileJpeg: exifs = [] for i in range(2): with Image.open("Tests/images/exif-200dpcm.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) exifs.append(im._getexif()) assert exifs[0] == exifs[1] @@ -396,13 +408,18 @@ class TestFileJpeg: } with Image.open("Tests/images/exif_gps.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif = im._getexif() + assert exif is not None for tag, value in expected_exif.items(): assert value == exif[tag] def test_exif_gps_typeerror(self) -> None: with Image.open("Tests/images/exif_gps_typeerror.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise a TypeError im._getexif() @@ -478,7 +495,9 @@ class TestFileJpeg: def test_exif(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) info = im._getexif() + assert info is not None assert info[305] == "Adobe Photoshop CS Macintosh" def test_get_child_images(self) -> None: @@ -490,6 +509,7 @@ class TestFileJpeg: def test_mp(self) -> None: with Image.open("Tests/images/pil_sample_rgb.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im._getmp() is None def test_quality_keep(self, tmp_path: Path) -> None: @@ -547,12 +567,14 @@ class TestFileJpeg: f = str(tmp_path / "temp.jpg") im.save(f, qtables=[[n] * 64] * n) with Image.open(f) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == n reloaded = self.roundtrip(im, qtables="keep") assert im.quantization == reloaded.quantization assert max(reloaded.quantization[0]) <= 255 with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) qtables = im.quantization reloaded = self.roundtrip(im, qtables=qtables, subsampling=0) assert im.quantization == reloaded.quantization @@ -652,6 +674,7 @@ class TestFileJpeg: def test_load_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert len(im.quantization) == 2 assert len(im.quantization[0]) == 64 assert max(im.quantization[0]) > 255 @@ -659,10 +682,12 @@ class TestFileJpeg: def test_save_multiple_16bit_qtables(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: im2 = self.roundtrip(im, qtables="keep") + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert im.quantization == im2.quantization def test_save_single_16bit_qtable(self) -> None: with Image.open("Tests/images/hopper_16bit_qtables.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im2 = self.roundtrip(im, qtables={0: im.quantization[0]}) assert len(im2.quantization) == 1 assert im2.quantization[0] == im.quantization[0] @@ -693,9 +718,10 @@ class TestFileJpeg: @pytest.mark.skipif(not djpeg_available(), reason="djpeg not available") def test_load_djpeg(self) -> None: - with Image.open(TEST_FILE) as img: - img.load_djpeg() - assert_image_similar_tofile(img, TEST_FILE, 5) + with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + im.load_djpeg() + assert_image_similar_tofile(im, TEST_FILE, 5) @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") def test_save_cjpeg(self, tmp_path: Path) -> None: @@ -868,7 +894,10 @@ class TestFileJpeg: # in contrast to normal 8 with Image.open("Tests/images/exif-ifd-offset.jpg") as im: # Act / Assert - assert im._getexif()[306] == "2017:03:13 23:03:09" + assert isinstance(im, JpegImagePlugin.JpegImageFile) + exif = im._getexif() + assert exif is not None + assert exif[306] == "2017:03:13 23:03:09" def test_multiple_exif(self) -> None: with Image.open("Tests/images/multiple_exif.jpg") as im: @@ -896,6 +925,7 @@ class TestFileJpeg: def test_photoshop_malformed_and_multiple(self) -> None: with Image.open("Tests/images/app13-multiple.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) assert "photoshop" in im.info assert 24 == len(im.info["photoshop"]) apps_13_lengths = [len(v) for k, v in im.applist if k == "APP13"] @@ -1020,7 +1050,7 @@ class TestFileJpeg: with Image.open(TEST_FILE) as im: im.tile = [ - ("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), + ImageFile._Tile("INFINITE", (0, 0, 128, 128), 0, ("RGB", 0, 1)), ] ImageFile.LOAD_TRUNCATED_IMAGES = True im.load() @@ -1067,6 +1097,7 @@ class TestFileJpeg: def test_deprecation(self) -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) with pytest.warns(DeprecationWarning): assert im.huffman_ac == {} with pytest.warns(DeprecationWarning): @@ -1083,8 +1114,9 @@ class TestFileCloseW32: im.save(tmpfile) im = Image.open(tmpfile) + assert im.fp is not None + assert not im.fp.closed fp = im.fp - assert not fp.closed with pytest.raises(OSError): os.remove(tmpfile) im.load() diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 26b085601..5976a9851 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -54,6 +54,7 @@ def test_sanity() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: px = im.load() + assert px is not None assert px[0, 0] == (0, 0, 0) assert im.mode == "RGB" assert im.size == (640, 480) @@ -154,7 +155,7 @@ def test_reduce() -> None: with Image.open("Tests/images/test-card-lossless.jp2") as im: assert callable(im.reduce) - im.reduce = 2 + im.reduce = 2 # type: ignore[method-assign, assignment] assert im.reduce == 2 im.load() @@ -221,12 +222,14 @@ def test_layers() -> None: out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 1 im.load() assert_image_similar(im, test_card, 13) out.seek(0) with Image.open(out) as im: + assert isinstance(im, Jpeg2KImagePlugin.Jpeg2KImageFile) im.layers = 3 im.load() assert_image_similar(im, test_card, 0.4) @@ -397,6 +400,7 @@ def test_subsampling_decode(name: str) -> None: def test_pclr() -> None: with Image.open(f"{EXTRA_DIR}/issue104_jpxstream.jp2") as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 256 assert im.palette.colors[(255, 255, 255)] == 0 @@ -404,6 +408,7 @@ def test_pclr() -> None: f"{EXTRA_DIR}/147af3f1083de4393666b7d99b01b58b_signal_sigsegv_130c531_6155_5136.jp2" ) as im: assert im.mode == "P" + assert im.palette is not None assert len(im.palette.colors) == 139 assert im.palette.colors[(0, 0, 0, 0)] == 0 diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 62f8719af..8fd89787b 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -11,7 +11,15 @@ from typing import Any, NamedTuple import pytest -from PIL import Image, ImageFilter, ImageOps, TiffImagePlugin, TiffTags, features +from PIL import ( + Image, + ImageFile, + ImageFilter, + ImageOps, + TiffImagePlugin, + TiffTags, + features, +) from PIL.TiffImagePlugin import OSUBFILETYPE, SAMPLEFORMAT, STRIPOFFSETS, SUBIFD from .helper import ( @@ -27,7 +35,7 @@ from .helper import ( @skip_unless_feature("libtiff") class LibTiffTestCase: - def _assert_noerr(self, tmp_path: Path, im: TiffImagePlugin.TiffImageFile) -> None: + def _assert_noerr(self, tmp_path: Path, im: ImageFile.ImageFile) -> None: """Helper tests that assert basic sanity about the g4 tiff reading""" # 1 bit assert im.mode == "1" @@ -36,6 +44,7 @@ class LibTiffTestCase: im.load() im.getdata() + assert isinstance(im, TiffImagePlugin.TiffImageFile) try: assert im._compression == "group4" except AttributeError: @@ -157,6 +166,7 @@ class TestFileLibTiff(LibTiffTestCase): """Test metadata writing through libtiff""" f = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) img.save(f, tiffinfo=img.tag) if legacy_api: @@ -174,6 +184,7 @@ class TestFileLibTiff(LibTiffTestCase): ] with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) if legacy_api: reloaded = loaded.tag.named() else: @@ -216,6 +227,7 @@ class TestFileLibTiff(LibTiffTestCase): # Exclude ones that have special meaning # that we're already testing them with Image.open("Tests/images/hopper_g4.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) for tag in im.tag_v2: try: del core_items[tag] @@ -321,6 +333,7 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, tiffinfo=tiffinfo) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) for tag, value in tiffinfo.items(): reloaded_value = reloaded.tag_v2[tag] if ( @@ -353,12 +366,14 @@ class TestFileLibTiff(LibTiffTestCase): def test_osubfiletype(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[OSUBFILETYPE] = 1 im.save(outfile) def test_subifd(self, tmp_path: Path) -> None: outfile = str(tmp_path / "temp.tif") with Image.open("Tests/images/g4_orientation_6.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.tag_v2[SUBIFD] = 10000 # Should not segfault @@ -373,6 +388,7 @@ class TestFileLibTiff(LibTiffTestCase): hopper().save(out, tiffinfo={700: b"xmlpacket tag"}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if 700 in reloaded.tag_v2: assert reloaded.tag_v2[700] == b"xmlpacket tag" @@ -434,12 +450,14 @@ class TestFileLibTiff(LibTiffTestCase): """Tests String data in info directory""" test_file = "Tests/images/hopper_g4_500.tif" with Image.open(test_file) as orig: - out = str(tmp_path / "temp.tif") + assert isinstance(orig, TiffImagePlugin.TiffImageFile) + out = str(tmp_path / "temp.tif") orig.tag[269] = "temp.tif" orig.save(out) with Image.open(out) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert "temp.tif" == reread.tag_v2[269] assert "temp.tif" == reread.tag[269][0] @@ -462,8 +480,8 @@ class TestFileLibTiff(LibTiffTestCase): # test case from irc, how to do blur on b/w image # and save to compressed tif. out = str(tmp_path / "temp.tif") - with Image.open("Tests/images/pport_g4.tif") as im: - im = im.convert("L") + with Image.open("Tests/images/pport_g4.tif") as img: + im = img.convert("L") im = im.filter(ImageFilter.GaussianBlur(4)) im.save(out, compression="tiff_adobe_deflate") @@ -545,6 +563,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open(out) as reloaded: # colormap/palette tag + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[320]) == 768 @pytest.mark.parametrize("compression", ("tiff_ccitt", "group3", "group4")) @@ -556,8 +575,9 @@ class TestFileLibTiff(LibTiffTestCase): im.save(out, compression=compression) def test_fp_leak(self) -> None: - im: Image.Image | None = Image.open("Tests/images/hopper_g4_500.tif") + im: ImageFile.ImageFile | None = Image.open("Tests/images/hopper_g4_500.tif") assert im is not None + assert im.fp is not None fn = im.fp.fileno() os.fstat(fn) @@ -576,6 +596,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/multipage.tiff") as im: # file is a multipage tiff, 10x10 green, 10x10 red, 20x20 blue + assert isinstance(im, TiffImagePlugin.TiffImageFile) im.seek(0) assert im.size == (10, 10) assert im.convert("RGB").getpixel((0, 0)) == (0, 128, 0) @@ -595,6 +616,7 @@ class TestFileLibTiff(LibTiffTestCase): # issue #862 monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) frames = im.n_frames assert frames == 3 for _ in range(frames): @@ -614,6 +636,7 @@ class TestFileLibTiff(LibTiffTestCase): def test__next(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert not im.tag.next im.load() assert not im.tag.next @@ -694,21 +717,25 @@ class TestFileLibTiff(LibTiffTestCase): im.save(outfile, compression="jpeg") with Image.open(outfile) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[530] == (1, 1) assert reloaded.tag_v2[532] == (0, 255, 128, 255, 128, 255) def test_exif_ifd(self) -> None: out = io.BytesIO() with Image.open("Tests/images/tiff_adobe_deflate.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[34665] == 125456 im.save(out, "TIFF") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 34665 not in reloaded.tag_v2 im.save(out, "TIFF", tiffinfo={34665: 125456}) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) if Image.core.libtiff_support_custom_tags: assert reloaded.tag_v2[34665] == 125456 @@ -790,6 +817,8 @@ class TestFileLibTiff(LibTiffTestCase): def test_multipage_compression(self) -> None: with Image.open("Tests/images/compression.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + im.seek(0) assert im._compression == "tiff_ccitt" assert im.size == (10, 10) @@ -1083,6 +1112,7 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 274 in im.tag_v2 im.load() @@ -1094,9 +1124,10 @@ class TestFileLibTiff(LibTiffTestCase): with Image.open("Tests/images/g4_orientation_1.tif") as base_im: for i in range(2, 9): with Image.open("Tests/images/g4_orientation_" + str(i) + ".tif") as im: - im = ImageOps.exif_transpose(im) + transposed_im = ImageOps.exif_transpose(im) + assert transposed_im is not None - assert_image_similar(base_im, im, 0.7) + assert_image_similar(base_im, transposed_im, 0.7) @pytest.mark.valgrind_known_error(reason="Backtrace in Python Core") def test_sampleformat_not_corrupted(self) -> None: diff --git a/Tests/test_file_mic.py b/Tests/test_file_mic.py index 9a6f13ea3..0706af4c0 100644 --- a/Tests/test_file_mic.py +++ b/Tests/test_file_mic.py @@ -22,19 +22,21 @@ def test_sanity() -> None: # Adjust for the gamma of 2.2 encoded into the file lut = ImagePalette.make_gamma_lut(1 / 2.2) - im = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) + im1 = Image.merge("RGBA", [chan.point(lut) for chan in im.split()]) im2 = hopper("RGBA") - assert_image_similar(im, im2, 10) + assert_image_similar(im1, im2, 10) def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.n_frames == 1 def test_is_animated() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, MicImagePlugin.MicImageFile) assert not im.is_animated @@ -55,10 +57,11 @@ def test_seek() -> None: def test_close() -> None: with Image.open(TEST_FILE) as im: - pass + assert isinstance(im, MicImagePlugin.MicImageFile) assert im.ole.fp.closed im = Image.open(TEST_FILE) + assert isinstance(im, MicImagePlugin.MicImageFile) im.close() assert im.ole.fp.closed diff --git a/Tests/test_file_mpo.py b/Tests/test_file_mpo.py index e0f42a266..08618df61 100644 --- a/Tests/test_file_mpo.py +++ b/Tests/test_file_mpo.py @@ -6,7 +6,7 @@ from typing import Any import pytest -from PIL import Image, ImageFile, MpoImagePlugin +from PIL import Image, ImageFile, JpegImagePlugin, MpoImagePlugin from .helper import ( assert_image_equal, @@ -71,6 +71,7 @@ def test_context_manager() -> None: def test_app(test_file: str) -> None: # Test APP/COM reader (@PIL135) with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.applist[0][0] == "APP1" assert im.applist[1][0] == "APP2" assert ( @@ -110,9 +111,11 @@ def test_ignore_frame_size() -> None: # Ignore the different size of the second frame # since this is not a "Large Thumbnail" image with Image.open("Tests/images/ignore_frame_size.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.size == (64, 64) im.seek(1) + assert im.mpinfo is not None assert ( im.mpinfo[0xB002][1]["Attribute"]["MPType"] == "Multi-Frame Image: (Disparity)" @@ -145,7 +148,9 @@ def test_reload_exif_after_seek() -> None: @pytest.mark.parametrize("test_file", test_files) def test_mp(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -154,7 +159,9 @@ def test_mp_offset() -> None: # This image has been manually hexedited to have an IFD offset of 10 # in APP2 data, in contrast to normal 8 with Image.open("Tests/images/sugarshack_ifd_offset.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None assert mpinfo[45056] == b"0100" assert mpinfo[45057] == 2 @@ -170,7 +177,9 @@ def test_mp_no_data() -> None: @pytest.mark.parametrize("test_file", test_files) def test_mp_attribute(test_file: str) -> None: with Image.open(test_file) as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) mpinfo = im._getmp() + assert mpinfo is not None for frame_number, mpentry in enumerate(mpinfo[0xB002]): mpattr = mpentry["Attribute"] if frame_number: @@ -211,12 +220,14 @@ def test_seek(test_file: str) -> None: def test_n_frames() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open("Tests/images/sugarshack.mpo") as im: + assert isinstance(im, MpoImagePlugin.MpoImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -230,6 +241,8 @@ def test_eoferror() -> None: def test_adopt_jpeg() -> None: with Image.open("Tests/images/hopper.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + with pytest.raises(ValueError): MpoImagePlugin.MpoImageFile.adopt(im) @@ -267,6 +280,7 @@ def test_save(test_file: str) -> None: def test_save_all() -> None: + im: Image.Image for test_file in test_files: with Image.open(test_file) as im: im_reloaded = roundtrip(im, save_all=True) diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index 0abf9866f..7e43a35be 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -4,7 +4,7 @@ import re import sys import warnings import zlib -from io import BytesIO +from io import BytesIO, TextIOWrapper from pathlib import Path from types import ModuleType from typing import Any, cast @@ -93,6 +93,7 @@ class TestFilePng: hopper("RGB").save(test_file) + im: Image.Image with Image.open(test_file) as im: im.load() assert im.mode == "RGB" @@ -103,6 +104,8 @@ class TestFilePng: for mode in ["1", "L", "P", "RGB", "I", "I;16", "I;16B"]: im = hopper(mode) im.save(test_file) + + reloaded: Image.Image with Image.open(test_file) as reloaded: if mode in ("I", "I;16B"): reloaded = reloaded.convert(mode) @@ -225,11 +228,13 @@ class TestFilePng: test_file = "Tests/images/pil123p.png" with Image.open(test_file) as im: assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + rgba_im = im.convert("RGBA") + assert_image(rgba_im, "RGBA", (162, 150)) # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 + colors = rgba_im.getchannel("A").getcolors() + assert colors is not None + assert len(colors) == 124 def test_load_transparent_rgb(self) -> None: test_file = "Tests/images/rgb_trns.png" @@ -237,11 +242,13 @@ class TestFilePng: assert im.info["transparency"] == (0, 255, 52) assert_image(im, "RGB", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) + rgba_im = im.convert("RGBA") + assert_image(rgba_im, "RGBA", (64, 64)) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + colors = rgba_im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == 876 def test_save_p_transparent_palette(self, tmp_path: Path) -> None: in_file = "Tests/images/pil123p.png" @@ -258,11 +265,13 @@ class TestFilePng: assert len(im.info["transparency"]) == 256 assert_image(im, "P", (162, 150)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (162, 150)) + rgba_im = im.convert("RGBA") + assert_image(rgba_im, "RGBA", (162, 150)) # image has 124 unique alpha values - assert len(im.getchannel("A").getcolors()) == 124 + colors = rgba_im.getchannel("A").getcolors() + assert colors is not None + assert len(colors) == 124 def test_save_p_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/p_trns_single.png" @@ -279,13 +288,15 @@ class TestFilePng: assert im.info["transparency"] == 164 assert im.getpixel((31, 31)) == 164 assert_image(im, "P", (64, 64)) - im = im.convert("RGBA") - assert_image(im, "RGBA", (64, 64)) + rgba_im = im.convert("RGBA") + assert_image(rgba_im, "RGBA", (64, 64)) - assert im.getpixel((31, 31)) == (0, 255, 52, 0) + assert rgba_im.getpixel((31, 31)) == (0, 255, 52, 0) # image has 876 transparent pixels - assert im.getchannel("A").getcolors()[0][0] == 876 + colors = rgba_im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == 876 def test_save_p_transparent_black(self, tmp_path: Path) -> None: # check if solid black image with full transparency @@ -313,7 +324,9 @@ class TestFilePng: assert im.info["transparency"] == 255 im_rgba = im.convert("RGBA") - assert im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + colors = im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent test_file = str(tmp_path / "temp.png") im.save(test_file) @@ -324,7 +337,9 @@ class TestFilePng: assert_image_equal(im, test_im) test_im_rgba = test_im.convert("RGBA") - assert test_im_rgba.getchannel("A").getcolors()[0][0] == num_transparent + colors = test_im_rgba.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent def test_save_rgb_single_transparency(self, tmp_path: Path) -> None: in_file = "Tests/images/caption_6_33_22.png" @@ -578,6 +593,7 @@ class TestFilePng: def test_read_private_chunks(self) -> None: with Image.open("Tests/images/exif.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.private_chunks == [(b"orNT", b"\x01")] def test_roundtrip_private_chunk(self) -> None: @@ -600,6 +616,7 @@ class TestFilePng: def test_textual_chunks_after_idat(self) -> None: with Image.open("Tests/images/hopper.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert "comment" in im.text for k, v in { "date:create": "2014-09-04T09:37:08+03:00", @@ -609,20 +626,24 @@ class TestFilePng: # Raises a SyntaxError in load_end with Image.open("Tests/images/broken_data_stream.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) with pytest.raises(OSError): assert isinstance(im.text, dict) # Raises a UnicodeDecodeError in load_end with Image.open("Tests/images/truncated_image.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) + # The file is truncated with pytest.raises(OSError): - im.text() + im.text ImageFile.LOAD_TRUNCATED_IMAGES = True assert isinstance(im.text, dict) ImageFile.LOAD_TRUNCATED_IMAGES = False # Raises an EOFError in load_end with Image.open("Tests/images/hopper_idat_after_image_end.png") as im: + assert isinstance(im, PngImagePlugin.PngImageFile) assert im.text == {"TXT": "VALUE", "ZIP": "VALUE"} def test_unknown_compression_method(self) -> None: @@ -667,6 +688,9 @@ class TestFilePng: im.save(out, bits=4, save_all=save_all) with Image.open(out) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + assert reloaded.png is not None + assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 48 def test_plte_length(self, tmp_path: Path) -> None: @@ -677,6 +701,9 @@ class TestFilePng: im.save(str(tmp_path / "temp.png")) with Image.open(out) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) + assert reloaded.png is not None + assert reloaded.png.im_palette is not None assert len(reloaded.png.im_palette[1]) == 3 def test_getxmp(self) -> None: @@ -698,13 +725,17 @@ class TestFilePng: def test_exif(self) -> None: # With an EXIF chunk with Image.open("Tests/images/exif.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # With an ImageMagick zTXt chunk with Image.open("Tests/images/exif_imagemagick.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # Assert that info still can be extracted # when the image is no longer a PngImageFile instance @@ -713,8 +744,10 @@ class TestFilePng: # With a tEXt chunk with Image.open("Tests/images/exif_text.png") as im: - exif = im._getexif() - assert exif[274] == 1 + assert isinstance(im, PngImagePlugin.PngImageFile) + exif_data = im._getexif() + assert exif_data is not None + assert exif_data[274] == 1 # With XMP tags with Image.open("Tests/images/xmp_tags_orientation.png") as im: @@ -728,6 +761,7 @@ class TestFilePng: im.save(test_file) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) assert reloaded._getexif() is None # Test passing in exif @@ -735,7 +769,9 @@ class TestFilePng: im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) exif = reloaded._getexif() + assert exif is not None assert exif[274] == 1 @mark_if_feature_version( @@ -747,7 +783,9 @@ class TestFilePng: im.save(test_file, exif=im.getexif()) with Image.open(test_file) as reloaded: + assert isinstance(reloaded, PngImagePlugin.PngImageFile) exif = reloaded._getexif() + assert exif is not None assert exif[305] == "Adobe Photoshop CS Macintosh" def test_exif_argument(self, tmp_path: Path) -> None: @@ -773,10 +811,8 @@ class TestFilePng: def test_save_stdout(self, buffer: bool) -> None: old_stdout = sys.stdout - class MyStdOut: - buffer = BytesIO() - - mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() + b = BytesIO() + mystdout: TextIOWrapper | BytesIO = TextIOWrapper(b) if buffer else b sys.stdout = mystdout @@ -786,9 +822,7 @@ class TestFilePng: # Reset stdout sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): - mystdout = mystdout.buffer - with Image.open(mystdout) as reloaded: + with Image.open(b) as reloaded: assert_image_equal_tofile(reloaded, TEST_PNG_FILE) def test_truncated_end_chunk(self) -> None: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fb08d613a..a7e523013 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from io import BytesIO +from io import BytesIO, TextIOWrapper from pathlib import Path import pytest @@ -79,6 +79,7 @@ def test_arbitrary_maxval( assert im.mode == mode px = im.load() + assert px is not None assert tuple(px[x, 0] for x in range(3)) == pixels @@ -370,10 +371,8 @@ def test_mimetypes(tmp_path: Path) -> None: def test_save_stdout(buffer: bool) -> None: old_stdout = sys.stdout - class MyStdOut: - buffer = BytesIO() - - mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() + b = BytesIO() + mystdout: TextIOWrapper | BytesIO = TextIOWrapper(b) if buffer else b sys.stdout = mystdout @@ -383,7 +382,5 @@ def test_save_stdout(buffer: bool) -> None: # Reset stdout sys.stdout = old_stdout - if isinstance(mystdout, MyStdOut): - mystdout = mystdout.buffer - with Image.open(mystdout) as reloaded: + with Image.open(b) as reloaded: assert_image_equal_tofile(reloaded, TEST_FILE) diff --git a/Tests/test_file_psd.py b/Tests/test_file_psd.py index e6c79e40b..00eba589a 100644 --- a/Tests/test_file_psd.py +++ b/Tests/test_file_psd.py @@ -55,17 +55,21 @@ def test_invalid_file() -> None: def test_n_frames() -> None: with Image.open("Tests/images/hopper_merged.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 assert not im.is_animated for path in [test_file, "Tests/images/negative_layer_count.psd"]: with Image.open(path) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 2 assert im.is_animated def test_eoferror() -> None: with Image.open(test_file) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + # PSD seek index starts at 1 rather than 0 n_frames = im.n_frames + 1 @@ -115,11 +119,13 @@ def test_rgba() -> None: def test_negative_top_left_layer() -> None: with Image.open("Tests/images/negative_top_left_layer.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.layers[0][2] == (-50, -50, 50, 50) def test_layer_skip() -> None: with Image.open("Tests/images/five_channels.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) assert im.n_frames == 1 @@ -171,5 +177,6 @@ def test_crashes(test_file: str, raises: type[Exception]) -> None: def test_layer_crashes(test_file: str) -> None: with open(test_file, "rb") as f: with Image.open(f) as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) with pytest.raises(SyntaxError): im.layers diff --git a/Tests/test_file_spider.py b/Tests/test_file_spider.py index 66c88e9d8..0155c1366 100644 --- a/Tests/test_file_spider.py +++ b/Tests/test_file_spider.py @@ -92,6 +92,7 @@ def test_tell() -> None: def test_n_frames() -> None: with Image.open(TEST_FILE) as im: + assert isinstance(im, SpiderImagePlugin.SpiderImageFile) assert im.n_frames == 1 assert not im.is_animated diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index a03a6a6e1..25dbeaea6 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -72,6 +72,7 @@ def test_palette_depth_8(tmp_path: Path) -> None: def test_palette_depth_16(tmp_path: Path) -> None: with Image.open("Tests/images/p_16.tga") as im: + assert im.palette is not None assert im.palette.mode == "RGBA" assert_image_equal_tofile(im.convert("RGBA"), "Tests/images/p_16.png") @@ -213,10 +214,18 @@ def test_save_orientation(tmp_path: Path) -> None: def test_horizontal_orientations() -> None: # These images have been manually hexedited to have the relevant orientations with Image.open("Tests/images/rgb32rle_top_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 0, 0) + px = im.load() + assert px is not None + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 0, 0) with Image.open("Tests/images/rgb32rle_bottom_right.tga") as im: - assert im.load()[90, 90][:3] == (0, 255, 0) + px = im.load() + assert px is not None + value = px[90, 90] + assert isinstance(value, tuple) + assert value[:3] == (0, 255, 0) def test_save_rle(tmp_path: Path) -> None: @@ -259,13 +268,17 @@ def test_save_l_transparency(tmp_path: Path) -> None: in_file = "Tests/images/la.tga" with Image.open(in_file) as im: assert im.mode == "LA" - assert im.getchannel("A").getcolors()[0][0] == num_transparent + colors = im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent out = str(tmp_path / "temp.tga") im.save(out) with Image.open(out) as test_im: assert test_im.mode == "LA" - assert test_im.getchannel("A").getcolors()[0][0] == num_transparent + colors = test_im.getchannel("A").getcolors() + assert colors is not None + assert colors[0][0] == num_transparent assert_image_equal(im, test_im) diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index 190f83f40..c00178bff 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -9,7 +9,13 @@ from types import ModuleType import pytest -from PIL import Image, ImageFile, TiffImagePlugin, UnidentifiedImageError +from PIL import ( + Image, + ImageFile, + JpegImagePlugin, + TiffImagePlugin, + UnidentifiedImageError, +) from PIL.TiffImagePlugin import RESOLUTION_UNIT, X_RESOLUTION, Y_RESOLUTION from .helper import ( @@ -108,6 +114,8 @@ class TestFileTiff: assert_image_equal_tofile(im, "Tests/images/hopper.tif") with Image.open("Tests/images/hopper_bigtiff.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # multistrip support not yet implemented del im.tag_v2[273] @@ -127,6 +135,8 @@ class TestFileTiff: def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy api assert isinstance(im.tag[X_RESOLUTION][0], tuple) assert isinstance(im.tag[Y_RESOLUTION][0], tuple) @@ -140,6 +150,8 @@ class TestFileTiff: def test_xyres_fallback_tiff(self) -> None: filename = "Tests/images/compression.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 api assert isinstance(im.tag_v2[X_RESOLUTION], TiffImagePlugin.IFDRational) assert isinstance(im.tag_v2[Y_RESOLUTION], TiffImagePlugin.IFDRational) @@ -154,6 +166,8 @@ class TestFileTiff: def test_int_resolution(self) -> None: filename = "Tests/images/pil168.tif" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # Try to read a file where X,Y_RESOLUTION are ints im.tag_v2[X_RESOLUTION] = 71 im.tag_v2[Y_RESOLUTION] = 71 @@ -168,6 +182,7 @@ class TestFileTiff: with Image.open( "Tests/images/hopper_float_dpi_" + str(resolution_unit) + ".tif" ) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.get(RESOLUTION_UNIT) == resolution_unit assert im.info["dpi"] == (dpi, dpi) @@ -185,6 +200,7 @@ class TestFileTiff: with Image.open("Tests/images/10ct_32bit_128.tiff") as im: im.save(b, format="tiff", resolution=123.45) with Image.open(b) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2[X_RESOLUTION] == 123.45 assert im.tag_v2[Y_RESOLUTION] == 123.45 @@ -200,10 +216,12 @@ class TestFileTiff: TiffImagePlugin.PREFIXES.pop() def test_bad_exif(self) -> None: - with Image.open("Tests/images/hopper_bad_exif.jpg") as i: + with Image.open("Tests/images/hopper_bad_exif.jpg") as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) + # Should not raise struct.error. with pytest.warns(UserWarning): - i._getexif() + im._getexif() def test_save_rgba(self, tmp_path: Path) -> None: im = hopper("RGBA") @@ -294,11 +312,13 @@ class TestFileTiff: ) def test_n_frames(self, path: str, n_frames: int) -> None: with Image.open(path) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == n_frames assert im.is_animated == (n_frames != 1) def test_eoferror(self) -> None: with Image.open("Tests/images/multipage-lastframe.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) n_frames = im.n_frames # Test seeking past the last frame @@ -342,20 +362,24 @@ class TestFileTiff: def test_frame_order(self) -> None: # A frame can't progress to itself after reading with Image.open("Tests/images/multipage_single_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 1 # A frame can't progress to a frame that has already been read with Image.open("Tests/images/multipage_multiple_frame_loop.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 2 # Frames don't have to be in sequence with Image.open("Tests/images/multipage_out_of_order.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.n_frames == 3 def test___str__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: # Act + assert isinstance(im, TiffImagePlugin.TiffImageFile) ret = str(im.ifd) # Assert @@ -365,6 +389,8 @@ class TestFileTiff: # Arrange filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # v2 interface v2_tags = { 256: 55, @@ -404,6 +430,7 @@ class TestFileTiff: def test__delitem__(self) -> None: filename = "Tests/images/pil136.tiff" with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) len_before = len(dict(im.ifd)) del im.ifd[256] len_after = len(dict(im.ifd)) @@ -436,6 +463,7 @@ class TestFileTiff: def test_ifd_tag_type(self) -> None: with Image.open("Tests/images/ifd_tag_type.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert 0x8825 in im.tag_v2 def test_exif(self, tmp_path: Path) -> None: @@ -524,6 +552,7 @@ class TestFileTiff: im = hopper(mode) im.save(filename, tiffinfo={262: 0}) with Image.open(filename) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[262] == 0 assert_image_equal(im, reloaded) @@ -602,6 +631,8 @@ class TestFileTiff: filename = str(tmp_path / "temp.tif") hopper("RGB").save(filename, "TIFF", **kwargs) with Image.open(filename) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + # legacy interface assert im.tag[X_RESOLUTION][0][0] == 72 assert im.tag[Y_RESOLUTION][0][0] == 36 @@ -676,6 +707,7 @@ class TestFileTiff: def test_planar_configuration_save(self, tmp_path: Path) -> None: infile = "Tests/images/tiff_tiled_planar_raw.tif" with Image.open(infile) as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im._planar_configuration == 2 outfile = str(tmp_path / "temp.tif") @@ -703,12 +735,13 @@ class TestFileTiff: def test_tiff_save_all(self) -> None: mp = BytesIO() - with Image.open("Tests/images/multipage.tiff") as im: - im.save(mp, format="tiff", save_all=True) + with Image.open("Tests/images/multipage.tiff") as img: + img.save(mp, format="tiff", save_all=True) mp.seek(0, os.SEEK_SET) - with Image.open(mp) as im: - assert im.n_frames == 3 + with Image.open(mp) as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) + assert img.n_frames == 3 # Test appending images mp = BytesIO() @@ -718,6 +751,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 # Test appending using a generator @@ -729,6 +763,7 @@ class TestFileTiff: mp.seek(0, os.SEEK_SET) with Image.open(mp) as reread: + assert isinstance(reread, TiffImagePlugin.TiffImageFile) assert reread.n_frames == 3 def test_saving_icc_profile(self, tmp_path: Path) -> None: @@ -792,6 +827,7 @@ class TestFileTiff: def test_get_photoshop_blocks(self) -> None: with Image.open("Tests/images/lab.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert list(im.get_photoshop_blocks().keys()) == [ 1061, 1002, @@ -846,6 +882,7 @@ class TestFileTiff: im = Image.open(tmpfile) fp = im.fp + assert fp is not None assert not fp.closed im.load() assert fp.closed @@ -859,6 +896,7 @@ class TestFileTiff: with open(tmpfile, "rb") as f: im = Image.open(f) fp = im.fp + assert fp is not None assert not fp.closed im.load() assert not fp.closed @@ -910,8 +948,9 @@ class TestFileTiffW32: im.save(tmpfile) im = Image.open(tmpfile) + assert im.fp is not None + assert not im.fp.closed fp = im.fp - assert not fp.closed with pytest.raises(OSError): os.remove(tmpfile) im.load() diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py index 1e0310001..c131e16f4 100644 --- a/Tests/test_file_tiff_metadata.py +++ b/Tests/test_file_tiff_metadata.py @@ -61,6 +61,7 @@ def test_rt_metadata(tmp_path: Path) -> None: img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),) @@ -80,12 +81,14 @@ def test_rt_metadata(tmp_path: Path) -> None: info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8) img.save(f, tiffinfo=info) with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8) def test_read_metadata() -> None: with Image.open("Tests/images/hopper_g4.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) assert { "YResolution": IFDRational(4294967295, 113653537), "PlanarConfiguration": 1, @@ -128,6 +131,8 @@ def test_read_metadata() -> None: def test_write_metadata(tmp_path: Path) -> None: """Test metadata writing through the python code""" with Image.open("Tests/images/hopper.tif") as img: + assert isinstance(img, TiffImagePlugin.TiffImageFile) + f = str(tmp_path / "temp.tiff") del img.tag[278] img.save(f, tiffinfo=img.tag) @@ -135,6 +140,7 @@ def test_write_metadata(tmp_path: Path) -> None: original = img.tag_v2.named() with Image.open(f) as loaded: + assert isinstance(loaded, TiffImagePlugin.TiffImageFile) reloaded = loaded.tag_v2.named() ignored = ["StripByteCounts", "RowsPerStrip", "PageNumber", "StripOffsets"] @@ -165,19 +171,21 @@ def test_write_metadata(tmp_path: Path) -> None: def test_change_stripbytecounts_tag_type(tmp_path: Path) -> None: out = str(tmp_path / "temp.tiff") with Image.open("Tests/images/hopper.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) info = im.tag_v2 del info[278] # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT - im = im.resize((500, 500)) - info[TiffImagePlugin.IMAGEWIDTH] = im.width + resized_im = im.resize((500, 500)) + info[TiffImagePlugin.IMAGEWIDTH] = resized_im.width # STRIPBYTECOUNTS can be a SHORT or a LONG info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT - im.save(out, tiffinfo=info) + resized_im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG @@ -208,6 +216,7 @@ def test_writing_other_types_to_ascii( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[271] == expected @@ -225,6 +234,7 @@ def test_writing_other_types_to_bytes(value: int | IFDRational, tmp_path: Path) im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[700] == b"\x01" @@ -244,6 +254,7 @@ def test_writing_other_types_to_undefined( im.save(out, tiffinfo=info) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[33723] == b"1" @@ -288,6 +299,7 @@ def test_iccprofile_binary() -> None: # but probably won't be able to save it. with Image.open("Tests/images/hopper.iccprofile_binary.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert im.tag_v2.tagtype[34675] == 1 assert im.info["icc_profile"] @@ -313,6 +325,7 @@ def test_exif_div_zero(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 0 == reloaded.tag_v2[41988].numerator assert 0 == reloaded.tag_v2[41988].denominator @@ -332,6 +345,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -344,6 +358,7 @@ def test_ifd_unsigned_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert max_long == reloaded.tag_v2[41493].numerator assert 1 == reloaded.tag_v2[41493].denominator @@ -362,6 +377,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -374,6 +390,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert numerator == reloaded.tag_v2[37380].numerator assert denominator == reloaded.tag_v2[37380].denominator @@ -387,6 +404,7 @@ def test_ifd_signed_rational(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert 2**31 - 1 == reloaded.tag_v2[37380].numerator assert -1 == reloaded.tag_v2[37380].denominator @@ -401,6 +419,7 @@ def test_ifd_signed_long(tmp_path: Path) -> None: im.save(out, tiffinfo=info, compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert reloaded.tag_v2[37000] == -60000 @@ -421,11 +440,13 @@ def test_empty_values() -> None: def test_photoshop_info(tmp_path: Path) -> None: with Image.open("Tests/images/issue_2278.tif") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) assert len(im.tag_v2[34377]) == 70 assert isinstance(im.tag_v2[34377], bytes) out = str(tmp_path / "temp.tiff") im.save(out) with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert len(reloaded.tag_v2[34377]) == 70 assert isinstance(reloaded.tag_v2[34377], bytes) diff --git a/Tests/test_file_wal.py b/Tests/test_file_wal.py index b34975e83..bd1036712 100644 --- a/Tests/test_file_wal.py +++ b/Tests/test_file_wal.py @@ -21,7 +21,9 @@ def test_open() -> None: def test_load() -> None: with WalImageFile.open(TEST_FILE) as im: - assert im.load()[0, 0] == 122 + px = im.load() + assert px is not None + assert px[0, 0] == 122 # Test again now that it has already been loaded once - assert im.load()[0, 0] == 122 + assert px[0, 0] == 122 diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index e75e3ddd2..f305dbb95 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -218,6 +218,7 @@ class TestFileWebp: # Save P mode GIF with background with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) + assert isinstance(original_value, tuple) # Save as WEBP out_webp = str(tmp_path / "temp.webp") @@ -230,6 +231,7 @@ class TestFileWebp: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) + assert isinstance(reread_value, tuple) difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) assert difference < 5 diff --git a/Tests/test_file_webp_animated.py b/Tests/test_file_webp_animated.py index 967a0aae8..9035d9057 100644 --- a/Tests/test_file_webp_animated.py +++ b/Tests/test_file_webp_animated.py @@ -6,7 +6,7 @@ from pathlib import Path import pytest from packaging.version import parse as parse_version -from PIL import Image, features +from PIL import GifImagePlugin, Image, WebPImagePlugin, features from .helper import ( assert_image_equal, @@ -22,10 +22,12 @@ def test_n_frames() -> None: """Ensure that WebP format sets n_frames and is_animated attributes correctly.""" with Image.open("Tests/images/hopper.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 1 assert not im.is_animated with Image.open("Tests/images/iss634.webp") as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 42 assert im.is_animated @@ -37,11 +39,13 @@ def test_write_animation_L(tmp_path: Path) -> None: """ with Image.open("Tests/images/iss634.gif") as orig: + assert isinstance(orig, GifImagePlugin.GifImageFile) assert orig.n_frames > 1 temp_file = str(tmp_path / "temp.webp") orig.save(temp_file, save_all=True) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == orig.n_frames # Compare first and last frames to the original animated GIF @@ -69,6 +73,7 @@ def test_write_animation_RGB(tmp_path: Path) -> None: def check(temp_file: str) -> None: with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 2 # Compare first frame to original @@ -127,6 +132,7 @@ def test_timestamp_and_duration(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated @@ -170,6 +176,7 @@ def test_seeking(tmp_path: Path) -> None: ) with Image.open(temp_file) as im: + assert isinstance(im, WebPImagePlugin.WebPImageFile) assert im.n_frames == 5 assert im.is_animated diff --git a/Tests/test_file_webp_metadata.py b/Tests/test_file_webp_metadata.py index d9a834c75..6ef9c34e3 100644 --- a/Tests/test_file_webp_metadata.py +++ b/Tests/test_file_webp_metadata.py @@ -6,7 +6,7 @@ from types import ModuleType import pytest -from PIL import Image +from PIL import Image, WebPImagePlugin from .helper import mark_if_feature_version, skip_unless_feature @@ -22,11 +22,13 @@ except ImportError: def test_read_exif_metadata() -> None: file_path = "Tests/images/flower.webp" with Image.open(file_path) as image: + assert isinstance(image, WebPImagePlugin.WebPImageFile) assert image.format == "WEBP" exif_data = image.info.get("exif", None) assert exif_data exif = image._getexif() + assert exif is not None # Camera make assert exif[271] == "Canon" @@ -110,6 +112,7 @@ def test_read_no_exif() -> None: test_buffer.seek(0) with Image.open(test_buffer) as webp_image: + assert isinstance(webp_image, WebPImagePlugin.WebPImageFile) assert not webp_image._getexif() diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 79e707263..49e9c2c23 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -31,7 +31,9 @@ def test_load_raw() -> None: def test_load() -> None: with Image.open("Tests/images/drawing.emf") as im: if hasattr(Image.core, "drawwmf"): - assert im.load()[0, 0] == (255, 255, 255) + px = im.load() + assert px is not None + assert px[0, 0] == (255, 255, 255) def test_register_handler(tmp_path: Path) -> None: @@ -64,6 +66,7 @@ def test_load_float_dpi() -> None: def test_load_set_dpi() -> None: with Image.open("Tests/images/drawing.wmf") as im: + assert isinstance(im, WmfImagePlugin.WmfStubImageFile) assert im.size == (82, 82) if hasattr(Image.core, "drawwmf"): diff --git a/Tests/test_file_xpm.py b/Tests/test_file_xpm.py index 26afe93f4..73c62a44d 100644 --- a/Tests/test_file_xpm.py +++ b/Tests/test_file_xpm.py @@ -30,6 +30,7 @@ def test_invalid_file() -> None: def test_load_read() -> None: # Arrange with Image.open(TEST_FILE) as im: + assert isinstance(im, XpmImagePlugin.XpmImageFile) dummy_bytes = 1 # Act diff --git a/Tests/test_image.py b/Tests/test_image.py index 97e97acaa..f0877c5d0 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -605,8 +605,8 @@ class TestImage: assert im.mode == mode assert im.getpixel((0, 0)) == 0 assert im.getpixel((255, 255)) == 255 - with Image.open(target_file) as target: - target = target.convert(mode) + with Image.open(target_file) as img: + target = img.convert(mode) assert_image_equal(im, target) def test_radial_gradient_wrong_mode(self) -> None: @@ -630,8 +630,8 @@ class TestImage: assert im.mode == mode assert im.getpixel((0, 0)) == 255 assert im.getpixel((128, 128)) == 0 - with Image.open(target_file) as target: - target = target.convert(mode) + with Image.open(target_file) as img: + target = img.convert(mode) assert_image_equal(im, target) def test_register_extensions(self) -> None: @@ -652,8 +652,8 @@ class TestImage: def test_remap_palette(self) -> None: # Test identity transform - with Image.open("Tests/images/hopper.gif") as im: - assert_image_equal(im, im.remap_palette(list(range(256)))) + with Image.open("Tests/images/hopper.gif") as img: + assert_image_equal(img, img.remap_palette(list(range(256)))) # Test identity transform with an RGBA palette im = Image.new("P", (256, 1)) @@ -662,12 +662,14 @@ class TestImage: im.putpalette(list(range(256)) * 4, "RGBA") im_remapped = im.remap_palette(list(range(256))) assert_image_equal(im, im_remapped) + assert im.palette is not None + assert im_remapped.palette is not None assert im.palette.palette == im_remapped.palette.palette # Test illegal image mode with hopper() as im: with pytest.raises(ValueError): - im.remap_palette(None) + im.remap_palette([]) def test_remap_palette_transparency(self) -> None: im = Image.new("P", (1, 2), (0, 0, 0)) @@ -768,7 +770,7 @@ class TestImage: assert dict(exif) # Test that exif data is cleared after another load - exif.load(None) + exif.load(b"") assert not dict(exif) # Test loading just the EXIF header diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py index 6a925975e..3ed5784b5 100644 --- a/Tests/test_image_convert.py +++ b/Tests/test_image_convert.py @@ -76,8 +76,8 @@ def test_8bit() -> None: def test_16bit() -> None: - with Image.open("Tests/images/16bit.cropped.tif") as im: - _test_float_conversion(im) + with Image.open("Tests/images/16bit.cropped.tif") as img: + _test_float_conversion(img) for color in (65535, 65536): im = Image.new("I", (1, 1), color) @@ -236,6 +236,7 @@ def test_gif_with_rgba_palette_to_p() -> None: with Image.open("Tests/images/hopper.gif") as im: im.info["transparency"] = 255 im.load() + assert im.palette is not None assert im.palette.mode == "RGB" im_p = im.convert("P") diff --git a/Tests/test_image_crop.py b/Tests/test_image_crop.py index 07fec2e64..b90ce84bc 100644 --- a/Tests/test_image_crop.py +++ b/Tests/test_image_crop.py @@ -78,13 +78,13 @@ def test_crop_crash() -> None: extents = (1, 1, 10, 10) # works prepatch with Image.open(test_img) as img: - img2 = img.crop(extents) - img2.load() + img1 = img.crop(extents) + img1.load() # fail prepatch with Image.open(test_img) as img: - img = img.crop(extents) - img.load() + img2 = img.crop(extents) + img2.load() def test_crop_zero() -> None: diff --git a/Tests/test_image_load.py b/Tests/test_image_load.py index 4f1d63b8f..1d5f0d17c 100644 --- a/Tests/test_image_load.py +++ b/Tests/test_image_load.py @@ -38,6 +38,7 @@ def test_close_after_load(caplog: pytest.LogCaptureFixture) -> None: def test_contextmanager() -> None: fn = None with Image.open("Tests/images/hopper.gif") as im: + assert im.fp is not None fn = im.fp.fileno() os.fstat(fn) diff --git a/Tests/test_image_putpalette.py b/Tests/test_image_putpalette.py index f2c447f71..661764b60 100644 --- a/Tests/test_image_putpalette.py +++ b/Tests/test_image_putpalette.py @@ -62,6 +62,7 @@ def test_putpalette_with_alpha_values() -> None: expected = im.convert("RGBA") palette = im.getpalette() + assert palette is not None transparency = im.info.pop("transparency") palette_with_alpha_values = [] diff --git a/Tests/test_image_quantize.py b/Tests/test_image_quantize.py index 7c564d967..432ec8707 100644 --- a/Tests/test_image_quantize.py +++ b/Tests/test_image_quantize.py @@ -56,20 +56,21 @@ def test_rgba_quantize() -> None: def test_quantize() -> None: with Image.open("Tests/images/caption_6_33_22.png") as image: - image = image.convert("RGB") - converted = image.quantize() + converted = image.convert("RGB") + converted = converted.quantize() assert converted.mode == "P" assert_image_similar(converted.convert("RGB"), image, 1) def test_quantize_no_dither() -> None: - image = hopper() + palette: Image.Image with Image.open("Tests/images/caption_6_33_22.png") as palette: palette = palette.convert("P") - converted = image.quantize(dither=Image.Dither.NONE, palette=palette) + converted = hopper().quantize(dither=Image.Dither.NONE, palette=palette) assert converted.mode == "P" assert converted.palette is not None + assert palette.palette is not None assert converted.palette.palette == palette.palette.palette @@ -93,8 +94,8 @@ def test_quantize_no_dither2() -> None: def test_quantize_dither_diff() -> None: image = hopper() - with Image.open("Tests/images/caption_6_33_22.png") as palette: - palette = palette.convert("P") + with Image.open("Tests/images/caption_6_33_22.png") as im: + palette = im.convert("P") dither = image.quantize(dither=Image.Dither.FLOYDSTEINBERG, palette=palette) nodither = image.quantize(dither=Image.Dither.NONE, palette=palette) diff --git a/Tests/test_image_resize.py b/Tests/test_image_resize.py index c9e304512..b33e264c0 100644 --- a/Tests/test_image_resize.py +++ b/Tests/test_image_resize.py @@ -297,14 +297,14 @@ class TestImageResize: # Test unknown resampling filter with hopper() as im: with pytest.raises(ValueError): - im.resize((10, 10), "unknown") + im.resize((10, 10), -1) @skip_unless_feature("libtiff") def test_load_first(self) -> None: # load() may change the size of the image # Test that resize() is calling it before getting the size - with Image.open("Tests/images/g4_orientation_5.tif") as im: - im = im.resize((64, 64)) + with Image.open("Tests/images/g4_orientation_5.tif") as img: + im = img.resize((64, 64)) assert im.size == (64, 64) @pytest.mark.parametrize("mode", ("L", "RGB", "I", "F")) diff --git a/Tests/test_image_rotate.py b/Tests/test_image_rotate.py index 252a15db7..317328469 100644 --- a/Tests/test_image_rotate.py +++ b/Tests/test_image_rotate.py @@ -40,8 +40,8 @@ def test_mode(mode: str) -> None: @pytest.mark.parametrize("angle", (0, 90, 180, 270)) def test_angle(angle: int) -> None: - with Image.open("Tests/images/test-card.png") as im: - rotate(im, im.mode, angle) + with Image.open("Tests/images/test-card.png") as img: + rotate(img, img.mode, angle) im = hopper() assert_image_equal(im.rotate(angle), im.rotate(angle, expand=1)) @@ -74,6 +74,7 @@ def test_center_0() -> None: im = hopper() im = im.rotate(45, center=(0, 0), resample=Image.Resampling.BICUBIC) + target: Image.Image with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 target = target.crop((0, target_origin, 128, target_origin + 128)) @@ -85,6 +86,7 @@ def test_center_14() -> None: im = hopper() im = im.rotate(45, center=(14, 14), resample=Image.Resampling.BICUBIC) + target: Image.Image with Image.open("Tests/images/hopper_45.png") as target: target_origin = target.size[1] / 2 - 14 target = target.crop((6, target_origin, 128 + 6, target_origin + 128)) @@ -94,6 +96,8 @@ def test_center_14() -> None: def test_translate() -> None: im = hopper() + + target: Image.Image with Image.open("Tests/images/hopper_45.png") as target: target_origin = (target.size[1] / 2 - 64) - 5 target = target.crop( diff --git a/Tests/test_image_thumbnail.py b/Tests/test_image_thumbnail.py index bdbf09c40..d97232b62 100644 --- a/Tests/test_image_thumbnail.py +++ b/Tests/test_image_thumbnail.py @@ -106,20 +106,20 @@ def test_load_first() -> None: assert im.size == (590, 88) -def test_load_first_unless_jpeg() -> None: +def test_load_first_unless_jpeg(monkeypatch: pytest.MonkeyPatch) -> None: # Test that thumbnail() still uses draft() for JPEG with Image.open("Tests/images/hopper.jpg") as im: - draft = im.draft + orig_draft = im.draft def im_draft( mode: str, size: tuple[int, int] ) -> tuple[str, tuple[int, int, float, float]] | None: - result = draft(mode, size) + result = orig_draft(mode, size) assert result is not None return result - im.draft = im_draft + monkeypatch.setattr(im, "draft", im_draft) im.thumbnail((64, 64)) @@ -158,6 +158,7 @@ def test_reducing_gap_values() -> None: def test_reducing_gap_for_DCT_scaling() -> None: + ref: Image.Image with Image.open("Tests/images/hopper.jpg") as ref: # thumbnail should call draft with reducing_gap scale ref.draft(None, (18 * 3, 18 * 3)) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7e83396de..818f784b1 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -47,6 +47,8 @@ class TestImageTransform: transformed = im.transform( im.size, Image.Transform.AFFINE, [1, 0, 0, 0, 1, 0] ) + assert im.palette is not None + assert transformed.palette is not None assert im.palette.palette == transformed.palette.palette def test_extent(self) -> None: @@ -245,14 +247,14 @@ class TestImageTransform: def test_missing_method_data(self) -> None: with hopper() as im: with pytest.raises(ValueError): - im.transform((100, 100), None) + im.transform((100, 100), None) # type: ignore[arg-type] @pytest.mark.parametrize("resample", (Image.Resampling.BOX, "unknown")) def test_unknown_resampling_filter(self, resample: Image.Resampling | str) -> None: with hopper() as im: (w, h) = im.size with pytest.raises(ValueError): - im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) + im.transform((100, 100), Image.Transform.EXTENT, (0, 0, w, h), resample) # type: ignore[arg-type] class TestImageTransformAffine: diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index e852b8471..be5a3358e 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -190,6 +190,7 @@ def test_bitmap() -> None: # Arrange im = Image.new("RGB", (W, H)) draw = ImageDraw.Draw(im) + small: Image.Image with Image.open("Tests/images/pil123rgba.png") as small: small = small.resize((50, 50), Image.Resampling.NEAREST) diff --git a/Tests/test_imageops.py b/Tests/test_imageops.py index 2fb2a60b6..0c5254805 100644 --- a/Tests/test_imageops.py +++ b/Tests/test_imageops.py @@ -249,8 +249,8 @@ def test_colorize_2color() -> None: # Test the colorizing function with 2-color functionality # Open test image (256px by 10px, black to white) - with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + with Image.open("Tests/images/bw_gradient.png") as img: + im = img.convert("L") # Create image with original 2-color functionality im_test = ImageOps.colorize(im, "red", "green") @@ -289,8 +289,8 @@ def test_colorize_2color_offset() -> None: # Test the colorizing function with 2-color functionality and offset # Open test image (256px by 10px, black to white) - with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + with Image.open("Tests/images/bw_gradient.png") as img: + im = img.convert("L") # Create image with original 2-color functionality with offsets im_test = ImageOps.colorize( @@ -331,8 +331,8 @@ def test_colorize_3color_offset() -> None: # Test the colorizing function with 3-color functionality and offset # Open test image (256px by 10px, black to white) - with Image.open("Tests/images/bw_gradient.png") as im: - im = im.convert("L") + with Image.open("Tests/images/bw_gradient.png") as img: + im = img.convert("L") # Create image with new three color functionality with offsets im_test = ImageOps.colorize( @@ -428,6 +428,7 @@ def test_exif_transpose() -> None: check(orientation_im) # Orientation from "XML:com.adobe.xmp" info key + im: Image.Image for suffix in ("", "_exiftool"): with Image.open("Tests/images/xmp_tags_orientation" + suffix + ".png") as im: assert im.getexif()[0x0112] == 3 @@ -442,10 +443,10 @@ def test_exif_transpose() -> None: # Orientation from "Raw profile type exif" info key # This test image has been manually hexedited from exif_imagemagick.png # to have a different orientation - with Image.open("Tests/images/exif_imagemagick_orientation.png") as im: - assert im.getexif()[0x0112] == 3 + with Image.open("Tests/images/exif_imagemagick_orientation.png") as img: + assert img.getexif()[0x0112] == 3 - transposed_im = ImageOps.exif_transpose(im) + transposed_im = ImageOps.exif_transpose(img) assert transposed_im is not None assert 0x0112 not in transposed_im.getexif() diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index 6cf0079dd..e2f8308ea 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -17,6 +17,7 @@ def test_sanity() -> None: def test_reload() -> None: with Image.open("Tests/images/hopper.gif") as im: original = im.copy() + assert im.palette is not None im.palette.dirty = 1 assert_image_equal(im.convert("RGB"), original.convert("RGB")) diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 9b37435eb..ff55073f5 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from PIL import Image, ImageSequence, TiffImagePlugin +from PIL import Image, ImageSequence, PsdImagePlugin, TiffImagePlugin from .helper import assert_image_equal, hopper, skip_unless_feature @@ -31,6 +31,8 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: + assert isinstance(im, TiffImagePlugin.TiffImageFile) + i = ImageSequence.Iterator(im) for index in range(0, im.n_frames): assert i[index] == next(i) @@ -42,6 +44,8 @@ def test_iterator() -> None: def test_iterator_min_frame() -> None: with Image.open("Tests/images/hopper.psd") as im: + assert isinstance(im, PsdImagePlugin.PsdImageFile) + i = ImageSequence.Iterator(im) for index in range(1, im.n_frames): assert i[index] == next(i) @@ -74,9 +78,14 @@ def test_consecutive() -> None: def test_palette_mmap() -> None: # Using mmap in ImageFile can require to reload the palette. with Image.open("Tests/images/multipage-mmap.tiff") as im: - color1 = im.getpalette()[:3] + palette = im.getpalette() + assert palette is not None + color1 = palette[:3] im.seek(0) - color2 = im.getpalette()[:3] + + palette = im.getpalette() + assert palette is not None + color2 = palette[:3] assert color1 == color2 diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index be143e9c6..15760b465 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -17,6 +17,7 @@ def helper_pickle_file( tmp_path: Path, protocol: int, test_file: str, mode: str | None ) -> None: # Arrange + im: Image.Image with Image.open(test_file) as im: filename = str(tmp_path / "temp.pkl") if mode: @@ -33,6 +34,7 @@ def helper_pickle_file( def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> None: + im: Image.Image with Image.open(test_file) as im: if mode: im = im.convert(mode) @@ -77,8 +79,8 @@ def test_pickle_image( def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: # Arrange filename = str(tmp_path / "temp.pkl") - with Image.open("Tests/images/hopper.jpg") as im: - im = im.convert("PA") + with Image.open("Tests/images/hopper.jpg") as img: + im = img.convert("PA") # Act / Assert for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): diff --git a/Tests/test_shell_injection.py b/Tests/test_shell_injection.py index dd4fc46c3..508d89352 100644 --- a/Tests/test_shell_injection.py +++ b/Tests/test_shell_injection.py @@ -39,6 +39,7 @@ class TestShellInjection: shutil.copy(TEST_JPG, src_file) with Image.open(src_file) as im: + assert isinstance(im, JpegImagePlugin.JpegImageFile) im.load_djpeg() @pytest.mark.skipif(not cjpeg_available(), reason="cjpeg not available") @@ -49,11 +50,13 @@ class TestShellInjection: @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_filename_bmp_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: - im = im.convert("RGB") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + im_rgb = im.convert("RGB") + self.assert_save_filename_check( + tmp_path, im_rgb, GifImagePlugin._save_netpbm + ) @pytest.mark.skipif(not netpbm_available(), reason="Netpbm not available") def test_save_netpbm_filename_l_mode(self, tmp_path: Path) -> None: with Image.open(TEST_GIF) as im: - im = im.convert("L") - self.assert_save_filename_check(tmp_path, im, GifImagePlugin._save_netpbm) + im_l = im.convert("L") + self.assert_save_filename_check(tmp_path, im_l, GifImagePlugin._save_netpbm) diff --git a/Tests/test_tiff_ifdrational.py b/Tests/test_tiff_ifdrational.py index 13f1f9c80..6df1e4557 100644 --- a/Tests/test_tiff_ifdrational.py +++ b/Tests/test_tiff_ifdrational.py @@ -72,4 +72,5 @@ def test_ifd_rational_save( im.save(out, dpi=(res, res), compression="raw") with Image.open(out) as reloaded: + assert isinstance(reloaded, TiffImagePlugin.TiffImageFile) assert float(IFDRational(301, 1)) == float(reloaded.tag_v2[282]) diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index 10828bff0..b464ae58d 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -213,6 +213,7 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index e5605635e..aa9ba9e27 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -258,6 +258,7 @@ class BlpImageFile(ImageFile.ImageFile): format_description = "Blizzard Mipmap Format" def _open(self) -> None: + assert self.fp is not None self.magic = self.fp.read(4) self.fp.seek(5, os.SEEK_CUR) diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index bf8f29577..45b608d85 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -74,6 +74,7 @@ class BmpImageFile(ImageFile.ImageFile): def _bitmap(self, header: int = 0, offset: int = 0) -> None: """Read relevant info about the BMP""" + assert self.fp is not None read, seek = self.fp.read, self.fp.seek if header: seek(header) @@ -307,6 +308,7 @@ class BmpImageFile(ImageFile.ImageFile): def _open(self) -> None: """Open file, check magic number and read header""" # read 14 bytes: magic number, filesize, reserved, header final offset + assert self.fp is not None head_data = self.fp.read(14) # choke if the file does not have the required magic bytes if not _accept(head_data): diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0ee2f653b..521e5defd 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -40,6 +40,7 @@ class BufrStubImageFile(ImageFile.StubImageFile): format_description = "BUFR" def _open(self) -> None: + assert self.fp is not None offset = self.fp.tell() if not _accept(self.fp.read(4)): diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py index c4be0ceca..4688a30f7 100644 --- a/src/PIL/CurImagePlugin.py +++ b/src/PIL/CurImagePlugin.py @@ -38,6 +38,7 @@ class CurImageFile(BmpImagePlugin.BmpImageFile): format_description = "Windows Cursor" def _open(self) -> None: + assert self.fp is not None offset = self.fp.tell() # check magic diff --git a/src/PIL/DcxImagePlugin.py b/src/PIL/DcxImagePlugin.py index f67f27d73..d3f456ddc 100644 --- a/src/PIL/DcxImagePlugin.py +++ b/src/PIL/DcxImagePlugin.py @@ -24,6 +24,7 @@ from __future__ import annotations from . import Image from ._binary import i32le as i32 +from ._util import DeferredError from .PcxImagePlugin import PcxImageFile MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? @@ -44,6 +45,7 @@ class DcxImageFile(PcxImageFile): def _open(self) -> None: # Header + assert self.fp is not None s = self.fp.read(4) if not _accept(s): msg = "not a DCX file" @@ -66,6 +68,8 @@ class DcxImageFile(PcxImageFile): def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.frame = frame self.fp = self._fp self.fp.seek(self._offset[frame]) diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 1b6408237..540d12ad6 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -333,6 +333,7 @@ class DdsImageFile(ImageFile.ImageFile): format_description = "DirectDraw Surface" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a DDS file" raise SyntaxError(msg) diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index dd6ae4a77..f71e7e151 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -184,6 +184,7 @@ class EpsImageFile(ImageFile.ImageFile): mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"} def _open(self) -> None: + assert self.fp is not None (length, offset) = self._find_offset(self.fp) # go to offset - start of "%!PS" @@ -377,6 +378,7 @@ class EpsImageFile(ImageFile.ImageFile): ) -> Image.core.PixelAccess | None: # Load EPS via Ghostscript if self.tile: + assert self.fp is not None self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency) self._mode = self.im.mode self._size = self.im.size diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py index 666390be9..f46232963 100644 --- a/src/PIL/FliImagePlugin.py +++ b/src/PIL/FliImagePlugin.py @@ -22,6 +22,7 @@ from . import Image, ImageFile, ImagePalette from ._binary import i16le as i16 from ._binary import i32le as i32 from ._binary import o8 +from ._util import DeferredError # # decoder @@ -47,6 +48,7 @@ class FliImageFile(ImageFile.ImageFile): def _open(self) -> None: # HEAD + assert self.fp is not None s = self.fp.read(128) if not (_accept(s) and s[20:22] == b"\x00\x00"): msg = "not an FLI/FLC file" @@ -110,6 +112,7 @@ class FliImageFile(ImageFile.ImageFile): # load palette i = 0 + assert self.fp is not None for e in range(i16(self.fp.read(2))): s = self.fp.read(2) i = i + s[0] @@ -134,6 +137,8 @@ class FliImageFile(ImageFile.ImageFile): self._seek(f) def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: self.__frame = -1 self._fp.seek(self.__rewind) diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py index 8fef51076..d650be026 100644 --- a/src/PIL/FpxImagePlugin.py +++ b/src/PIL/FpxImagePlugin.py @@ -58,6 +58,7 @@ class FpxImageFile(ImageFile.ImageFile): # read the OLE directory and see if this is a likely # to be a FlashPix file + assert self.fp is not None try: self.ole = olefile.OleFileIO(self.fp) except OSError as e: @@ -229,6 +230,7 @@ class FpxImageFile(ImageFile.ImageFile): if y >= ysize: break # isn't really required + assert self.fp is not None self.stream = stream self._fp = self.fp self.fp = None diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py index ddb469bc3..a875a097f 100644 --- a/src/PIL/FtexImagePlugin.py +++ b/src/PIL/FtexImagePlugin.py @@ -72,6 +72,7 @@ class FtexImageFile(ImageFile.ImageFile): format_description = "Texture File Format (IW2:EOC)" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not an FTEX file" raise SyntaxError(msg) diff --git a/src/PIL/GbrImagePlugin.py b/src/PIL/GbrImagePlugin.py index f319d7e84..a61bb883f 100644 --- a/src/PIL/GbrImagePlugin.py +++ b/src/PIL/GbrImagePlugin.py @@ -42,6 +42,7 @@ class GbrImageFile(ImageFile.ImageFile): format_description = "GIMP brush file" def _open(self) -> None: + assert self.fp is not None header_size = i32(self.fp.read(4)) if header_size < 20: msg = "not a GIMP brush" @@ -91,6 +92,8 @@ class GbrImageFile(ImageFile.ImageFile): def load(self) -> Image.core.PixelAccess | None: if self._im is None: self.im = Image.core.new(self.mode, self.size) + + assert self.fp is not None self.frombytes(self.fp.read(self._data_size)) return Image.Image.load(self) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index f206fbb9c..0d96f457c 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -31,7 +31,7 @@ import os import subprocess from enum import IntEnum from functools import cached_property -from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union +from typing import IO, TYPE_CHECKING, Any, Literal, NamedTuple, Union, cast from . import ( Image, @@ -45,6 +45,7 @@ from . import ( from ._binary import i16le as i16 from ._binary import o8 from ._binary import o16le as o16 +from ._util import DeferredError if TYPE_CHECKING: from . import _imaging @@ -83,6 +84,7 @@ class GifImageFile(ImageFile.ImageFile): global_palette = None def data(self) -> bytes | None: + assert self.fp is not None s = self.fp.read(1) if s and s[0]: return self.fp.read(s[0]) @@ -96,6 +98,7 @@ class GifImageFile(ImageFile.ImageFile): def _open(self) -> None: # Screen + assert self.fp is not None s = self.fp.read(13) if not _accept(s): msg = "not a GIF file" @@ -113,8 +116,8 @@ class GifImageFile(ImageFile.ImageFile): # check if palette contains colour indices p = self.fp.read(3 << bits) if self._is_palette_needed(p): - p = ImagePalette.raw("RGB", p) - self.global_palette = self.palette = p + palette = ImagePalette.raw("RGB", p) + self.global_palette = self.palette = palette self._fp = self.fp # FIXME: hack self.__rewind = self.fp.tell() @@ -168,6 +171,8 @@ class GifImageFile(ImageFile.ImageFile): raise EOFError(msg) from e def _seek(self, frame: int, update_image: bool = True) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex if frame == 0: # rewind self.__offset = 0 @@ -251,7 +256,7 @@ class GifImageFile(ImageFile.ImageFile): info["comment"] += b"\n" + comment else: info["comment"] = comment - s = None + s = b"" continue elif s[0] == 255 and frame == 0 and block is not None: # @@ -294,7 +299,7 @@ class GifImageFile(ImageFile.ImageFile): bits = self.fp.read(1)[0] self.__offset = self.fp.tell() break - s = None + s = b"" if interlace is None: msg = "image not found in GIF frame" @@ -347,7 +352,10 @@ class GifImageFile(ImageFile.ImageFile): if self._frame_palette: if color * 3 + 3 > len(self._frame_palette.palette): color = 0 - return tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]) + return cast( + tuple[int, int, int], + tuple(self._frame_palette.palette[color * 3 : color * 3 + 3]), + ) else: return (color, color, color) diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index e9aa084b2..63ec390ae 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -40,6 +40,7 @@ class GribStubImageFile(ImageFile.StubImageFile): format_description = "GRIB" def _open(self) -> None: + assert self.fp is not None offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index cc9e73deb..b3e73064f 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -40,6 +40,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile): format_description = "HDF5" def _open(self) -> None: + assert self.fp is not None offset = self.fp.tell() if not _accept(self.fp.read(8)): diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index ca66aa0fd..faf02779a 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -266,6 +266,7 @@ class IcnsImageFile(ImageFile.ImageFile): format_description = "Mac OS icns resource" def _open(self) -> None: + assert self.fp is not None self.icns = IcnsFile(self.fp) self._mode = "RGBA" self.info["sizes"] = self.icns.itersizes() diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py index e879f1801..2eb12f86a 100644 --- a/src/PIL/IcoImagePlugin.py +++ b/src/PIL/IcoImagePlugin.py @@ -326,6 +326,7 @@ class IcoImageFile(ImageFile.ImageFile): format_description = "Windows Icon" def _open(self) -> None: + assert self.fp is not None self.ico = IcoFile(self.fp) self.info["sizes"] = self.ico.sizes() self.size = self.ico.entry[0].dim diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py index f9f47348c..a12a45866 100644 --- a/src/PIL/ImImagePlugin.py +++ b/src/PIL/ImImagePlugin.py @@ -31,6 +31,7 @@ import re from typing import IO, Any from . import Image, ImageFile, ImagePalette +from ._util import DeferredError # -------------------------------------------------------------------- # Standard tags @@ -124,6 +125,7 @@ class ImImageFile(ImageFile.ImageFile): # Quick rejection: if there's not an LF among the first # 100 bytes, this is (probably) not a text header. + assert self.fp is not None if b"\n" not in self.fp.read(100): msg = "not an IM file" raise SyntaxError(msg) @@ -301,6 +303,8 @@ class ImImageFile(ImageFile.ImageFile): size = ((self.size[0] * bits + 7) // 8) * self.size[1] offs = self.__offset + frame * size + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp self.tile = [ diff --git a/src/PIL/Image.py b/src/PIL/Image.py index e6d9047f5..a1216cf17 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -604,22 +604,11 @@ class Image: return new # Context manager support - def __enter__(self): + def __enter__(self) -> Image: return self - def _close_fp(self): - if getattr(self, "_fp", False): - if self._fp != self.fp: - self._fp.close() - self._fp = DeferredError(ValueError("Operation on closed image")) - if self.fp: - self.fp.close() - - def __exit__(self, *args): - if hasattr(self, "fp"): - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None + def __exit__(self, *args: object) -> None: + pass def close(self) -> None: """ @@ -633,13 +622,6 @@ class Image: :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for more information. """ - if hasattr(self, "fp"): - try: - self._close_fp() - self.fp = None - except Exception as msg: - logger.debug("Error closing: %s", msg) - if getattr(self, "map", None): self.map: mmap.mmap | None = None @@ -1543,10 +1525,14 @@ class Image: exif_info = bytes.fromhex( "".join(self.info["Raw profile type exif"].split("\n")[3:]) ) - elif hasattr(self, "tag_v2"): - self._exif.bigtiff = self.tag_v2._bigtiff - self._exif.endian = self.tag_v2._endian - self._exif.load_from_fp(self.fp, self.tag_v2._offset) + else: + from . import TiffImagePlugin + + if isinstance(self, TiffImagePlugin.TiffImageFile): + self._exif.bigtiff = self.tag_v2._bigtiff + self._exif.endian = self.tag_v2._endian + assert self.fp is not None + self._exif.load_from_fp(self.fp, self.tag_v2._offset) if exif_info is not None: self._exif.load(exif_info) @@ -1566,52 +1552,6 @@ class Image: self._exif._loaded = False self.getexif() - def get_child_images(self) -> list[ImageFile.ImageFile]: - child_images = [] - exif = self.getexif() - ifds = [] - if ExifTags.Base.SubIFDs in exif: - subifd_offsets = exif[ExifTags.Base.SubIFDs] - if subifd_offsets: - if not isinstance(subifd_offsets, tuple): - subifd_offsets = (subifd_offsets,) - for subifd_offset in subifd_offsets: - ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) - ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) - if ifd1 and ifd1.get(513): - assert exif._info is not None - ifds.append((ifd1, exif._info.next)) - - offset = None - for ifd, ifd_offset in ifds: - current_offset = self.fp.tell() - if offset is None: - offset = current_offset - - fp = self.fp - if ifd is not None: - thumbnail_offset = ifd.get(513) - if thumbnail_offset is not None: - thumbnail_offset += getattr(self, "_exif_offset", 0) - self.fp.seek(thumbnail_offset) - data = self.fp.read(ifd.get(514)) - fp = io.BytesIO(data) - - with open(fp) as im: - from . import TiffImagePlugin - - if thumbnail_offset is None and isinstance( - im, TiffImagePlugin.TiffImageFile - ): - im._frame_pos = [ifd_offset] - im._seek(0) - im.load() - child_images.append(im) - - if offset is not None: - self.fp.seek(offset) - return child_images - def getim(self) -> CapsuleType: """ Returns a capsule that points to the internal image memory. @@ -2522,7 +2462,10 @@ class Image: ) def save( - self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any + self, + fp: StrOrBytesPath | IO[bytes] | io.TextIOWrapper, + format: str | None = None, + **params: Any, ) -> None: """ Saves this image under the given filename. If no format is diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index b8bf0b7fe..6fdc9c7cb 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -31,18 +31,21 @@ from __future__ import annotations import abc import io import itertools +import logging import os import struct import sys from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast -from . import Image +from . import ExifTags, Image from ._deprecate import deprecate -from ._util import is_path +from ._util import DeferredError, is_path if TYPE_CHECKING: from ._typing import StrOrBytesPath +logger = logging.getLogger(__name__) + MAXBLOCK = 65536 SAFEBLOCK = 1024 * 1024 @@ -127,6 +130,8 @@ class ImageFile(Image.Image): self.decoderconfig: tuple[Any, ...] = () self.decodermaxblock = MAXBLOCK + self.fp: IO[bytes] | None + self._fp: IO[bytes] | DeferredError if is_path(fp): # filename self.fp = open(fp, "rb") @@ -163,6 +168,93 @@ class ImageFile(Image.Image): def _open(self) -> None: pass + # Context manager support + def __enter__(self) -> ImageFile: + return self + + def _close_fp(self) -> None: + if getattr(self, "_fp", False): + if self._fp != self.fp and not isinstance(self._fp, DeferredError): + self._fp.close() + self._fp = DeferredError(ValueError("Operation on closed image")) + if self.fp: + self.fp.close() + + def __exit__(self, *args: object) -> None: + if getattr(self, "_exclusive_fp", False): + self._close_fp() + self.fp = None + + def close(self) -> None: + """ + Closes the file pointer, if possible. + + This operation will destroy the image core and release its memory. + The image data will be unusable afterward. + + This function is required to close images that have multiple frames or + have not had their file read and closed by the + :py:meth:`~PIL.Image.Image.load` method. See :ref:`file-handling` for + more information. + """ + try: + self._close_fp() + self.fp = None + except Exception as msg: + logger.debug("Error closing: %s", msg) + + super().close() + + def get_child_images(self) -> list[ImageFile]: + child_images = [] + exif = self.getexif() + ifds = [] + if ExifTags.Base.SubIFDs in exif: + subifd_offsets = exif[ExifTags.Base.SubIFDs] + if subifd_offsets: + if not isinstance(subifd_offsets, tuple): + subifd_offsets = (subifd_offsets,) + for subifd_offset in subifd_offsets: + ifds.append((exif._get_ifd_dict(subifd_offset), subifd_offset)) + ifd1 = exif.get_ifd(ExifTags.IFD.IFD1) + if ifd1 and ifd1.get(513): + assert exif._info is not None + ifds.append((ifd1, exif._info.next)) + + offset = None + assert self.fp is not None + for ifd, ifd_offset in ifds: + current_offset = self.fp.tell() + if offset is None: + offset = current_offset + + fp = self.fp + if ifd is not None: + thumbnail_offset = ifd.get(513) + if thumbnail_offset is not None: + thumbnail_offset += getattr(self, "_exif_offset", 0) + self.fp.seek(thumbnail_offset) + + length = ifd.get(514) + assert isinstance(length, int) + data = self.fp.read(length) + fp = io.BytesIO(data) + + with Image.open(fp) as im: + from . import TiffImagePlugin + + if thumbnail_offset is None and isinstance( + im, TiffImagePlugin.TiffImageFile + ): + im._frame_pos = [ifd_offset] + im._seek(0) + im.load() + child_images.append(im) + + if offset is not None: + self.fp.seek(offset) + return child_images + def get_format_mimetype(self) -> str | None: if self.custom_mimetype: return self.custom_mimetype @@ -179,7 +271,7 @@ class ImageFile(Image.Image): # raise exception if something's wrong. must be called # directly after open, and closes file when finished. - if self._exclusive_fp: + if self._exclusive_fp and self.fp: self.fp.close() self.fp = None @@ -199,6 +291,7 @@ class ImageFile(Image.Image): # As of pypy 2.1.0, memory mapping was failing here. use_mmap = use_mmap and not hasattr(sys, "pypy_version_info") + assert self.fp is not None readonly = 0 # look for read/seek overrides diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py index 60ab7c83f..7ca5e6680 100644 --- a/src/PIL/IptcImagePlugin.py +++ b/src/PIL/IptcImagePlugin.py @@ -77,6 +77,7 @@ class IptcImageFile(ImageFile.ImageFile): def field(self) -> tuple[tuple[int, int] | None, int]: # # get a IPTC field header + assert self.fp is not None s = self.fp.read(5) if not s.strip(b"\x00"): return None, 0 @@ -104,6 +105,7 @@ class IptcImageFile(ImageFile.ImageFile): def _open(self) -> None: # load descriptive fields + assert self.fp is not None while True: offset = self.fp.tell() tag, size = self.field() @@ -157,6 +159,7 @@ class IptcImageFile(ImageFile.ImageFile): offset, compression = self.tile[0][2:] + assert self.fp is not None self.fp.seek(offset) # Copy image data to temporary file @@ -165,6 +168,7 @@ class IptcImageFile(ImageFile.ImageFile): # To simplify access to the extracted file, # prepend a PPM header o.write(b"P5\n%d %d\n255\n" % self.size) + assert self.fp is not None while True: type, size = self.field() if type != (8, 10): @@ -188,7 +192,7 @@ Image.register_extension(IptcImageFile.format, ".iim") def getiptcinfo( - im: ImageFile.ImageFile, + im: Image.Image, ) -> dict[tuple[int, int], bytes | list[bytes]] | None: """ Get IPTC information from TIFF, JPEG, or IPTC file. diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index b6ebd562b..f4bb9c445 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -248,6 +248,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): format_description = "JPEG 2000 (ISO 15444)" def _open(self) -> None: + assert self.fp is not None sig = self.fp.read(4) if sig == b"\xff\x4f\xff\x51": self.codec = "j2k" @@ -296,6 +297,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile): ] def _parse_comment(self) -> None: + assert self.fp is not None hdr = self.fp.read(2) length = _binary.i16be(hdr) self.fp.seek(length - 2, os.SEEK_CUR) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 6510e072e..bfc922e21 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -60,6 +60,7 @@ if TYPE_CHECKING: def Skip(self: JpegImageFile, marker: int) -> None: + assert self.fp is not None n = i16(self.fp.read(2)) - 2 ImageFile._safe_read(self.fp, n) @@ -69,6 +70,7 @@ def APP(self: JpegImageFile, marker: int) -> None: # Application marker. Store these in the APP dictionary. # Also look for well-known application markers. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) @@ -170,6 +172,7 @@ def APP(self: JpegImageFile, marker: int) -> None: def COM(self: JpegImageFile, marker: int) -> None: # # Comment marker. Store these in the APP dictionary. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) @@ -186,6 +189,7 @@ def SOF(self: JpegImageFile, marker: int) -> None: # mode. Note that this could be made a bit brighter, by # looking for JFIF and Adobe APP markers. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) self._size = i16(s, 3), i16(s, 1) @@ -234,6 +238,7 @@ def DQT(self: JpegImageFile, marker: int) -> None: # FIXME: The quantization tables can be used to estimate the # compression quality. + assert self.fp is not None n = i16(self.fp.read(2)) - 2 s = ImageFile._safe_read(self.fp, n) while len(s): @@ -334,6 +339,7 @@ class JpegImageFile(ImageFile.ImageFile): format_description = "JPEG (ISO 10918)" def _open(self) -> None: + assert self.fp is not None s = self.fp.read(3) if not _accept(s): @@ -401,6 +407,7 @@ class JpegImageFile(ImageFile.ImageFile): For premature EOF and LOAD_TRUNCATED_IMAGES adds EOI marker so libjpeg can finish decoding """ + assert self.fp is not None s = self.fp.read(read_bytes) if not s and ImageFile.LOAD_TRUNCATED_IMAGES and not hasattr(self, "_ended"): diff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py index 5f23a34b9..ba5daaafc 100644 --- a/src/PIL/MicImagePlugin.py +++ b/src/PIL/MicImagePlugin.py @@ -67,6 +67,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile): self._n_frames = len(self.images) self.is_animated = self._n_frames > 1 + assert self.fp is not None self.__fp = self.fp self.seek(0) diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py index 71f89a09a..02db52af4 100644 --- a/src/PIL/MpoImagePlugin.py +++ b/src/PIL/MpoImagePlugin.py @@ -32,6 +32,7 @@ from . import ( TiffImagePlugin, ) from ._binary import o32le +from ._util import DeferredError def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: @@ -98,6 +99,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): _close_exclusive_fp_after_loading = False def _open(self) -> None: + assert self.fp is not None self.fp.seek(0) # prep the fp in order to pass the JPEG test JpegImagePlugin.JpegImageFile._open(self) self._after_jpeg_open() @@ -117,6 +119,7 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): assert self.n_frames == len(self.__mpoffsets) del self.info["mpoffset"] # no longer needed self.is_animated = self.n_frames > 1 + assert self.fp is not None self._fp = self.fp # FIXME: hack self._fp.seek(self.__mpoffsets[0]) # get ready to read first frame self.__frame = 0 @@ -125,11 +128,15 @@ class MpoImageFile(JpegImagePlugin.JpegImageFile): self.readonly = 1 def load_seek(self, pos: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(pos) def seek(self, frame: int) -> None: if not self._seek_check(frame): return + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp self.offset = self.__mpoffsets[frame] diff --git a/src/PIL/PngImagePlugin.py b/src/PIL/PngImagePlugin.py index c268d7b1a..0887f36fb 100644 --- a/src/PIL/PngImagePlugin.py +++ b/src/PIL/PngImagePlugin.py @@ -48,6 +48,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._binary import o16be as o16 from ._binary import o32be as o32 +from ._util import DeferredError if TYPE_CHECKING: from . import _imaging @@ -752,6 +753,7 @@ class PngImageFile(ImageFile.ImageFile): format_description = "Portable network graphics" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(8)): msg = "not a PNG file" raise SyntaxError(msg) @@ -869,6 +871,8 @@ class PngImageFile(ImageFile.ImageFile): def _seek(self, frame: int, rewind: bool = False) -> None: assert self.png is not None + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.dispose: _imaging.ImagingCore | None dispose_extent = None @@ -981,6 +985,7 @@ class PngImageFile(ImageFile.ImageFile): """internal: read more image data""" assert self.png is not None + assert self.fp is not None while self.__idat == 0: # end of chunk, skip forward to next one @@ -1014,6 +1019,7 @@ class PngImageFile(ImageFile.ImageFile): def load_end(self) -> None: """internal: finished reading image data""" assert self.png is not None + assert self.fp is not None if self.__idat != 0: self.fp.read(self.__idat) while True: diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py index 8ff5e3908..eb1c74758 100644 --- a/src/PIL/PsdImagePlugin.py +++ b/src/PIL/PsdImagePlugin.py @@ -27,6 +27,7 @@ from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import si16be as si16 from ._binary import si32be as si32 +from ._util import DeferredError MODES = { # (photoshop mode, bits) -> (pil mode, required channels) @@ -60,6 +61,7 @@ class PsdImageFile(ImageFile.ImageFile): _close_exclusive_fp_after_loading = False def _open(self) -> None: + assert self.fp is not None read = self.fp.read # @@ -148,6 +150,8 @@ class PsdImageFile(ImageFile.ImageFile): ) -> list[tuple[str, str, tuple[int, int, int, int], list[ImageFile._Tile]]]: layers = [] if self._layers_position is not None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self._fp.seek(self._layers_position) _layer_data = io.BytesIO(ImageFile._safe_read(self._fp, self._layers_size)) layers = _layerinfo(_layer_data, self._layers_size) @@ -174,6 +178,8 @@ class PsdImageFile(ImageFile.ImageFile): self._mode = mode self.tile = tile self.frame = layer + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp except IndexError as e: msg = "no such layer" diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index 010d3f941..37590f3d3 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -22,6 +22,7 @@ class QoiImageFile(ImageFile.ImageFile): format_description = "Quite OK Image" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(4)): msg = "not a QOI file" raise SyntaxError(msg) diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py index 075073f9f..88dd045d0 100644 --- a/src/PIL/SpiderImagePlugin.py +++ b/src/PIL/SpiderImagePlugin.py @@ -40,6 +40,7 @@ import sys from typing import IO, TYPE_CHECKING, Any, cast from . import Image, ImageFile +from ._util import DeferredError def isInt(f: Any) -> int: @@ -100,6 +101,7 @@ class SpiderImageFile(ImageFile.ImageFile): def _open(self) -> None: # check header + assert self.fp is not None n = 27 * 4 # read 27 float values f = self.fp.read(n) @@ -181,6 +183,9 @@ class SpiderImageFile(ImageFile.ImageFile): if not self._seek_check(frame): return self.stkoffset = self.hdrlen + frame * (self.hdrlen + self.imgbytes) + + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp self.fp.seek(self.stkoffset) self._open() @@ -211,26 +216,27 @@ class SpiderImageFile(ImageFile.ImageFile): # given a list of filenames, return a list of images -def loadImageSeries(filelist: list[str] | None = None) -> list[SpiderImageFile] | None: +def loadImageSeries(filelist: list[str] | None = None) -> list[Image.Image] | None: """create a list of :py:class:`~PIL.Image.Image` objects for use in a montage""" if filelist is None or len(filelist) < 1: return None - imglist = [] + byte_imgs = [] for img in filelist: if not os.path.exists(img): print(f"unable to find {img}") continue try: with Image.open(img) as im: - im = im.convert2byte() + assert isinstance(im, SpiderImageFile) + byte_im = im.convert2byte() except Exception: if not isSpiderImage(img): print(f"{img} is not a Spider image file") continue - im.info["filename"] = img - imglist.append(im) - return imglist + byte_im.info["filename"] = img + byte_imgs.append(byte_im) + return byte_imgs # -------------------------------------------------------------------- @@ -321,9 +327,9 @@ if __name__ == "__main__": outfile = sys.argv[2] # perform some image operation - im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) + transposed_im = im.transpose(Image.Transpose.FLIP_LEFT_RIGHT) print( f"saving a flipped version of {os.path.basename(filename)} " f"as {outfile} " ) - im.save(outfile, SpiderImageFile.format) + transposed_im.save(outfile, SpiderImageFile.format) diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index cc16cbfb0..c9cc65f41 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -58,7 +58,7 @@ from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate from ._typing import StrOrBytesPath -from ._util import is_path +from ._util import DeferredError, is_path from .TiffTags import TYPES if TYPE_CHECKING: @@ -1154,6 +1154,7 @@ class TiffImageFile(ImageFile.ImageFile): """Open the first image in a TIFF file""" # Header + assert self.fp is not None ifh = self.fp.read(8) if ifh[2] == 43: ifh += self.fp.read(8) @@ -1198,6 +1199,8 @@ class TiffImageFile(ImageFile.ImageFile): self.im = Image.core.new(self.mode, self.size) def _seek(self, frame: int) -> None: + if isinstance(self._fp, DeferredError): + raise self._fp.ex self.fp = self._fp # reset buffered io handle in case fp @@ -1283,6 +1286,7 @@ class TiffImageFile(ImageFile.ImageFile): # reset buffered io handle in case fp # was passed to libtiff, invalidating the buffer + assert self.fp is not None self.fp.tell() # load IFD data from fp before it is closed @@ -1316,6 +1320,7 @@ class TiffImageFile(ImageFile.ImageFile): # To be nice on memory footprint, if there's a # file descriptor, use that instead of reading # into a string in python. + assert self.fp is not None try: fp = hasattr(self.fp, "fileno") and self.fp.fileno() # flush the file descriptor, prevents error on pypy 2.4+ diff --git a/src/PIL/WalImageFile.py b/src/PIL/WalImageFile.py index 87e32878b..7e967ff14 100644 --- a/src/PIL/WalImageFile.py +++ b/src/PIL/WalImageFile.py @@ -39,6 +39,7 @@ class WalImageFile(ImageFile.ImageFile): self._mode = "P" # read header fields + assert self.fp is not None header = self.fp.read(32 + 24 + 32 + 12) self._size = i32(header, 32), i32(header, 36) Image._decompression_bomb_check(self.size) @@ -55,6 +56,7 @@ class WalImageFile(ImageFile.ImageFile): def load(self) -> Image.core.PixelAccess | None: if self._im is None: + assert self.fp is not None self.im = Image.core.new(self.mode, self.size) self.frombytes(self.fp.read(self.size[0] * self.size[1])) self.putpalette(quake2palette) diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py index f8d6168ba..343b3892d 100644 --- a/src/PIL/WebPImagePlugin.py +++ b/src/PIL/WebPImagePlugin.py @@ -47,6 +47,7 @@ class WebPImageFile(ImageFile.ImageFile): def _open(self) -> None: # Use the newer AnimDecoder API to parse the (possibly) animated file, # and access muxed chunks like ICC/EXIF/XMP. + assert self.fp is not None self._decoder = _webp.WebPAnimDecoder(self.fp.read()) # Get info from decoder diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 68f8a74f5..f81cca936 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -49,6 +49,7 @@ if hasattr(Image.core, "drawwmf"): self.bbox = im.info["wmf_bbox"] def load(self, im: ImageFile.StubImageFile) -> Image.Image: + assert im.fp is not None im.fp.seek(0) # rewind return Image.frombytes( "RGB", @@ -85,6 +86,7 @@ class WmfStubImageFile(ImageFile.StubImageFile): self._inch = None # check placable header + assert self.fp is not None s = self.fp.read(80) if s[:6] == b"\xd7\xcd\xc6\x9a\x00\x00": diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py index 5d1f201a4..787bd94a2 100644 --- a/src/PIL/XVThumbImagePlugin.py +++ b/src/PIL/XVThumbImagePlugin.py @@ -58,15 +58,15 @@ class XVThumbImageFile(ImageFile.ImageFile): # skip info comments while True: - s = self.fp.readline() - if not s: + line = self.fp.readline() + if not line: msg = "Unexpected EOF reading XV thumbnail file" raise SyntaxError(msg) - if s[0] != 35: # ie. when not a comment: '#' + if line[0] != 35: # ie. when not a comment: '#' break # parse header line (already read) - s = s.strip().split() + s = line.strip().split() self._mode = "P" self._size = int(s[0]), int(s[1]) diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 1fc6c0c39..019813291 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -37,17 +37,18 @@ class XpmImageFile(ImageFile.ImageFile): format_description = "X11 Pixel Map" def _open(self) -> None: + assert self.fp is not None if not _accept(self.fp.read(9)): msg = "not an XPM file" raise SyntaxError(msg) # skip forward to next string while True: - s = self.fp.readline() - if not s: + line = self.fp.readline() + if not line: msg = "broken XPM file" raise SyntaxError(msg) - m = xpm_head.match(s) + m = xpm_head.match(line) if m: break @@ -66,14 +67,14 @@ class XpmImageFile(ImageFile.ImageFile): palette = [b"\0\0\0"] * 256 for _ in range(pal): - s = self.fp.readline() - if s[-2:] == b"\r\n": - s = s[:-2] - elif s[-1:] in b"\r\n": - s = s[:-1] + line = self.fp.readline() + if line[-2:] == b"\r\n": + line = line[:-2] + elif line[-1:] in b"\r\n": + line = line[:-1] - c = s[1] - s = s[2:-2].split() + c = line[1] + s = line[2:-2].split() for i in range(0, len(s), 2): if s[i] == b"c": @@ -83,9 +84,11 @@ class XpmImageFile(ImageFile.ImageFile): self.info["transparency"] = c elif rgb[:1] == b"#": # FIXME: handle colour names (see ImagePalette.py) - rgb = int(rgb[1:], 16) + rgb_int = int(rgb[1:], 16) palette[c] = ( - o8((rgb >> 16) & 255) + o8((rgb >> 8) & 255) + o8(rgb & 255) + o8((rgb_int >> 16) & 255) + + o8((rgb_int >> 8) & 255) + + o8(rgb_int & 255) ) else: # unknown colour @@ -111,6 +114,7 @@ class XpmImageFile(ImageFile.ImageFile): xsize, ysize = self.size + assert self.fp is not None s = [self.fp.readline()[1 : xsize + 1].ljust(xsize) for i in range(ysize)] return b"".join(s)