Decoder and encoders subclass PyDecoder and PyEncoder

This commit is contained in:
Andrew Murray 2024-02-15 20:20:42 +11:00
parent 912a33f5e9
commit 3199c0ea40
4 changed files with 45 additions and 57 deletions

View File

@ -986,13 +986,7 @@ class TestFileJpeg:
def decode(self, buffer: bytes) -> tuple[int, int]: def decode(self, buffer: bytes) -> tuple[int, int]:
return 0, 0 return 0, 0
decoder = InfiniteMockPyDecoder(None) Image.register_decoder("INFINITE", InfiniteMockPyDecoder)
def closure(mode: str, *args) -> InfiniteMockPyDecoder:
decoder.__init__(mode, *args)
return decoder
Image.register_decoder("INFINITE", closure)
with Image.open(TEST_FILE) as im: with Image.open(TEST_FILE) as im:
im.tile = [ im.tile = [

View File

@ -16,6 +16,7 @@ from PIL import (
ExifTags, ExifTags,
Image, Image,
ImageDraw, ImageDraw,
ImageFile,
ImagePalette, ImagePalette,
UnidentifiedImageError, UnidentifiedImageError,
features, features,
@ -1038,25 +1039,20 @@ class TestImage:
assert im.fp is None assert im.fp is None
class MockEncoder: class MockEncoder(ImageFile.PyEncoder):
args: tuple[str, ...] pass
def mock_encode(*args: str) -> MockEncoder:
encoder = MockEncoder()
encoder.args = args
return encoder
class TestRegistry: class TestRegistry:
def test_encode_registry(self) -> None: def test_encode_registry(self) -> None:
Image.register_encoder("MOCK", mock_encode) Image.register_encoder("MOCK", MockEncoder)
assert "MOCK" in Image.ENCODERS assert "MOCK" in Image.ENCODERS
enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",)) enc = Image._getencoder("RGB", "MOCK", ("args",), extra=("extra",))
assert isinstance(enc, MockEncoder) 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: def test_encode_registry_fail(self) -> None:
with pytest.raises(OSError): with pytest.raises(OSError):

View File

@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from typing import Any
import pytest import pytest
@ -201,12 +202,22 @@ class TestImageFile:
class MockPyDecoder(ImageFile.PyDecoder): class MockPyDecoder(ImageFile.PyDecoder):
def __init__(self, mode: str, *args: Any) -> None:
MockPyDecoder.last = self
super().__init__(mode, *args)
def decode(self, buffer): def decode(self, buffer):
# eof # eof
return -1, 0 return -1, 0
class MockPyEncoder(ImageFile.PyEncoder): class MockPyEncoder(ImageFile.PyEncoder):
def __init__(self, mode: str, *args: Any) -> None:
MockPyEncoder.last = self
super().__init__(mode, *args)
def encode(self, buffer): def encode(self, buffer):
return 1, 1, b"" return 1, 1, b""
@ -228,19 +239,8 @@ class MockImageFile(ImageFile.ImageFile):
class CodecsTest: class CodecsTest:
@classmethod @classmethod
def setup_class(cls) -> None: def setup_class(cls) -> None:
cls.decoder = MockPyDecoder(None) Image.register_decoder("MOCK", MockPyDecoder)
cls.encoder = MockPyEncoder(None) Image.register_encoder("MOCK", MockPyEncoder)
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)
class TestPyDecoder(CodecsTest): class TestPyDecoder(CodecsTest):
@ -251,13 +251,13 @@ class TestPyDecoder(CodecsTest):
im.load() im.load()
assert self.decoder.state.xoff == xoff assert MockPyDecoder.last.state.xoff == xoff
assert self.decoder.state.yoff == yoff assert MockPyDecoder.last.state.yoff == yoff
assert self.decoder.state.xsize == xsize assert MockPyDecoder.last.state.xsize == xsize
assert self.decoder.state.ysize == ysize assert MockPyDecoder.last.state.ysize == ysize
with pytest.raises(ValueError): with pytest.raises(ValueError):
self.decoder.set_as_raw(b"\x00") MockPyDecoder.last.set_as_raw(b"\x00")
def test_extents_none(self) -> None: def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
@ -267,10 +267,10 @@ class TestPyDecoder(CodecsTest):
im.load() im.load()
assert self.decoder.state.xoff == 0 assert MockPyDecoder.last.state.xoff == 0
assert self.decoder.state.yoff == 0 assert MockPyDecoder.last.state.yoff == 0
assert self.decoder.state.xsize == 200 assert MockPyDecoder.last.state.xsize == 200
assert self.decoder.state.ysize == 200 assert MockPyDecoder.last.state.ysize == 200
def test_negsize(self) -> None: def test_negsize(self) -> None:
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
@ -315,10 +315,10 @@ class TestPyEncoder(CodecsTest):
im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")] im, fp, [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 0, "RGB")]
) )
assert self.encoder.state.xoff == xoff assert MockPyEncoder.last.state.xoff == xoff
assert self.encoder.state.yoff == yoff assert MockPyEncoder.last.state.yoff == yoff
assert self.encoder.state.xsize == xsize assert MockPyEncoder.last.state.xsize == xsize
assert self.encoder.state.ysize == ysize assert MockPyEncoder.last.state.ysize == ysize
def test_extents_none(self) -> None: def test_extents_none(self) -> None:
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
@ -329,10 +329,10 @@ class TestPyEncoder(CodecsTest):
fp = BytesIO() fp = BytesIO()
ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")]) ImageFile._save(im, fp, [("MOCK", None, 0, "RGB")])
assert self.encoder.state.xoff == 0 assert MockPyEncoder.last.state.xoff == 0
assert self.encoder.state.yoff == 0 assert MockPyEncoder.last.state.yoff == 0
assert self.encoder.state.xsize == 200 assert MockPyEncoder.last.state.xsize == 200
assert self.encoder.state.ysize == 200 assert MockPyEncoder.last.state.ysize == 200
def test_negsize(self) -> None: def test_negsize(self) -> None:
buf = BytesIO(b"\x00" * 255) buf = BytesIO(b"\x00" * 255)
@ -340,12 +340,12 @@ class TestPyEncoder(CodecsTest):
im = MockImageFile(buf) im = MockImageFile(buf)
fp = BytesIO() fp = BytesIO()
self.encoder.cleanup_called = False MockPyEncoder.last = None
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(
im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")] im, fp, [("MOCK", (xoff, yoff, -10, yoff + ysize), 0, "RGB")]
) )
assert self.encoder.cleanup_called assert MockPyEncoder.last.cleanup_called
with pytest.raises(ValueError): with pytest.raises(ValueError):
ImageFile._save( ImageFile._save(

View File

@ -229,8 +229,8 @@ MIME: dict[str, str] = {}
SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} SAVE: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {} SAVE_ALL: dict[str, Callable[[Image, IO[bytes], str | bytes], None]] = {}
EXTENSION: dict[str, str] = {} EXTENSION: dict[str, str] = {}
DECODERS: dict[str, object] = {} DECODERS: dict[str, type[ImageFile.PyDecoder]] = {}
ENCODERS: dict[str, object] = {} ENCODERS: dict[str, type[ImageFile.PyEncoder]] = {}
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Modes # Modes
@ -3524,28 +3524,26 @@ def registered_extensions():
return EXTENSION 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 Registers an image decoder. This function should not be
used in application code. used in application code.
:param name: The name of the decoder :param name: The name of the decoder
:param decoder: A callable(mode, args) that returns an :param decoder: An ImageFile.PyDecoder object
ImageFile.PyDecoder object
.. versionadded:: 4.1.0 .. versionadded:: 4.1.0
""" """
DECODERS[name] = decoder 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 Registers an image encoder. This function should not be
used in application code. used in application code.
:param name: The name of the encoder :param name: The name of the encoder
:param encoder: A callable(mode, args) that returns an :param encoder: An ImageFile.PyEncoder object
ImageFile.PyEncoder object
.. versionadded:: 4.1.0 .. versionadded:: 4.1.0
""" """