From 1f4beb4a5c5724019ea9b0683432cbc3357d10cc Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:53:47 +0200 Subject: [PATCH 1/4] Lint with flake8-pie --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2ffd9faca..780a938a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,6 +121,7 @@ lint.select = [ "ISC", # flake8-implicit-str-concat "LOG", # flake8-logging "PGH", # pygrep-hooks + "PIE", # flake8-pie "PT", # flake8-pytest-style "PYI", # flake8-pyi "RUF100", # unused noqa (yesqa) @@ -133,6 +134,7 @@ lint.ignore = [ "E221", # Multiple spaces before operator "E226", # Missing whitespace around arithmetic operator "E241", # Multiple spaces after ',' + "PIE790", # flake8-pie: unnecessary-placeholder "PT001", # pytest-fixture-incorrect-parentheses-style "PT007", # pytest-parametrize-values-wrong-type "PT011", # pytest-raises-too-broad From e4cac21044d6b7bfe958e9f9d0a4c8d150b444e7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:54:22 +0200 Subject: [PATCH 2/4] Don't use start=0 in range() --- Tests/test_file_gif.py | 2 +- Tests/test_file_webp.py | 2 +- Tests/test_imagedraw.py | 4 ++-- Tests/test_imagepalette.py | 2 +- Tests/test_imagesequence.py | 2 +- Tests/test_pickle.py | 8 ++++---- src/PIL/Image.py | 6 +++--- src/PIL/ImageDraw.py | 4 ++-- src/PIL/ImageOps.py | 10 +++++----- src/PIL/JpegImagePlugin.py | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py index 2254178d5..d2592da97 100644 --- a/Tests/test_file_gif.py +++ b/Tests/test_file_gif.py @@ -601,7 +601,7 @@ def test_save_dispose(tmp_path: Path) -> None: Image.new("L", (100, 100), "#111"), Image.new("L", (100, 100), "#222"), ] - for method in range(0, 4): + for method in range(4): im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=method) with Image.open(out) as img: for _ in range(2): diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index abe888241..6f6074ef2 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -231,7 +231,7 @@ class TestFileWebp: with Image.open(out_gif) as reread: reread_value = reread.convert("RGB").getpixel((1, 1)) - difference = sum(abs(original_value[i] - reread_value[i]) for i in range(0, 3)) + difference = sum(abs(original_value[i] - reread_value[i]) for i in range(3)) assert difference < 5 def test_duration(self, tmp_path: Path) -> None: diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 232cbb16c..1b4d09f39 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1044,8 +1044,8 @@ def create_base_image_draw( background2: tuple[int, int, int] = GRAY, ) -> tuple[Image.Image, ImageDraw.ImageDraw]: img = Image.new(mode, size, background1) - for x in range(0, size[0]): - for y in range(0, size[1]): + for x in range(size[0]): + for y in range(size[1]): if (x + y) % 2 == 0: img.putpixel((x, y), background2) return img, ImageDraw.Draw(img) diff --git a/Tests/test_imagepalette.py b/Tests/test_imagepalette.py index e0b6359b0..782022f51 100644 --- a/Tests/test_imagepalette.py +++ b/Tests/test_imagepalette.py @@ -112,7 +112,7 @@ def test_make_linear_lut() -> None: assert isinstance(lut, list) assert len(lut) == 256 # Check values - for i in range(0, len(lut)): + for i in range(len(lut)): assert lut[i] == i diff --git a/Tests/test_imagesequence.py b/Tests/test_imagesequence.py index 9b37435eb..26b287bb4 100644 --- a/Tests/test_imagesequence.py +++ b/Tests/test_imagesequence.py @@ -32,7 +32,7 @@ def test_sanity(tmp_path: Path) -> None: def test_iterator() -> None: with Image.open("Tests/images/multipage.tiff") as im: i = ImageSequence.Iterator(im) - for index in range(0, im.n_frames): + for index in range(im.n_frames): assert i[index] == next(i) with pytest.raises(IndexError): i[index + 1] diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index c4f8de013..05c41a802 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -65,7 +65,7 @@ def helper_pickle_string(protocol: int, test_file: str, mode: str | None) -> Non ("Tests/images/itxt_chunks.png", None), ], ) -@pytest.mark.parametrize("protocol", range(0, pickle.HIGHEST_PROTOCOL + 1)) +@pytest.mark.parametrize("protocol", range(pickle.HIGHEST_PROTOCOL + 1)) def test_pickle_image( tmp_path: Path, test_file: str, test_mode: str | None, protocol: int ) -> None: @@ -92,7 +92,7 @@ def test_pickle_la_mode_with_palette(tmp_path: Path) -> None: im = im.convert("PA") # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): im._mode = "LA" with open(filename, "wb") as f: pickle.dump(im, f, protocol) @@ -133,7 +133,7 @@ def helper_assert_pickled_font_images( @skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_string(protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) @@ -147,7 +147,7 @@ def test_pickle_font_string(protocol: int) -> None: @skip_unless_feature("freetype2") -@pytest.mark.parametrize("protocol", list(range(0, pickle.HIGHEST_PROTOCOL + 1))) +@pytest.mark.parametrize("protocol", list(range(pickle.HIGHEST_PROTOCOL + 1))) def test_pickle_font_file(tmp_path: Path, protocol: int) -> None: # Arrange font = ImageFont.truetype(FONT_PATH, FONT_SIZE) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6a2aa3e4c..684c87c4d 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -1001,7 +1001,7 @@ class Image: elif len(mode) == 3: transparency = tuple( convert_transparency(matrix[i * 4 : i * 4 + 4], transparency) - for i in range(0, len(transparency)) + for i in range(len(transparency)) ) new_im.info["transparency"] = transparency return new_im @@ -4003,7 +4003,7 @@ class Exif(_ExifBase): ifd_data = tag_data[ifd_offset:] makernote = {} - for i in range(0, struct.unpack("H", tag_data[:2])[0]): + for i in range(struct.unpack(">H", tag_data[:2])[0]): ifd_tag, typ, count, data = struct.unpack( ">HHL4s", tag_data[i * 12 + 2 : (i + 1) * 12 + 2] ) diff --git a/src/PIL/ImageDraw.py b/src/PIL/ImageDraw.py index c4ebc5931..c2ed9034d 100644 --- a/src/PIL/ImageDraw.py +++ b/src/PIL/ImageDraw.py @@ -1204,7 +1204,7 @@ def _compute_regular_polygon_vertices( degrees = 360 / n_sides # Start with the bottom left polygon vertex current_angle = (270 - 0.5 * degrees) + rotation - for _ in range(0, n_sides): + for _ in range(n_sides): angles.append(current_angle) current_angle += degrees if current_angle > 360: @@ -1227,4 +1227,4 @@ def _color_diff( first = color1 if isinstance(color1, tuple) else (color1,) second = color2 if isinstance(color2, tuple) else (color2,) - return sum(abs(first[i] - second[i]) for i in range(0, len(second))) + return sum(abs(first[i] - second[i]) for i in range(len(second))) diff --git a/src/PIL/ImageOps.py b/src/PIL/ImageOps.py index 75dfbee22..da28854b5 100644 --- a/src/PIL/ImageOps.py +++ b/src/PIL/ImageOps.py @@ -213,14 +213,14 @@ def colorize( blue = [] # Create the low-end values - for i in range(0, blackpoint): + for i in range(blackpoint): red.append(rgb_black[0]) green.append(rgb_black[1]) blue.append(rgb_black[2]) # Create the mapping (2-color) if rgb_mid is None: - range_map = range(0, whitepoint - blackpoint) + range_map = range(whitepoint - blackpoint) for i in range_map: red.append( @@ -235,8 +235,8 @@ def colorize( # Create the mapping (3-color) else: - range_map1 = range(0, midpoint - blackpoint) - range_map2 = range(0, whitepoint - midpoint) + range_map1 = range(midpoint - blackpoint) + range_map2 = range(whitepoint - midpoint) for i in range_map1: red.append( @@ -256,7 +256,7 @@ def colorize( blue.append(rgb_mid[2] + i * (rgb_white[2] - rgb_mid[2]) // len(range_map2)) # Create the high-end values - for i in range(0, 256 - whitepoint): + for i in range(256 - whitepoint): red.append(rgb_white[0]) green.append(rgb_white[1]) blue.append(rgb_white[2]) diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py index 3e882403b..9465d8e2d 100644 --- a/src/PIL/JpegImagePlugin.py +++ b/src/PIL/JpegImagePlugin.py @@ -569,7 +569,7 @@ def _getmp(self: JpegImageFile) -> dict[int, Any] | None: mpentries = [] try: rawmpentries = mp[0xB002] - for entrynum in range(0, quant): + for entrynum in range(quant): unpackedentry = struct.unpack_from( f"{endianness}LLLHH", rawmpentries, entrynum * 16 ) From a2b13cc02a68bd8f0bc3a9f84e603930c3a2496f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 3 Mar 2025 23:56:43 +0200 Subject: [PATCH 3/4] Call startswith/endswith once with a tuple --- src/PIL/IcnsImagePlugin.py | 3 +-- src/PIL/TiffImagePlugin.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/PIL/IcnsImagePlugin.py b/src/PIL/IcnsImagePlugin.py index a5d5b93ae..5a88429e5 100644 --- a/src/PIL/IcnsImagePlugin.py +++ b/src/PIL/IcnsImagePlugin.py @@ -123,8 +123,7 @@ def read_png_or_jpeg2000( Image._decompression_bomb_check(im.size) return {"RGBA": im} elif ( - sig.startswith(b"\xff\x4f\xff\x51") - or sig.startswith(b"\x0d\x0a\x87\x0a") + sig.startswith((b"\xff\x4f\xff\x51", b"\x0d\x0a\x87\x0a")) or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" ): if not enable_jpeg2k: diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 3d36d1abc..b8ff47a12 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -1584,7 +1584,7 @@ class TiffImageFile(ImageFile.ImageFile): # byte order. elif rawmode == "I;16": rawmode = "I;16N" - elif rawmode.endswith(";16B") or rawmode.endswith(";16L"): + elif rawmode.endswith((";16B", ";16L")): rawmode = rawmode[:-1] + "N" # Offset in the tile tuple is 0, we go from 0,0 to From 3607d1ade397fc5a5b41f2a0607a15927e2810fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 5 Mar 2025 00:03:37 +1100 Subject: [PATCH 4/4] Use match argument --- Tests/test_file_libtiff.py | 6 ++---- Tests/test_file_ppm.py | 14 +++++--------- Tests/test_file_tiff.py | 3 +-- Tests/test_file_webp.py | 3 +-- Tests/test_image.py | 3 +-- Tests/test_imagedraw.py | 7 +++---- Tests/test_imagefile.py | 3 +-- Tests/test_imagemorph.py | 26 ++++++++++---------------- Tests/test_imagepath.py | 12 ++---------- 9 files changed, 26 insertions(+), 51 deletions(-) diff --git a/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py index 369c2db1b..f284c3f2f 100644 --- a/Tests/test_file_libtiff.py +++ b/Tests/test_file_libtiff.py @@ -1140,11 +1140,9 @@ class TestFileLibTiff(LibTiffTestCase): def test_realloc_overflow(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(TiffImagePlugin, "READ_LIBTIFF", True) with Image.open("Tests/images/tiff_overflow_rows_per_strip.tif") as im: - with pytest.raises(OSError) as e: - im.load() - # Assert that the error code is IMAGING_CODEC_MEMORY - assert str(e.value) == "decoder error -9" + with pytest.raises(OSError, match="decoder error -9"): + im.load() @pytest.mark.parametrize("compression", ("tiff_adobe_deflate", "jpeg")) def test_save_multistrip(self, compression: str, tmp_path: Path) -> None: diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index d87192ca5..c93a8c73a 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -293,12 +293,10 @@ def test_header_token_too_long(tmp_path: Path) -> None: with open(path, "wb") as f: f.write(b"P6\n 01234567890") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Token too long in file header: 01234567890"): with Image.open(path): pass - assert str(e.value) == "Token too long in file header: 01234567890" - def test_truncated_file(tmp_path: Path) -> None: # Test EOF in header @@ -306,12 +304,10 @@ def test_truncated_file(tmp_path: Path) -> None: with open(path, "wb") as f: f.write(b"P6") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Reached EOF while reading header"): with Image.open(path): pass - assert str(e.value) == "Reached EOF while reading header" - # Test EOF for PyDecoder fp = BytesIO(b"P5 3 1 4") with Image.open(fp) as im: @@ -335,12 +331,12 @@ def test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None: with open(path, "wb") as f: f.write(b"P6\n3 1 " + maxval) - with pytest.raises(ValueError) as e: + with pytest.raises( + ValueError, match="maxval must be greater than 0 and less than 65536" + ): with Image.open(path): pass - assert str(e.value) == "maxval must be greater than 0 and less than 65536" - def test_neg_ppm() -> None: # Storage.c accepted negative values for xsize, ysize. the diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py index a8a407963..c1ccf3fe2 100644 --- a/Tests/test_file_tiff.py +++ b/Tests/test_file_tiff.py @@ -134,9 +134,8 @@ class TestFileTiff: def test_set_legacy_api(self) -> None: ifd = TiffImagePlugin.ImageFileDirectory_v2() - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="Not allowing setting of legacy api"): ifd.legacy_api = False - assert str(e.value) == "Not allowing setting of legacy api" def test_xyres_tiff(self) -> None: filename = "Tests/images/pil168.tif" diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index abe888241..d8c4eb589 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -154,9 +154,8 @@ class TestFileWebp: @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_message(self, tmp_path: Path) -> None: im = Image.new("RGB", (15000, 15000)) - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="encoding error 6"): im.save(tmp_path / "temp.webp", method=0) - assert str(e.value) == "encoding error 6" @pytest.mark.skipif(sys.maxsize <= 2**32, reason="Requires 64-bit system") def test_write_encoding_error_bad_dimension(self, tmp_path: Path) -> None: diff --git a/Tests/test_image.py b/Tests/test_image.py index 5474f951c..d64816b1e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -65,9 +65,8 @@ class TestImage: @pytest.mark.parametrize("mode", ("", "bad", "very very long")) def test_image_modes_fail(self, mode: str) -> None: - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="unrecognized image mode"): Image.new(mode, (1, 1)) - assert str(e.value) == "unrecognized image mode" def test_exception_inheritance(self) -> None: assert issubclass(UnidentifiedImageError, OSError) diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py index 232cbb16c..1af4455b8 100644 --- a/Tests/test_imagedraw.py +++ b/Tests/test_imagedraw.py @@ -1626,7 +1626,7 @@ def test_compute_regular_polygon_vertices( 0, ValueError, "bounding_circle should contain 2D coordinates " - "and a radius (e.g. (x, y, r) or ((x, y), r) )", + r"and a radius \(e.g. \(x, y, r\) or \(\(x, y\), r\) \)", ), ( 3, @@ -1640,7 +1640,7 @@ def test_compute_regular_polygon_vertices( ((50, 50, 50), 25), 0, ValueError, - "bounding_circle centre should contain 2D coordinates (e.g. (x, y))", + r"bounding_circle centre should contain 2D coordinates \(e.g. \(x, y\)\)", ), ( 3, @@ -1665,9 +1665,8 @@ def test_compute_regular_polygon_vertices_input_error_handling( expected_error: type[Exception], error_message: str, ) -> None: - with pytest.raises(expected_error) as e: + with pytest.raises(expected_error, match=error_message): ImageDraw._compute_regular_polygon_vertices(bounding_circle, n_sides, rotation) # type: ignore[arg-type] - assert str(e.value) == error_message def test_continuous_horizontal_edges_polygon() -> None: diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index b05d29dae..c60a475a3 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -176,9 +176,8 @@ class TestImageFile: b"0" * ImageFile.SAFEBLOCK ) # only SAFEBLOCK bytes, so that the header is truncated ) - with pytest.raises(OSError) as e: + with pytest.raises(OSError, match="Truncated File Read"): BmpImagePlugin.BmpImageFile(b) - assert str(e.value) == "Truncated File Read" @skip_unless_feature("zlib") def test_truncated_with_errors(self) -> None: diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 6180a7b5d..515e29cea 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -80,15 +80,12 @@ def test_lut(op: str) -> None: def test_no_operator_loaded() -> None: im = Image.new("L", (1, 1)) mop = ImageMorph.MorphOp() - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.apply(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.match(im) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="No operator loaded"): mop.save_lut("") - assert str(e.value) == "No operator loaded" # Test the named patterns @@ -238,15 +235,12 @@ def test_incorrect_mode() -> None: im = hopper("RGB") mop = ImageMorph.MorphOp(op_name="erosion8") - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.apply(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.match(im) - assert str(e.value) == "Image mode must be L" - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="Image mode must be L"): mop.get_on_pixels(im) - assert str(e.value) == "Image mode must be L" def test_add_patterns() -> None: @@ -279,9 +273,10 @@ def test_pattern_syntax_error() -> None: lb.add_patterns(new_patterns) # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises( + Exception, match='Syntax error in pattern "a pattern with a syntax error"' + ): lb.build_lut() - assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' def test_load_invalid_mrl() -> None: @@ -290,9 +285,8 @@ def test_load_invalid_mrl() -> None: mop = ImageMorph.MorphOp() # Act / Assert - with pytest.raises(Exception) as e: + with pytest.raises(Exception, match="Wrong size operator file!"): mop.load_lut(invalid_mrl) - assert str(e.value) == "Wrong size operator file!" def test_roundtrip_mrl(tmp_path: Path) -> None: diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 1b1ee6bac..1ebf12d22 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -81,13 +81,9 @@ def test_path_constructors( def test_invalid_path_constructors( coords: tuple[str, str] | Sequence[Sequence[int]], ) -> None: - # Act - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="incorrect coordinate type"): ImagePath.Path(coords) - # Assert - assert str(e.value) == "incorrect coordinate type" - @pytest.mark.parametrize( "coords", @@ -99,13 +95,9 @@ def test_invalid_path_constructors( ), ) def test_path_odd_number_of_coordinates(coords: Sequence[int]) -> None: - # Act - with pytest.raises(ValueError) as e: + with pytest.raises(ValueError, match="wrong number of coordinates"): ImagePath.Path(coords) - # Assert - assert str(e.value) == "wrong number of coordinates" - @pytest.mark.parametrize( "coords, expected",