From b9afe18646c5dc28efed1a74579129f8c64976e2 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 15:00:19 +0300
Subject: [PATCH 1/5] Bump pre-commit hooks

---
 .pre-commit-config.yaml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a1a054e008f..c6485829984 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.11.12
+    rev: v0.12.0
     hooks:
       - id: ruff
         args: [--exit-non-zero-on-fix]
@@ -11,7 +11,7 @@ repos:
       - id: black
 
   - repo: https://github.com/PyCQA/bandit
-    rev: 1.8.3
+    rev: 1.8.5
     hooks:
     - id: bandit
       args: [--severity-level=high]
@@ -24,7 +24,7 @@ repos:
         exclude: (Makefile$|\.bat$|\.cmake$|\.eps$|\.fits$|\.gd$|\.opt$)
 
   - repo: https://github.com/pre-commit/mirrors-clang-format
-    rev: v20.1.5
+    rev: v20.1.6
     hooks:
       - id: clang-format
         types: [c]
@@ -51,7 +51,7 @@ repos:
         exclude: ^.github/.*TEMPLATE|^Tests/(fonts|images)/
 
   - repo: https://github.com/python-jsonschema/check-jsonschema
-    rev: 0.33.0
+    rev: 0.33.1
     hooks:
       - id: check-github-workflows
       - id: check-readthedocs

From 234875bf9001037f7510769f83e281bfe6012441 Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 15:01:14 +0300
Subject: [PATCH 2/5] Update Ruff hook from legacy

---
 .pre-commit-config.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c6485829984..6abb732bbaa 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ repos:
   - repo: https://github.com/astral-sh/ruff-pre-commit
     rev: v0.12.0
     hooks:
-      - id: ruff
+      - id: ruff-check
         args: [--exit-non-zero-on-fix]
 
   - repo: https://github.com/psf/black-pre-commit-mirror

From d1894dcd46c9096a91dd6ab39fe1df918ba18d6c Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Wed, 25 Jun 2025 16:47:06 +0300
Subject: [PATCH 3/5] Add match parameter to pytest.warns()

---
 Tests/helper.py                     |  2 +-
 Tests/test_core_resources.py        |  2 +-
 Tests/test_features.py              | 13 ++++++-------
 Tests/test_file_apng.py             |  8 ++++----
 Tests/test_file_avif.py             |  4 ++--
 Tests/test_file_gif.py              |  6 ++++--
 Tests/test_file_icns.py             |  4 +++-
 Tests/test_file_ico.py              |  2 +-
 Tests/test_file_iptc.py             |  6 +++---
 Tests/test_file_jpeg.py             | 13 +++++++------
 Tests/test_file_png.py              |  2 +-
 Tests/test_file_tga.py              |  4 +++-
 Tests/test_file_tiff.py             |  4 ++--
 Tests/test_file_tiff_metadata.py    |  4 ++--
 Tests/test_file_webp.py             |  4 ++--
 Tests/test_image.py                 | 12 ++++++------
 Tests/test_image_access.py          |  2 +-
 Tests/test_image_array.py           |  6 +++---
 Tests/test_image_convert.py         |  5 ++++-
 Tests/test_image_getim.py           |  4 ++--
 Tests/test_image_putdata.py         |  2 +-
 Tests/test_imagecms.py              | 14 +++++++-------
 Tests/test_imagedraw.py             |  2 +-
 Tests/test_imagefile.py             |  2 +-
 Tests/test_imagefont.py             | 12 ++++++------
 Tests/test_imagemath_lambda_eval.py |  2 +-
 Tests/test_imagemath_unsafe_eval.py |  4 ++--
 Tests/test_lib_pack.py              |  4 +++-
 28 files changed, 80 insertions(+), 69 deletions(-)

