From 3199c0ea40c041d41fe2499c86893a7e795f0929 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 15 Feb 2024 20:20:42 +1100 Subject: [PATCH] Decoder and encoders subclass PyDecoder and PyEncoder --- Tests/test_file_jpeg.py | 8 +----- Tests/test_image.py | 16 ++++------- Tests/test_imagefile.py | 64 ++++++++++++++++++++--------------------- src/PIL/Image.py | 14 ++++----- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index 65424214838..4858d92e6ea 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -986,13 +986,7 @@ class InfiniteMockPyDecoder(ImageFile.PyDecoder): def decode(self, buffer: bytes) -> tuple[int, int]: return 0, 0 - decoder = InfiniteMockPyDecoder(None) - - def closure(mode: str, *args) -> InfiniteMockPyDecoder: - decoder.__init__(mode, *args) - return decoder - - Image.register_decoder("INFINITE", closure) + Image.register_decoder("INFINITE", InfiniteMockPyDecoder) with Image.open(TEST_FILE) as im: im.tile = [ diff --git a/Tests/test_image.py b/Tests/test_image.py index 4c04e0da48e..aae51eaa42a 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -16,6 +16,7 @@ ExifTags, Image, ImageDraw, + ImageFile, ImagePalette, UnidentifiedImageError, features, @@ -1038,25 +1039,20 @@ def test_close_graceful(self, caplog: pytest.LogCaptureFixture) -> None: assert im.fp is None -class MockEncoder: - args: tuple[str, ...] - - -def mock_encode(*args: str) -> MockEncoder: - encoder = MockEncoder() - encoder.args = args - return encoder +class MockEncoder(ImageFile.PyEncoder): + pass class TestRegistry: def test_encode_registry(self) -> None: - Image.register_encoder("MOCK", mock_encode) + Image.register_encoder("MOCK", MockEncoder) assert "MOCK" in Image.ENCODERS enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) assert isinstance(enc, MockEncoder) - assert enc.args == ("RGB", "args", "extra") + assert enc.mode == "RGB" + assert enc.args == ("args", "extra") def test_encode_registry_fail(self) -> None: with pytest.raises(OSError): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 49140978168..cf251c9cec6 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -1,6 +1,7 @@ from __future__ import annotations from io import BytesIO +from typing import Any import pytest @@ -201,12 +202,22 @@ def test_broken_datastream_without_errors(self) -> None: class MockPyDecoder(ImageFile.PyDecoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyDecoder.last = self + + super().__init__(mode, *args) + def decode(self, buffer): # eof return -1, 0 class MockPyEncoder(ImageFile.PyEncoder): + def __init__(self, mode: str, *args: Any) -> None: + MockPyEncoder.last = self + + super().__init__(mode, *args) + def encode(self, buffer): return 1, 1, b"" @@ -228,19 +239,8 @@ def _open(self) -> None: class CodecsTest: @classmethod def setup_class(cls) -> None: - cls.decoder = MockPyDecoder(None) - cls.encoder = MockPyEncoder(None) - - def decoder_closure(mode, *args): - cls.decoder.__init__(mode, *args) - return cls.decoder - - def encoder_closure(mode, *args): - cls.encoder.__init__(mode, *args) - return cls.encoder - - Image.register_decoder("MOCK", decoder_closure) - Image.register_encoder("MOCK", encoder_closure) + Image.register_decoder("MOCK", MockPyDecoder) + Image.register_encoder("MOCK", MockPyEncoder) class TestPyDecoder(CodecsTest): @@ -251,13 +251,13 @@ def test_setimage(self) -> None: im.load() - assert self.decoder.state.xoff == xoff - assert self.decoder.state.yoff == yoff - assert self.decoder.state.xsize == xsize - assert self.decoder.state.ysize == ysize + assert MockPyDecoder.last.state.xoff == xoff + assert MockPyDecoder.last.state.yoff == yoff + assert MockPyDecoder.last.state.xsize == xsize + assert MockPyDecoder.last.state.ysize == ysize with pytest.raises(ValueError): - self.decoder.set_as_raw(b"\x00") + MockPyDecoder.last.set_as_raw(b"\x00") def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -267,10 +267,10 @@ def test_extents_none(self) -> None: im.load() - assert self.decoder.state.xoff == 0 - assert self.decoder.state.yoff == 0 - assert self.decoder.state.xsize == 200 - assert self.decoder.state.ysize == 200 + assert MockPyDecoder.last.state.xoff == 0 + assert MockPyDecoder.last.state.yoff == 0 + assert MockPyDecoder.last.state.xsize == 200 + assert MockPyDecoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -315,10 +315,10 @@ def test_setimage(self) -> None: im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] ) - assert self.encoder.state.xoff == xoff - assert self.encoder.state.yoff == yoff - assert self.encoder.state.xsize == xsize - assert self.encoder.state.ysize == ysize + assert MockPyEncoder.last.state.xoff == xoff + assert MockPyEncoder.last.state.yoff == yoff + assert MockPyEncoder.last.state.xsize == xsize + assert MockPyEncoder.last.state.ysize == ysize def test_extents_none(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -329,10 +329,10 @@ def test_extents_none(self) -> None: fp = BytesIO() ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) - assert self.encoder.state.xoff == 0 - assert self.encoder.state.yoff == 0 - assert self.encoder.state.xsize == 200 - assert self.encoder.state.ysize == 200 + assert MockPyEncoder.last.state.xoff == 0 + assert MockPyEncoder.last.state.yoff == 0 + assert MockPyEncoder.last.state.xsize == 200 + assert MockPyEncoder.last.state.ysize == 200 def test_negsize(self) -> None: buf = BytesIO(b"\x00" * 255) @@ -340,12 +340,12 @@ def test_negsize(self) -> None: im = MockImageFile(buf) fp = BytesIO() - self.encoder.cleanup_called = False + MockPyEncoder.last = None with pytest.raises(ValueError): ImageFile._save( im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] ) - assert self.encoder.cleanup_called + assert MockPyEncoder.last.cleanup_called with pytest.raises(ValueError): ImageFile._save( diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a770488b7ec..eba30537f13 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -229,8 +229,8 @@ class Quantize(IntEnum): SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} EXTENSION: dict[str, str] = {} -DECODERS: dict[str, object] = {} -ENCODERS: dict[str, object] = {} +DECODERS: dict[str, type[ImageFile.PyDecoder]] = {} +ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {} # -------------------------------------------------------------------- # Modes @@ -3524,28 +3524,26 @@ def registered_extensions(): return EXTENSION -def register_decoder(name: str, decoder) -> None: +def register_decoder(name: str, decoder: type[ImageFile.PyDecoder]) -> None: """ Registers an image decoder. This function should not be used in application code. :param name: The name of the decoder - :param decoder: A callable(mode, args) that returns an - ImageFile.PyDecoder object + :param decoder: An ImageFile.PyDecoder object .. versionadded:: 4.1.0 """ DECODERS[name] = decoder -def register_encoder(name, encoder): +def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None: """ Registers an image encoder. This function should not be used in application code. :param name: The name of the encoder - :param encoder: A callable(mode, args) that returns an - ImageFile.PyEncoder object + :param encoder: An ImageFile.PyEncoder object .. versionadded:: 4.1.0 """