From 51b35d17e1429a7a1fda48e61832fb9dd9bc2adf Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jan 2026 20:15:05 +1100 Subject: [PATCH 1/2] Added fp type hint --- src/PIL/ImageFile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index f609c7d13..1df1d33a1 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -131,6 +131,7 @@ 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 @@ -268,7 +269,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 From ce11a0c4993c284791b3100227cbd0338937074d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 1 Jan 2026 20:27:48 +1100 Subject: [PATCH 2/2] Added ImageFile context manager --- Tests/test_file_jpeg2k.py | 2 +- Tests/test_file_png.py | 2 +- Tests/test_file_ppm.py | 2 +- Tests/test_image_transform.py | 4 ++-- src/PIL/Image.py | 11 +++-------- src/PIL/ImageFile.py | 9 +++++++++ 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index a5365a90d..575d911de 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -164,7 +164,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[assignment, method-assign] assert im.reduce == 2 im.load() diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py index e9830cd3d..ed3a91285 100644 --- a/Tests/test_file_png.py +++ b/Tests/test_file_png.py @@ -823,7 +823,7 @@ class TestFilePng: monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_PNG_FILE) as im: - im.save(sys.stdout, "PNG") + im.save(sys.stdout, "PNG") # type: ignore[arg-type] if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 598e9a445..fbca46be5 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -389,7 +389,7 @@ def test_save_stdout(buffer: bool, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(sys, "stdout", mystdout) with Image.open(TEST_FILE) as im: - im.save(sys.stdout, "PPM") + im.save(sys.stdout, "PPM") # type: ignore[arg-type] if isinstance(mystdout, MyStdOut): mystdout = mystdout.buffer diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index 7cf52ddba..3e2b9fee8 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -250,14 +250,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/src/PIL/Image.py b/src/PIL/Image.py index 4e61900f6..b4de099be 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -590,16 +590,11 @@ class Image: return new # Context manager support - def __enter__(self): + def __enter__(self) -> Image: return self - def __exit__(self, *args): - from . import ImageFile - - if isinstance(self, ImageFile.ImageFile): - if getattr(self, "_exclusive_fp", False): - self._close_fp() - self.fp = None + def __exit__(self, *args: object) -> None: + pass def close(self) -> None: """ diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 1df1d33a1..3390dfa97 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -169,6 +169,10 @@ 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) and not isinstance(self._fp, DeferredError): if self._fp != self.fp: @@ -177,6 +181,11 @@ class ImageFile(Image.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.