diff --git a/Tests/helper.py b/Tests/helper.py
index ec61cd263e9..e71b4665b28 100644
--- a/Tests/helper.py
+++ b/Tests/helper.py
@@ -272,7 +272,7 @@ def _cached_hopper(mode: str) -> Image.Image:
     else:
         im = hopper()
     if mode.startswith("BGR;"):
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="BGR;"):
             im = im.convert(mode)
     else:
         try:
diff --git a/Tests/test_core_resources.py b/Tests/test_core_resources.py
index 2c1de8bc3d2..2a22f805d8d 100644
--- a/Tests/test_core_resources.py
+++ b/Tests/test_core_resources.py
@@ -188,5 +188,5 @@ def test_units(self) -> None:
         ),
     )
     def test_warnings(self, var: dict[str, str]) -> None:
-        with pytest.warns(UserWarning):
+        with pytest.warns(UserWarning, match=list(var)[0]):
             Image._apply_env_variables(var)
diff --git a/Tests/test_features.py b/Tests/test_features.py
index f8f7f6eec59..d06fb4d841c 100644
--- a/Tests/test_features.py
+++ b/Tests/test_features.py
@@ -19,7 +19,7 @@ def test_check() -> None:
         assert features.check_codec(codec) == features.check(codec)
     for feature in features.features:
         if "webp" in feature:
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(DeprecationWarning, match="webp"):
                 assert features.check_feature(feature) == features.check(feature)
         else:
             assert features.check_feature(feature) == features.check(feature)
@@ -49,24 +49,24 @@ def test(name: str, function: Callable[[str], str | None]) -> None:
         test(codec, features.version_codec)
     for feature in features.features:
         if "webp" in feature:
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(DeprecationWarning, match="webp"):
                 test(feature, features.version_feature)
         else:
             test(feature, features.version_feature)
 
 
 def test_webp_transparency() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="transp_webp"):
         assert (features.check("transp_webp") or False) == features.check_module("webp")
 
 
 def test_webp_mux() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="webp_mux"):
         assert (features.check("webp_mux") or False) == features.check_module("webp")
 
 
 def test_webp_anim() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="webp_anim"):
         assert (features.check("webp_anim") or False) == features.check_module("webp")
 
 
@@ -95,10 +95,9 @@ def test_check_codecs(feature: str) -> None:
 
 
 def test_check_warns_on_nonexistent() -> None:
-    with pytest.warns(UserWarning) as cm:
+    with pytest.warns(UserWarning, match="Unknown feature 'typo'."):
         has_feature = features.check("typo")
     assert has_feature is False
-    assert str(cm[-1].message) == "Unknown feature 'typo'."
 
 
 def test_supported_modules() -> None:
diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index a5734c202a7..66410b3dad8 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -303,7 +303,7 @@ def test_apng_chunk_errors() -> None:
         assert isinstance(im, PngImagePlugin.PngImageFile)
         assert not im.is_animated
 
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
         with Image.open("Tests/images/apng/chunk_multi_actl.png") as im:
             im.load()
         assert isinstance(im, PngImagePlugin.PngImageFile)
@@ -330,14 +330,14 @@ def test_apng_chunk_errors() -> None:
 
 
 def test_apng_syntax_errors() -> None:
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
         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 pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
         with Image.open("Tests/images/apng/syntax_num_frames_zero_default.png") as im:
             assert isinstance(im, PngImagePlugin.PngImageFile)
             assert not im.is_animated
@@ -354,7 +354,7 @@ def test_apng_syntax_errors() -> None:
             im.seek(im.n_frames - 1)
             im.load()
 
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
         with Image.open("Tests/images/apng/syntax_num_frames_invalid.png") as im:
             assert isinstance(im, PngImagePlugin.PngImageFile)
             assert not im.is_animated
diff --git a/Tests/test_file_avif.py b/Tests/test_file_avif.py
index b2e586637fa..e42e1029161 100644
--- a/Tests/test_file_avif.py
+++ b/Tests/test_file_avif.py
@@ -77,8 +77,8 @@ class TestUnsupportedAvif:
     def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
         monkeypatch.setattr(AvifImagePlugin, "SUPPORTED", False)
 
