Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decoders and encoders subclass PyDecoder and PyEncoder #7801

Merged
merged 1 commit into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions Tests/test_file_jpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
16 changes: 6 additions & 10 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ExifTags,
Image,
ImageDraw,
ImageFile,
ImagePalette,
UnidentifiedImageError,
features,
Expand Down Expand Up @@ -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):
Expand Down
64 changes: 32 additions & 32 deletions Tests/test_imagefile.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from io import BytesIO
from typing import Any

import pytest

Expand Down Expand Up @@ -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""

Expand All @@ -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):
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -329,23 +329,23 @@ 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)

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(
Expand Down
14 changes: 6 additions & 8 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand Down
Loading