diff --git a/Tests/test_file_jpeg.py b/Tests/test_file_jpeg.py index f818927f6a3..be6461c93be 100644 --- a/Tests/test_file_jpeg.py +++ b/Tests/test_file_jpeg.py @@ -19,6 +19,7 @@ UnidentifiedImageError, features, ) +from PIL._typing import Buffer from .helper import ( assert_image, @@ -40,6 +41,7 @@ except ImportError: ElementTree = None + TEST_FILE = "Tests/images/hopper.jpg" @@ -1064,7 +1066,7 @@ def test_eof(self, monkeypatch: pytest.MonkeyPatch) -> None: # 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 diff --git a/Tests/test_file_jpeg2k.py b/Tests/test_file_jpeg2k.py index 575d911def5..0b88526ff17 100644 --- a/Tests/test_file_jpeg2k.py +++ b/Tests/test_file_jpeg2k.py @@ -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) diff --git a/Tests/test_image_frombytes.py b/Tests/test_image_frombytes.py index 98c0ea0b43c..18aa4ccf108 100644 --- a/Tests/test_image_frombytes.py +++ b/Tests/test_image_frombytes.py @@ -11,7 +11,7 @@ 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) diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index 7dfb3abf986..ec3cdfaec4e 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -14,6 +14,7 @@ _binary, features, ) +from PIL._typing import Buffer from .helper import ( assert_image, @@ -238,7 +239,7 @@ def __init__(self, mode: str, *args: Any) -> None: 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 diff --git a/Tests/test_imagewin.py b/Tests/test_imagewin.py index e7af160ddd2..505dcbe8e68 100644 --- a/Tests/test_imagewin.py +++ b/Tests/test_imagewin.py @@ -116,7 +116,7 @@ def test_dib_frombytes_tobytes_roundtrip(self) -> None: # 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) diff --git a/Tests/test_qt_image_toqimage.py b/Tests/test_qt_image_toqimage.py index 0004b552153..5647847d427 100644 --- a/Tests/test_qt_image_toqimage.py +++ b/Tests/test_qt_image_toqimage.py @@ -10,13 +10,15 @@ 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: diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py index e0557976c28..7a054dcca5e 100644 --- a/docs/example/DdsImagePlugin.py +++ b/docs/example/DdsImagePlugin.py @@ -14,10 +14,16 @@ 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 @@ def load_seek(self, pos: int) -> None: 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 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int 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)) diff --git a/pyproject.toml b/pyproject.toml index cc616bc547c..12f6fab3c2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py index 6bb92edf891..11139c3522d 100644 --- a/src/PIL/BlpImagePlugin.py +++ b/src/PIL/BlpImagePlugin.py @@ -39,6 +39,7 @@ from typing import IO from . import Image, ImageFile +from ._typing import Buffer class Format(IntEnum): @@ -295,7 +296,7 @@ def _open(self) -> None: 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() diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py index a1227137017..810c19d2504 100644 --- a/src/PIL/BmpImagePlugin.py +++ b/src/PIL/BmpImagePlugin.py @@ -33,6 +33,7 @@ from ._binary import o8 from ._binary import o16le as o16 from ._binary import o32le as o32 +from ._typing import Buffer # # -------------------------------------------------------------------- @@ -327,7 +328,7 @@ def _open(self) -> None: 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() diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 312f602a6b1..4a61759f61e 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -21,6 +21,7 @@ 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 @@ def load_seek(self, pos: int) -> None: 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 diff --git a/src/PIL/EpsImagePlugin.py b/src/PIL/EpsImagePlugin.py index 2effb816cfb..5ca78c5d7a5 100644 --- a/src/PIL/EpsImagePlugin.py +++ b/src/PIL/EpsImagePlugin.py @@ -203,7 +203,9 @@ def _open(self) -> None: 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 diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py index a3fdc0efeec..943dd62c580 100644 --- a/src/PIL/FitsImagePlugin.py +++ b/src/PIL/FitsImagePlugin.py @@ -14,6 +14,7 @@ import math from . import Image, ImageFile +from ._typing import Buffer def _accept(prefix: bytes) -> bool: @@ -126,7 +127,7 @@ def _parse_headers( 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()) diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py index 76a0d4ab99f..64378d52a8e 100644 --- a/src/PIL/GifImagePlugin.py +++ b/src/PIL/GifImagePlugin.py @@ -52,7 +52,6 @@ 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) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index fdcc680b900..394b333abba 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -69,6 +69,8 @@ from types import ModuleType from typing import Any, Literal + from ._typing import Buffer + logger = logging.getLogger(__name__) @@ -86,37 +88,45 @@ class DecompressionBombError(Exception): 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 __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) +if TYPE_CHECKING: + from . import _imaging -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 + # 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 + + 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 @@ def tobitmap(self, name: str = "image") -> bytes: 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: diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 78abe3c77be..3b29c47c9e7 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -41,7 +41,7 @@ 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 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int 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. diff --git a/src/PIL/ImageWin.py b/src/PIL/ImageWin.py index 98c28f29f1d..18f3f73c30f 100644 --- a/src/PIL/ImageWin.py +++ b/src/PIL/ImageWin.py @@ -19,6 +19,7 @@ from __future__ import annotations from . import Image +from ._typing import Buffer class HDC: @@ -183,7 +184,7 @@ def paste( 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. diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py index 277087a8677..81a288858bb 100644 --- a/src/PIL/MspImagePlugin.py +++ b/src/PIL/MspImagePlugin.py @@ -31,6 +31,7 @@ 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() diff --git a/src/PIL/PdfParser.py b/src/PIL/PdfParser.py index 2c9031469ad..f537c78bb9e 100644 --- a/src/PIL/PdfParser.py +++ b/src/PIL/PdfParser.py @@ -14,6 +14,8 @@ 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 @@ def __bytes__(self) -> bytes: 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: diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 307bc97ff65..6bc5c8e0a03 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -22,6 +22,7 @@ from ._binary import i16be as i16 from ._binary import o8 from ._binary import o32le as o32 +from ._typing import Buffer # # -------------------------------------------------------------------- @@ -169,12 +170,12 @@ def _read_block(self) -> bytes: 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 @@ def _decode_bitonal(self) -> bytearray: 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 @@ def _decode_blocks(self, maxval: int) -> bytearray: 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 @@ def _decode_blocks(self, maxval: int) -> bytearray: 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 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int 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() diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py index d0709b1198a..0dd6e7416b6 100644 --- a/src/PIL/QoiImagePlugin.py +++ b/src/PIL/QoiImagePlugin.py @@ -14,6 +14,7 @@ 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 @@ def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: 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 @@ def _delta(self, left: int, right: int) -> int: 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)} diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py index 853022150ae..336f859fb36 100644 --- a/src/PIL/SgiImagePlugin.py +++ b/src/PIL/SgiImagePlugin.py @@ -29,6 +29,7 @@ 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 diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py index 3be240fbc1a..55302fab4fb 100644 --- a/src/PIL/XpmImagePlugin.py +++ b/src/PIL/XpmImagePlugin.py @@ -19,6 +19,7 @@ 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 @@ def load_read(self, read_bytes: int) -> bytes: 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() diff --git a/src/PIL/_typing.py b/src/PIL/_typing.py index a941f89806f..19c6a4d6a32 100644 --- a/src/PIL/_typing.py +++ b/src/PIL/_typing.py @@ -21,10 +21,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