-        with pytest.warns(UserWarning):
-            with pytest.raises(UnidentifiedImageError):
+        with pytest.raises(UnidentifiedImageError):
+            with pytest.warns(UserWarning, match="AVIF support not installed"):
                 with Image.open(TEST_AVIF_FILE):
                     pass
 
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
index 20d58a9dda4..29bc55ee55d 100644
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -1229,7 +1229,9 @@ def test_removed_transparency(tmp_path: Path) -> None:
         im.putpixel((x, 0), (x, 0, 0))
 
     im.info["transparency"] = (255, 255, 255)
-    with pytest.warns(UserWarning):
+    with pytest.warns(
+        UserWarning, match="Couldn't allocate palette entry for transparency"
+    ):
         im.save(out)
 
     with Image.open(out) as reloaded:
@@ -1251,7 +1253,7 @@ def test_rgb_transparency(tmp_path: Path) -> None:
     im = Image.new("RGB", (1, 1))
     im.info["transparency"] = b""
     ims = [Image.new("RGB", (1, 1))]
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="should be converted to RGBA images"):
         im.save(out, save_all=True, append_images=ims)
 
     with Image.open(out) as reloaded:
diff --git a/Tests/test_file_icns.py b/Tests/test_file_icns.py
index 2dabfd2f30a..8ff59161ff3 100644
--- a/Tests/test_file_icns.py
+++ b/Tests/test_file_icns.py
@@ -95,7 +95,9 @@ def test_sizes() -> None:
         for w, h, r in im.info["sizes"]:
             wr = w * r
             hr = h * r
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(
+                DeprecationWarning, match=r"Setting size to \(width, height, scale\)"
+            ):
                 im.size = (w, h, r)
             im.load()
             assert im.mode == "RGBA"
diff --git a/Tests/test_file_ico.py b/Tests/test_file_ico.py
index 5d2ace35e28..99c312ead92 100644
--- a/Tests/test_file_ico.py
+++ b/Tests/test_file_ico.py
@@ -233,7 +233,7 @@ def test_save_append_images(tmp_path: Path) -> None:
 def test_unexpected_size() -> None:
     # This image has been manually hexedited to state that it is 16x32
     # while the image within is still 16x16
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="Image was not the expected size"):
         with Image.open("Tests/images/hopper_unexpected.ico") as im:
             assert im.size == (16, 16)
 
diff --git a/Tests/test_file_iptc.py b/Tests/test_file_iptc.py
index c6c0c1aab9d..ffd3aad3e30 100644
--- a/Tests/test_file_iptc.py
+++ b/Tests/test_file_iptc.py
@@ -99,7 +99,7 @@ def test_i() -> None:
     c = b"a"
 
     # Act
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="IptcImagePlugin.i"):
         ret = IptcImagePlugin.i(c)
 
     # Assert
@@ -114,7 +114,7 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.setattr(sys, "stdout", mystdout)
 
     # Act
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="IptcImagePlugin.dump"):
         IptcImagePlugin.dump(c)
 
     # Assert
@@ -122,5 +122,5 @@ def test_dump(monkeypatch: pytest.MonkeyPatch) -> None:
 
 
 def test_pad_deprecation() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="IptcImagePlugin.PAD"):
         assert IptcImagePlugin.PAD == b"\0\0\0\0"
diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index 614044d97eb..0ba08aaf954 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -752,10 +752,11 @@ def test_bad_mpo_header(self) -> None:
 
         # Act
         # Shouldn't raise error
