This commit is contained in:
Joren Hammudoglu 2026-02-03 07:24:23 +00:00 committed by GitHub
commit 93469770eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 116 additions and 76 deletions

View File

@ -19,6 +19,7 @@ from PIL import (
UnidentifiedImageError,
features,
)
from PIL._typing import Buffer
from .helper import (
assert_image,
@ -40,6 +41,7 @@ try:
except ImportError:
ElementTree = None
TEST_FILE = "Tests/images/hopper.jpg"
@ -1054,7 +1056,7 @@ class TestFileJpeg:
# the image should still end when there is no new data
class InfiniteMockPyDecoder(ImageFile.PyDecoder):
def decode(
self, buffer: bytes | Image.SupportsArrayInterface
self, buffer: Buffer | Image.SupportsArrayInterface
) -> tuple[int, int]:
return 0, 0

View File

@ -165,7 +165,7 @@ def test_reduce() -> None:
assert callable(im.reduce)
im.reduce = 2 # type: ignore[assignment, method-assign]
assert im.reduce == 2
assert im.reduce == 2 # type: ignore[comparison-overlap]
im.load()
assert im.size == (160, 120)

View File

@ -11,7 +11,7 @@ from .helper import assert_image_equal, hopper
def test_sanity(data_type: str) -> None:
im1 = hopper()
data = im1.tobytes()
data: bytes | memoryview[int] = im1.tobytes()
if data_type == "memoryview":
data = memoryview(data)
im2 = Image.frombytes(im1.mode, im1.size, data)

View File

@ -14,6 +14,7 @@ from PIL import (
_binary,
features,
)
from PIL._typing import Buffer
from .helper import (
assert_image,
@ -232,7 +233,7 @@ class MockPyDecoder(ImageFile.PyDecoder):
super().__init__(mode, *args)
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
# eof
return -1, 0

View File

@ -116,7 +116,7 @@ class TestImageWinDib:
# Act
# Make one the same as the using tobytes()/frombytes()
test_buffer = dib1.tobytes()
test_buffer: bytes | memoryview[int] = dib1.tobytes()
for datatype in ("bytes", "memoryview"):
if datatype == "memoryview":
test_buffer = memoryview(test_buffer)

View File

@ -10,13 +10,15 @@ TYPE_CHECKING = False
if TYPE_CHECKING:
from pathlib import Path
QImage = type
else:
if ImageQt.qt_is_installed:
from PIL.ImageQt import QImage
pytestmark = pytest.mark.skipif(
not ImageQt.qt_is_installed, reason="Qt bindings are not installed"
)
if ImageQt.qt_is_installed:
from PIL.ImageQt import QImage
@pytest.mark.parametrize("mode", ("RGB", "RGBA", "L", "P", "1"))
def test_sanity(mode: str, tmp_path: Path) -> None:

View File

@ -14,10 +14,16 @@ from __future__ import annotations
import struct
from io import BytesIO
from typing import IO
from PIL import Image, ImageFile
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO
from typing_extensions import Buffer
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
@ -258,7 +264,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DXT1Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt1(self.fd, self.state.xsize, self.state.ysize))
@ -271,7 +277,7 @@ class DXT1Decoder(ImageFile.PyDecoder):
class DXT5Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
try:
self.set_as_raw(_dxt5(self.fd, self.state.xsize, self.state.ysize))

View File

@ -216,11 +216,10 @@ testpaths = [
[tool.mypy]
python_version = "3.10"
pretty = true
disallow_any_generics = true
disallow_untyped_defs = true
strict = true
disallow_subclassing_any = false
disallow_untyped_calls = false
enable_error_code = "ignore-without-code"
extra_checks = true
follow_imports = "silent"
warn_redundant_casts = true
no_implicit_reexport = false
warn_return_any = false
warn_unreachable = true
warn_unused_ignores = true

View File

@ -39,6 +39,7 @@ from io import BytesIO
from typing import IO
from . import Image, ImageFile
from ._typing import Buffer
class Format(IntEnum):
@ -295,7 +296,7 @@ class BlpImageFile(ImageFile.ImageFile):
class _BLPBaseDecoder(abc.ABC, ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
try:
self._read_header()
self._load()

View File

@ -33,6 +33,7 @@ from ._binary import i32le as i32
from ._binary import o8
from ._binary import o16le as o16
from ._binary import o32le as o32
from ._typing import Buffer
#
# --------------------------------------------------------------------
@ -327,7 +328,7 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
rle4 = self.args[1]
data = bytearray()

View File

@ -21,6 +21,7 @@ from . import Image, ImageFile, ImagePalette
from ._binary import i32le as i32
from ._binary import o8
from ._binary import o32le as o32
from ._typing import Buffer
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
@ -488,7 +489,7 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
bitcount, masks = self.args

View File

@ -203,7 +203,9 @@ class EpsImageFile(ImageFile.ImageFile):
imagedata_size: tuple[int, int] | None = None
byte_arr = bytearray(255)
bytes_mv = memoryview(byte_arr)
# the extra `bytes` annotation here works around several false positive
# `comparison-overlap` mypy errors
bytes_mv: bytes | memoryview = memoryview(byte_arr)
bytes_read = 0
reading_header_comments = True
reading_trailer_comments = False

View File

@ -14,6 +14,7 @@ import gzip
import math
from . import Image, ImageFile
from ._typing import Buffer
def _accept(prefix: bytes) -> bool:
@ -126,7 +127,7 @@ class FitsImageFile(ImageFile.ImageFile):
class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
value = gzip.decompress(self.fd.read())

View File

@ -52,7 +52,6 @@ if TYPE_CHECKING:
from typing import IO, Literal
from . import _imaging
from ._typing import Buffer
class LoadingStrategy(IntEnum):
@ -1188,7 +1187,7 @@ def getdata(
class Collector(BytesIO):
data = []
def write(self, data: Buffer) -> int:
def write(self, data: bytes) -> int: # type: ignore[override]
self.data.append(data)
return len(data)

View File

@ -69,6 +69,8 @@ if TYPE_CHECKING:
from types import ModuleType
from typing import Any, Literal
from ._typing import Buffer
logger = logging.getLogger(__name__)
@ -86,37 +88,45 @@ WARN_POSSIBLE_FORMATS: bool = False
MAX_IMAGE_PIXELS: int | None = int(1024 * 1024 * 1024 // 4 // 3)
try:
# If the _imaging C module is not present, Pillow will not load.
# Note that other modules should not refer to _imaging directly;
# import Image and use the Image.core variable instead.
# Also note that Image.core is not a publicly documented interface,
# and should be considered private and subject to change.
from . import _imaging as core
if TYPE_CHECKING:
from . import _imaging
if __version__ != getattr(core, "PILLOW_VERSION", None):
msg = (
"The _imaging extension was built for another version of Pillow or PIL:\n"
f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n"
f"Pillow version: {__version__}"
)
raise ImportError(msg)
# mypy will not recognize `core` as a public symbol when imported as
# `from . import _imaging as core`
core = _imaging
else:
try:
# If the _imaging C module is not present, Pillow will not load.
# Note that other modules should not refer to _imaging directly;
# import Image and use the Image.core variable instead.
# Also note that Image.core is not a publicly documented interface,
# and should be considered private and subject to change.
from . import _imaging as core
except ImportError as v:
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
# the right version (windows only). Print a warning, if
# possible.
warnings.warn(
"The _imaging extension was built for another version of Python.",
RuntimeWarning,
)
elif str(v).startswith("The _imaging extension"):
warnings.warn(str(v), RuntimeWarning)
# Fail here anyway. Don't let people run with a mostly broken Pillow.
# see docs/porting.rst
raise
if __version__ != getattr(core, "PILLOW_VERSION", None):
msg = (
f"The _imaging extension was built for another version of Pillow or "
f"PIL:\n "
f"Core version: {getattr(core, 'PILLOW_VERSION', None)}\n"
f"Pillow version: {__version__}"
)
raise ImportError(msg)
except ImportError as v:
# Explanations for ways that we know we might have an import error
if str(v).startswith("Module use of python"):
# The _imaging C module is present, but not compiled for
# the right version (windows only). Print a warning, if
# possible.
warnings.warn(
"The _imaging extension was built for another version of Python.",
RuntimeWarning,
)
elif str(v).startswith("The _imaging extension"):
warnings.warn(str(v), RuntimeWarning)
# Fail here anyway. Don't let people run with a mostly broken Pillow.
# see docs/porting.rst
raise
#
@ -931,7 +941,7 @@ class Image:
def frombytes(
self,
data: bytes | bytearray | SupportsArrayInterface,
data: Buffer | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> None:
@ -3232,7 +3242,7 @@ def new(
def frombytes(
mode: str,
size: tuple[int, int],
data: bytes | bytearray | SupportsArrayInterface,
data: Buffer | SupportsArrayInterface,
decoder_name: str = "raw",
*args: Any,
) -> Image:

View File

@ -41,7 +41,7 @@ from ._util import DeferredError, is_path
TYPE_CHECKING = False
if TYPE_CHECKING:
from ._typing import StrOrBytesPath
from ._typing import Buffer, StrOrBytesPath
logger = logging.getLogger(__name__)
@ -839,7 +839,7 @@ class PyDecoder(PyCodec):
def pulls_fd(self) -> bool:
return self._pulls_fd
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
"""
Override to perform the decoding process.
@ -852,7 +852,10 @@ class PyDecoder(PyCodec):
raise NotImplementedError(msg)
def set_as_raw(
self, data: bytes, rawmode: str | None = None, extra: tuple[Any, ...] = ()
self,
data: bytes | bytearray,
rawmode: str | None = None,
extra: tuple[Any, ...] = (),
) -> None:
"""
Convenience method to set the internal image from a stream of raw data
@ -893,7 +896,7 @@ class PyEncoder(PyCodec):
def pushes_fd(self) -> bool:
return self._pushes_fd
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
def encode(self, bufsize: int) -> tuple[int, int, bytes | bytearray]:
"""
Override to perform the encoding process.

View File

@ -19,6 +19,7 @@
from __future__ import annotations
from . import Image
from ._typing import Buffer
class HDC:
@ -183,7 +184,7 @@ class Dib:
else:
self.image.paste(im.im)
def frombytes(self, buffer: bytes) -> None:
def frombytes(self, buffer: Buffer) -> None:
"""
Load display memory contents from byte data.

View File

@ -31,6 +31,7 @@ from typing import IO
from . import Image, ImageFile
from ._binary import i16le as i16
from ._binary import o16le as o16
from ._typing import Buffer
#
# read MSP files
@ -112,7 +113,7 @@ class MspDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
img = io.BytesIO()

View File

@ -14,6 +14,8 @@ TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import IO
from ._typing import Buffer
_DictBase = collections.UserDict[str | bytes, Any]
else:
_DictBase = collections.UserDict
@ -316,11 +318,11 @@ class PdfBinary:
class PdfStream:
def __init__(self, dictionary: PdfDict, buf: bytes) -> None:
def __init__(self, dictionary: PdfDict, buf: Buffer) -> None:
self.dictionary = dictionary
self.buf = buf
def decode(self) -> bytes:
def decode(self) -> Buffer:
try:
filter = self.dictionary[b"Filter"]
except KeyError:

View File

@ -22,6 +22,7 @@ from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
from ._binary import o32le as o32
from ._typing import Buffer
#
# --------------------------------------------------------------------
@ -169,12 +170,12 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
return self.fd.read(ImageFile.SAFEBLOCK)
def _find_comment_end(self, block: bytes, start: int = 0) -> int:
def _find_comment_end(self, block: bytes | bytearray, start: int = 0) -> int:
a = block.find(b"\n", start)
b = block.find(b"\r", start)
return min(a, b) if a * b > 0 else max(a, b) # lowest nonnegative index (or -1)
def _ignore_comments(self, block: bytes) -> bytes:
def _ignore_comments(self, block: bytes | bytearray) -> bytes | bytearray:
if self._comment_spans:
# Finish current comment
while block:
@ -216,6 +217,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
data = bytearray()
total_bytes = self.state.xsize * self.state.ysize
block: bytes | bytearray
while len(data) != total_bytes:
block = self._read_block() # read next block
if not block:
@ -241,9 +243,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
bands = Image.getmodebands(self.mode)
total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count
half_token = b""
half_token: bytes | bytearray = b""
while len(data) != total_bytes:
block = self._read_block() # read next block
block: bytes | bytearray = self._read_block() # read next block
if not block:
if half_token:
block = bytearray(b" ") # flush half_token
@ -284,7 +286,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
break
return data
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
self._comment_spans = False
if self.mode == "1":
data = self._decode_bitonal()
@ -300,7 +302,7 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
class PpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
data = bytearray()

View File

@ -14,6 +14,7 @@ from . import Image, ImageFile
from ._binary import i32be as i32
from ._binary import o8
from ._binary import o32be as o32
from ._typing import Buffer
def _accept(prefix: bytes) -> bool:
@ -51,7 +52,7 @@ class QoiDecoder(ImageFile.PyDecoder):
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
self._previously_seen_pixels = {}
@ -151,7 +152,7 @@ class QoiEncoder(ImageFile.PyEncoder):
result -= 256
return result
def encode(self, bufsize: int) -> tuple[int, int, bytes]:
def encode(self, bufsize: int) -> tuple[int, int, bytearray]:
assert self.im is not None
self._previously_seen_pixels = {0: (0, 0, 0, 0)}

View File

@ -29,6 +29,7 @@ from typing import IO
from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
from ._typing import Buffer
def _accept(prefix: bytes) -> bool:
@ -198,7 +199,7 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
class SGI16Decoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
assert self.im is not None

View File

@ -19,6 +19,7 @@ import re
from . import Image, ImageFile, ImagePalette
from ._binary import o8
from ._typing import Buffer
# XPM header
xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)')
@ -118,7 +119,7 @@ class XpmImageFile(ImageFile.ImageFile):
class XpmDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int]:
def decode(self, buffer: Buffer | Image.SupportsArrayInterface) -> tuple[int, int]:
assert self.fd is not None
data = bytearray()

View File

@ -21,10 +21,13 @@ if sys.version_info >= (3, 13):
else:
CapsuleType = object
if sys.version_info >= (3, 12):
from collections.abc import Buffer
if TYPE_CHECKING:
from typing_extensions import Buffer
else:
Buffer = Any
if sys.version_info >= (3, 12):
from collections.abc import Buffer
else:
Buffer = Any
_Ink = float | tuple[int, ...] | str