Add ImageFile context manager (#9367)

This commit is contained in:
Hugo van Kemenade 2026-01-01 15:50:26 +02:00 committed by GitHub
commit 8dee8dd5ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 19 additions and 14 deletions

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:
"""

View File

@ -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
@ -168,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:
@ -176,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.
@ -268,7 +278,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