-        fn = "Tests/images/sugarshack_bad_mpo_header.jpg"
-        with pytest.warns(UserWarning, Image.open, fn) as im:
-            # Assert
-            assert im.format == "JPEG"
+        with pytest.warns(UserWarning, match="malformed MPO file"):
+            im = Image.open("Tests/images/sugarshack_bad_mpo_header.jpg")
+
+        # Assert
+        assert im.format == "JPEG"
 
     @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
     def test_save_correct_modes(self, mode: str) -> None:
@@ -1103,9 +1104,9 @@ def test_repr_jpeg_error_returns_none(self) -> None:
     def test_deprecation(self) -> None:
         with Image.open(TEST_FILE) as im:
             assert isinstance(im, JpegImagePlugin.JpegImageFile)
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(DeprecationWarning, match="huffman_ac"):
                 assert im.huffman_ac == {}
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(DeprecationWarning, match="huffman_dc"):
                 assert im.huffman_dc == {}
 
 
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
index 15f67385a2d..0a51fd49338 100644
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -805,7 +805,7 @@ def test_deprecation(self, tmp_path: Path) -> None:
         test_file = tmp_path / "out.png"
 
         im = hopper("I")
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="Saving I mode images as PNG"):
             im.save(test_file)
 
         with Image.open(test_file) as reloaded:
diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py
index 8b6ed3ed226..27ff4f1a4b8 100644
--- a/Tests/test_file_tga.py
+++ b/Tests/test_file_tga.py
@@ -190,7 +190,9 @@ def test_save_id_section(tmp_path: Path) -> None:
 
     # Save with custom id section greater than 255 characters
     id_section = b"Test content" * 25
-    with pytest.warns(UserWarning):
+    with pytest.warns(
+        UserWarning, match="id_section has been trimmed to 255 characters"
+    ):
         im.save(out, id_section=id_section)
 
     with Image.open(out) as test_im:
diff --git a/Tests/test_file_tiff.py b/Tests/test_file_tiff.py
index e92b97c8a5e..046a9f1f16e 100644
--- a/Tests/test_file_tiff.py
+++ b/Tests/test_file_tiff.py
@@ -221,7 +221,7 @@ def test_bad_exif(self) -> None:
             assert isinstance(im, JpegImagePlugin.JpegImageFile)
 
             # Should not raise struct.error.
-            with pytest.warns(UserWarning):
+            with pytest.warns(UserWarning, match="Corrupt EXIF data"):
                 im._getexif()
 
     def test_save_rgba(self, tmp_path: Path) -> None:
@@ -1014,7 +1014,7 @@ def test_timeout(self, monkeypatch: pytest.MonkeyPatch) -> None:
     @timeout_unless_slower_valgrind(2)
     def test_oom(self, test_file: str) -> None:
         with pytest.raises(UnidentifiedImageError):
-            with pytest.warns(UserWarning):
+            with pytest.warns(UserWarning, match="Corrupt EXIF data"):
                 with Image.open(test_file):
                     pass
 
diff --git a/Tests/test_file_tiff_metadata.py b/Tests/test_file_tiff_metadata.py
index 88486834593..36ad8cee93b 100644
--- a/Tests/test_file_tiff_metadata.py
+++ b/Tests/test_file_tiff_metadata.py
@@ -300,7 +300,7 @@ def test_empty_metadata() -> None:
     head = f.read(8)
     info = TiffImagePlugin.ImageFileDirectory(head)
     # Should not raise struct.error.
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="Corrupt EXIF data"):
         info.load(f)
 
 
@@ -481,7 +481,7 @@ def test_too_many_entries() -> None:
     ifd.tagtype[277] = TiffTags.SHORT
 
     # Should not raise ValueError.
-    with pytest.warns(UserWarning):
+    with pytest.warns(UserWarning, match="Metadata Warning"):
         assert ifd[277] == 4
 
 
diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py
index f61e2c82ee5..916ea56fcb6 100644
--- a/Tests/test_file_webp.py
+++ b/Tests/test_file_webp.py
@@ -33,8 +33,8 @@ def test_unsupported(self, monkeypatch: pytest.MonkeyPatch) -> None:
             monkeypatch.setattr(WebPImagePlugin, "SUPPORTED", False)
 
         file_path = "Tests/images/hopper.webp"
-        with pytest.warns(UserWarning):
-            with pytest.raises(OSError):
+        with pytest.raises(OSError):
+            with pytest.warns(UserWarning, match="WEBP support not installed"):
                 with Image.open(file_path):
                     pass
 
diff --git a/Tests/test_image.py b/Tests/test_image.py
index b018b4309e9..069083b1963 100644
--- a/Tests/test_image.py
+++ b/Tests/test_image.py
@@ -53,7 +53,7 @@
 # Deprecation helper
 def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
     if mode.startswith("BGR;"):
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="BGR;"):
             return Image.new(mode, size)
     else:
         return Image.new(mode, size)
@@ -141,8 +141,8 @@ def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None:
         monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True)
 
         im = io.BytesIO(b"")
-        with pytest.warns(UserWarning):
-            with pytest.raises(UnidentifiedImageError):
+        with pytest.raises(UnidentifiedImageError):
+            with pytest.warns(UserWarning, match="opening failed"):
                 with Image.open(im):
                     pass
 
@@ -1008,7 +1008,7 @@ def test_getxmp_padded(self) -> None:
 
     def test_get_child_images(self) -> None:
         im = Image.new("RGB", (1, 1))
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="Image.Image.get_child_images"):
             assert im.get_child_images() == []
 
     @pytest.mark.parametrize("size", ((1, 0), (0, 1), (0, 0)))
@@ -1139,7 +1139,7 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None:
             assert im.fp is None
 
     def test_deprecation(self) -> None:
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="Image.isImageType"):
             assert not Image.isImageType(None)
 
 
@@ -1150,7 +1150,7 @@ def test_roundtrip_bytes_constructor(self, mode: str) -> None:
         source_bytes = im.tobytes()
 
         if mode.startswith("BGR;"):
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(DeprecationWarning, match=mode):
                 reloaded = Image.frombytes(mode, im.size, source_bytes)
         else:
             reloaded = Image.frombytes(mode, im.size, source_bytes)
diff --git a/Tests/test_image_access.py b/Tests/test_image_access.py
index 14a5e2e7bfe..66412a03582 100644
--- a/Tests/test_image_access.py
+++ b/Tests/test_image_access.py
@@ -193,7 +193,7 @@ def test_basic(self, mode: str) -> None:
 
     @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
     def test_deprecated(self, mode: str) -> None:
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="BGR;"):
             self.check(mode)
 
     def test_list(self) -> None:
diff --git a/Tests/test_image_array.py b/Tests/test_image_array.py
index 2c71dceb841..c27ce13d5be 100644
--- a/Tests/test_image_array.py
+++ b/Tests/test_image_array.py
@@ -47,7 +47,7 @@ def test_with_dtype(dtype: npt.DTypeLike) -> None:
             with pytest.raises(OSError):
                 numpy.array(im_truncated)
         else:
-            with pytest.warns(DeprecationWarning):
+            with pytest.warns(DeprecationWarning, match="__array_interface__"):
                 numpy.array(im_truncated)
 
 
@@ -101,7 +101,7 @@ def __init__(self, arr_params: dict[str, Any]) -> None:
 
     with pytest.raises(ValueError):
         wrapped = Wrapper({"shape": (1, 1), "strides": (1, 1)})
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="'mode' parameter"):
             Image.fromarray(wrapped, "L")
 
 
@@ -111,7 +111,7 @@ def test_fromarray_palette() -> None:
     a = numpy.array(i)
 
     # Act
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="'mode' parameter"):
         out = Image.fromarray(a, "P")
 
     # Assert that the Python and C palettes match
diff --git a/Tests/test_image_convert.py b/Tests/test_image_convert.py
index 7d4f78c2371..33f8444378d 100644
--- a/Tests/test_image_convert.py
+++ b/Tests/test_image_convert.py
@@ -203,7 +203,10 @@ def test_trns_RGB(tmp_path: Path) -> None:
     assert "transparency" not in im_rgba.info
     assert im_rgba.getpixel((0, 0)) == (0, 0, 0, 0)
 
-    im_p = pytest.warns(UserWarning, im.convert, "P", palette=Image.Palette.ADAPTIVE)
+    with pytest.warns(
+        UserWarning, match="Couldn't allocate palette entry for transparency"
+    ):
+        im_p = im.convert("P", palette=Image.Palette.ADAPTIVE)
     assert "transparency" not in im_p.info
     im_p.save(f)
 
diff --git a/Tests/test_image_getim.py b/Tests/test_image_getim.py
index fa58492fc76..7b5f7a5890f 100644
--- a/Tests/test_image_getim.py
+++ b/Tests/test_image_getim.py
@@ -11,9 +11,9 @@ def test_sanity() -> None:
     type_repr = repr(type(im.getim()))
     assert "PyCapsule" in type_repr
 
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="id property"):
         assert isinstance(im.im.id, int)
 
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="unsafe_ptrs property"):
         ptrs = dict(im.im.unsafe_ptrs)
     assert ptrs.keys() == {"image8", "image32", "image"}
diff --git a/Tests/test_image_putdata.py b/Tests/test_image_putdata.py
index 27cb7c59d89..34c1763b886 100644
--- a/Tests/test_image_putdata.py
+++ b/Tests/test_image_putdata.py
@@ -81,7 +81,7 @@ def test_mode_F() -> None:
 @pytest.mark.parametrize("mode", ("BGR;15", "BGR;16", "BGR;24"))
 def test_mode_BGR(mode: str) -> None:
     data = [(16, 32, 49), (32, 32, 98)]
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match=mode):
         im = Image.new(mode, (1, 2))
     im.putdata(data)
 
diff --git a/Tests/test_imagecms.py b/Tests/test_imagecms.py
index f062651f0c5..b6db0ab5c1b 100644
--- a/Tests/test_imagecms.py
+++ b/Tests/test_imagecms.py
@@ -54,7 +54,7 @@ def skip_missing() -> None:
 def test_sanity() -> None:
     # basic smoke test.
     # this mostly follows the cms_test outline.
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="PIL.ImageCms.versions"):
         v = ImageCms.versions()  # should return four strings
     assert v[0] == "1.0.0 pil"
     assert list(map(type, v)) == [str, str, str, str]
@@ -679,7 +679,7 @@ def test_auxiliary_channels_isolated() -> None:
 
 def test_long_modes() -> None:
     p = ImageCms.getOpenProfile("Tests/icc/sGrey-v2-nano.icc")
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ABCDEFGHI"):
         ImageCms.buildTransform(p, p, "ABCDEFGHI", "ABCDEFGHI")
 
 
@@ -703,15 +703,15 @@ def test_cmyk_lab() -> None:
 
 
 def test_deprecation() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ImageCms.DESCRIPTION"):
         assert ImageCms.DESCRIPTION.strip().startswith("pyCMS")
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ImageCms.VERSION"):
         assert ImageCms.VERSION == "1.0.0 pil"
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ImageCms.FLAGS"):
         assert isinstance(ImageCms.FLAGS, dict)
 
     profile = ImageCmsProfile(ImageCms.createProfile("sRGB"))
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="RGBA;16B"):
         ImageCms.ImageCmsTransform(profile, profile, "RGBA;16B", "RGB")
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="RGBA;16B"):
         ImageCms.ImageCmsTransform(profile, profile, "RGB", "RGBA;16B")
diff --git a/Tests/test_imagedraw.py b/Tests/test_imagedraw.py
index 37669a2e55b..881f9c85dd2 100644
--- a/Tests/test_imagedraw.py
+++ b/Tests/test_imagedraw.py
@@ -1735,5 +1735,5 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
 
 
 def test_getdraw() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="'hints' parameter"):
         ImageDraw.getdraw(None, [])
diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index 7622eea99dd..a9444c26d5b 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -152,7 +152,7 @@ def read(self, size: int | None = None) -> bytes:
         assert reads.count(im.decodermaxblock) == 1
 
     def test_raise_oserror(self) -> None:
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="raise_oserror"):
             with pytest.raises(OSError):
                 ImageFile.raise_oserror(1)
 
diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py
index 69533c2f840..aa8bbb3394d 100644
--- a/Tests/test_imagefont.py
+++ b/Tests/test_imagefont.py
@@ -1175,15 +1175,15 @@ def test_oom(test_file: str) -> None:
 
 def test_raqm_missing_warning(monkeypatch: pytest.MonkeyPatch) -> None:
     monkeypatch.setattr(ImageFont.core, "HAVE_RAQM", False)
-    with pytest.warns(UserWarning) as record:
+    with pytest.warns(
+        UserWarning,
+        match="Raqm layout was requested, but Raqm is not available. "
+        "Falling back to basic layout.",
+    ):
         font = ImageFont.truetype(
             FONT_PATH, FONT_SIZE, layout_engine=ImageFont.Layout.RAQM
         )
     assert font.layout_engine == ImageFont.Layout.BASIC
-    assert str(record[-1].message) == (
-        "Raqm layout was requested, but Raqm is not available. "
-        "Falling back to basic layout."
-    )
 
 
 @pytest.mark.parametrize("size", [-1, 0])
@@ -1202,5 +1202,5 @@ def fake_version_module(module: str) -> str:
     monkeypatch.setattr(features, "version_module", fake_version_module)
 
     # Act / Assert
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="FreeType 2.9.0"):
         ImageFont.truetype(FONT_PATH, FONT_SIZE)
diff --git a/Tests/test_imagemath_lambda_eval.py b/Tests/test_imagemath_lambda_eval.py
index 360325780bb..eec76118af0 100644
--- a/Tests/test_imagemath_lambda_eval.py
+++ b/Tests/test_imagemath_lambda_eval.py
@@ -56,7 +56,7 @@ def test_sanity() -> None:
 
 
 def test_options_deprecated() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ImageMath.lambda_eval options"):
         assert ImageMath.lambda_eval(lambda args: 1, images) == 1
 
 
diff --git a/Tests/test_imagemath_unsafe_eval.py b/Tests/test_imagemath_unsafe_eval.py
index b7ac8469180..60ad6aafa49 100644
--- a/Tests/test_imagemath_unsafe_eval.py
+++ b/Tests/test_imagemath_unsafe_eval.py
@@ -36,12 +36,12 @@ def test_sanity() -> None:
 
 
 def test_eval_deprecated() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ImageMath.eval"):
         assert ImageMath.eval("1") == 1
 
 
 def test_options_deprecated() -> None:
-    with pytest.warns(DeprecationWarning):
+    with pytest.warns(DeprecationWarning, match="ImageMath.unsafe_eval options"):
         assert ImageMath.unsafe_eval("1", images) == 1
 
 
diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py
index b4a300d0c59..2d6af70eb78 100644
--- a/Tests/test_lib_pack.py
+++ b/Tests/test_lib_pack.py
@@ -362,13 +362,15 @@ def test_RGB(self) -> None:
         )
 
     def test_BGR(self) -> None:
-        with pytest.warns(DeprecationWarning):
+        with pytest.warns(DeprecationWarning, match="BGR;15"):
             self.assert_unpack(
                 "BGR;15", "BGR;15", 3, (8, 131, 0), (24, 0, 8), (41, 131, 8)
             )
+        with pytest.warns(DeprecationWarning, match="BGR;16"):
             self.assert_unpack(
                 "BGR;16", "BGR;16", 3, (8, 64, 0), (24, 129, 0), (41, 194, 0)
             )
+        with pytest.warns(DeprecationWarning, match="BGR;24"):
             self.assert_unpack("BGR;24", "BGR;24", 3, (1, 2, 3), (4, 5, 6), (7, 8, 9))
 
     def test_RGBA(self) -> None:

From a61a23d7ae054581db05d7eb94817685bc3ab3e4 Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Fri, 27 Jun 2025 13:00:48 +1000
Subject: [PATCH 4/5] Fixed PT031

---
 Tests/test_file_apng.py | 45 ++++++++++++++++++++++-------------------
 1 file changed, 24 insertions(+), 21 deletions(-)

diff --git a/Tests/test_file_apng.py b/Tests/test_file_apng.py
index 66410b3dad8..12204b5b78c 100644
--- a/Tests/test_file_apng.py
+++ b/Tests/test_file_apng.py
@@ -303,11 +303,11 @@ def test_apng_chunk_errors() -> None:
         assert isinstance(im, PngImagePlugin.PngImageFile)
         assert not im.is_animated
 
-    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
-        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 pytest.warns(UserWarning, match="Invalid APNG"):
+        im = Image.open("Tests/images/apng/chunk_multi_actl.png")
+    assert isinstance(im, PngImagePlugin.PngImageFile)
+    assert not im.is_animated
+    im.close()
 
     with Image.open("Tests/images/apng/chunk_actl_after_idat.png") as im:
         assert isinstance(im, PngImagePlugin.PngImageFile)
@@ -330,18 +330,20 @@ def test_apng_chunk_errors() -> None:
 
 
 def test_apng_syntax_errors() -> None:
-    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
-        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, match="Invalid APNG"):
+        im = Image.open("Tests/images/apng/syntax_num_frames_zero.png")
+    assert isinstance(im, PngImagePlugin.PngImageFile)
+    assert not im.is_animated
+    with pytest.raises(OSError):
+        im.load()
+    im.close()
 
-    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
-        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()
+    with pytest.warns(UserWarning, match="Invalid APNG"):
+        im = Image.open("Tests/images/apng/syntax_num_frames_zero_default.png")
+    assert isinstance(im, PngImagePlugin.PngImageFile)
+    assert not im.is_animated
+    im.load()
+    im.close()
 
     # we can handle this case gracefully
     with Image.open("Tests/images/apng/syntax_num_frames_low.png") as im:
@@ -354,11 +356,12 @@ def test_apng_syntax_errors() -> None:
             im.seek(im.n_frames - 1)
             im.load()
 
-    with pytest.warns(UserWarning, match="Invalid APNG"):  # noqa: PT031
-        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()
+    with pytest.warns(UserWarning, match="Invalid APNG"):
+        im = Image.open("Tests/images/apng/syntax_num_frames_invalid.png")
+    assert isinstance(im, PngImagePlugin.PngImageFile)
+    assert not im.is_animated
+    im.load()
+    im.close()
 
 
 @pytest.mark.parametrize(

From 958c449b988e49aa88a412990bebdb799a97a39f Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Fri, 27 Jun 2025 16:17:20 +0300
Subject: [PATCH 5/5] Close image after assert

Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com>
---
 Tests/test_file_jpeg.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py
index c0e6b467ec6..6dab418bfda 100644
--- a/Tests/test_file_jpeg.py
+++ b/Tests/test_file_jpeg.py
@@ -752,6 +752,8 @@ def test_bad_mpo_header(self) -> None:
         # Assert
         assert im.format == "JPEG"
 
+        im.close()
+
     @pytest.mark.parametrize("mode", ("1", "L", "RGB", "RGBX", "CMYK", "YCbCr"))
     def test_save_correct_modes(self, mode: str) -> None:
         out = BytesIO()
