Merge pull request #7944 from nulano/type-image-open

Add type hints for `Image.open`, `Image.init`, and `Image.Image.save`
This commit is contained in:
Andrew Murray 2024-04-07 08:31:55 +10:00 committed by GitHub
commit 84a02c851a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 59 additions and 42 deletions

View File

@ -241,7 +241,7 @@ class BLPFormatError(NotImplementedError):
pass pass
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] in (b"BLP1", b"BLP2") return prefix[:4] in (b"BLP1", b"BLP2")

View File

@ -48,7 +48,7 @@ BIT2MODE = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:2] == b"BM" return prefix[:2] == b"BM"

View File

@ -29,7 +29,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC" return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"

View File

@ -25,7 +25,7 @@ from ._binary import i32le as i32
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"\0\0\2\0" return prefix[:4] == b"\0\0\2\0"

View File

@ -29,7 +29,7 @@ from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then? MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 4 and i32(prefix) == MAGIC return len(prefix) >= 4 and i32(prefix) == MAGIC

View File

@ -562,7 +562,7 @@ def _save(im, fp, filename):
) )
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"DDS " return prefix[:4] == b"DDS "

View File

@ -195,7 +195,7 @@ class PSFile:
return b"".join(s).decode("latin-1") return b"".join(s).decode("latin-1")
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5) return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)

View File

@ -27,7 +27,7 @@ from ._binary import o8
# decoder # decoder
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
len(prefix) >= 6 len(prefix) >= 6
and i16(prefix, 4) in [0xAF11, 0xAF12] and i16(prefix, 4) in [0xAF11, 0xAF12]

View File

@ -41,7 +41,7 @@ MODES = {
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == olefile.MAGIC return prefix[:8] == olefile.MAGIC

View File

@ -107,7 +107,7 @@ class FtexImageFile(ImageFile.ImageFile):
pass pass
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == MAGIC return prefix[:4] == MAGIC

View File

@ -29,7 +29,7 @@ from . import Image, ImageFile
from ._binary import i32be as i32 from ._binary import i32be as i32
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2) return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2)

View File

@ -60,7 +60,7 @@ LOADING_STRATEGY = LoadingStrategy.RGB_AFTER_FIRST
# Identify/read GIF files # Identify/read GIF files
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:6] in [b"GIF87a", b"GIF89a"] return prefix[:6] in [b"GIF87a", b"GIF89a"]

View File

@ -29,7 +29,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"GRIB" and prefix[7] == 1 return prefix[:4] == b"GRIB" and prefix[7] == 1

View File

@ -29,7 +29,7 @@ def register_handler(handler):
# Image adapter # Image adapter
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == b"\x89HDF\r\n\x1a\n" return prefix[:8] == b"\x89HDF\r\n\x1a\n"

View File

@ -374,7 +374,7 @@ def _save(im, fp, filename):
fp.flush() fp.flush()
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == MAGIC return prefix[:4] == MAGIC

View File

@ -114,7 +114,7 @@ def _save(im, fp, filename):
fp.seek(current) fp.seek(current)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == _MAGIC return prefix[:4] == _MAGIC

View File

@ -41,7 +41,7 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any from typing import IO, TYPE_CHECKING, Any, Literal, cast
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -55,7 +55,7 @@ from . import (
_plugins, _plugins,
) )
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._typing import TypeGuard from ._typing import StrOrBytesPath, TypeGuard
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
ElementTree: ModuleType | None ElementTree: ModuleType | None
@ -223,7 +223,7 @@ OPEN: dict[
str, str,
tuple[ tuple[
Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
Callable[[bytes], bool] | None, Callable[[bytes], bool | str] | None,
], ],
] = {} ] = {}
MIME: dict[str, str] = {} MIME: dict[str, str] = {}
@ -357,7 +357,7 @@ def preinit() -> None:
_initialized = 1 _initialized = 1
def init(): def init() -> bool:
""" """
Explicitly initializes the Python Imaging Library. This function Explicitly initializes the Python Imaging Library. This function
loads all available file format drivers. loads all available file format drivers.
@ -368,7 +368,7 @@ def init():
global _initialized global _initialized
if _initialized >= 2: if _initialized >= 2:
return 0 return False
parent_name = __name__.rpartition(".")[0] parent_name = __name__.rpartition(".")[0]
for plugin in _plugins: for plugin in _plugins:
@ -380,7 +380,8 @@ def init():
if OPEN or SAVE: if OPEN or SAVE:
_initialized = 2 _initialized = 2
return 1 return True
return False
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -2373,7 +2374,9 @@ class Image:
(w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor (w, h), Transform.AFFINE, matrix, resample, fillcolor=fillcolor
) )
def save(self, fp, format=None, **params) -> None: def save(
self, fp: StrOrBytesPath | IO[bytes], format: str | None = None, **params: Any
) -> None:
""" """
Saves this image under the given filename. If no format is Saves this image under the given filename. If no format is
specified, the format to use is determined from the filename specified, the format to use is determined from the filename
@ -2454,6 +2457,8 @@ class Image:
fp = builtins.open(filename, "r+b") fp = builtins.open(filename, "r+b")
else: else:
fp = builtins.open(filename, "w+b") fp = builtins.open(filename, "w+b")
else:
fp = cast(IO[bytes], fp)
try: try:
save_handler(self, fp, filename) save_handler(self, fp, filename)
@ -3222,7 +3227,11 @@ def _decompression_bomb_check(size: tuple[int, int]) -> None:
) )
def open(fp, mode="r", formats=None) -> Image: def open(
fp: StrOrBytesPath | IO[bytes],
mode: Literal["r"] = "r",
formats: list[str] | tuple[str, ...] | None = None,
) -> ImageFile.ImageFile:
""" """
Opens and identifies the given image file. Opens and identifies the given image file.
@ -3253,10 +3262,10 @@ def open(fp, mode="r", formats=None) -> Image:
""" """
if mode != "r": if mode != "r":
msg = f"bad mode {repr(mode)}" msg = f"bad mode {repr(mode)}" # type: ignore[unreachable]
raise ValueError(msg) raise ValueError(msg)
elif isinstance(fp, io.StringIO): elif isinstance(fp, io.StringIO):
msg = ( msg = ( # type: ignore[unreachable]
"StringIO cannot be used to open an image. " "StringIO cannot be used to open an image. "
"Binary data must be used instead." "Binary data must be used instead."
) )
@ -3265,7 +3274,7 @@ def open(fp, mode="r", formats=None) -> Image:
if formats is None: if formats is None:
formats = ID formats = ID
elif not isinstance(formats, (list, tuple)): elif not isinstance(formats, (list, tuple)):
msg = "formats must be a list or tuple" msg = "formats must be a list or tuple" # type: ignore[unreachable]
raise TypeError(msg) raise TypeError(msg)
exclusive_fp = False exclusive_fp = False
@ -3276,6 +3285,8 @@ def open(fp, mode="r", formats=None) -> Image:
if filename: if filename:
fp = builtins.open(filename, "rb") fp = builtins.open(filename, "rb")
exclusive_fp = True exclusive_fp = True
else:
fp = cast(IO[bytes], fp)
try: try:
fp.seek(0) fp.seek(0)
@ -3287,9 +3298,14 @@ def open(fp, mode="r", formats=None) -> Image:
preinit() preinit()
accept_warnings = [] accept_warnings: list[str] = []
def _open_core(fp, filename, prefix, formats): def _open_core(
fp: IO[bytes],
filename: str | bytes,
prefix: bytes,
formats: list[str] | tuple[str, ...],
) -> ImageFile.ImageFile | None:
for i in formats: for i in formats:
i = i.upper() i = i.upper()
if i not in OPEN: if i not in OPEN:
@ -3297,7 +3313,7 @@ def open(fp, mode="r", formats=None) -> Image:
try: try:
factory, accept = OPEN[i] factory, accept = OPEN[i]
result = not accept or accept(prefix) result = not accept or accept(prefix)
if type(result) in [str, bytes]: if isinstance(result, str):
accept_warnings.append(result) accept_warnings.append(result)
elif result: elif result:
fp.seek(0) fp.seek(0)
@ -3318,7 +3334,7 @@ def open(fp, mode="r", formats=None) -> Image:
im = _open_core(fp, filename, prefix, formats) im = _open_core(fp, filename, prefix, formats)
if im is None and formats is ID: if im is None and formats is ID:
checked_formats = formats.copy() checked_formats = ID.copy()
if init(): if init():
im = _open_core( im = _open_core(
fp, fp,
@ -3448,7 +3464,7 @@ def merge(mode, bands):
def register_open( def register_open(
id, id,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile], factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool] | None = None, accept: Callable[[bytes], bool | str] | None = None,
) -> None: ) -> None:
""" """
Register an image file plugin. This function should not be used Register an image file plugin. This function should not be used

View File

@ -317,7 +317,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
return ImageFile.ImageFile.load(self) return ImageFile.ImageFile.load(self)
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
prefix[:4] == b"\xff\x4f\xff\x51" prefix[:4] == b"\xff\x4f\xff\x51"
or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a" or prefix[:12] == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"

View File

@ -344,7 +344,7 @@ MARKER = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool:
# Magic number was taken from https://en.wikipedia.org/wiki/JPEG # Magic number was taken from https://en.wikipedia.org/wiki/JPEG
return prefix[:3] == b"\xFF\xD8\xFF" return prefix[:3] == b"\xFF\xD8\xFF"

View File

@ -25,7 +25,7 @@ from . import Image, TiffImagePlugin
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == olefile.MAGIC return prefix[:8] == olefile.MAGIC

View File

@ -689,7 +689,7 @@ class PngStream(ChunkStream):
# PNG reader # PNG reader
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:8] == _MAGIC return prefix[:8] == _MAGIC

View File

@ -44,7 +44,7 @@ MODES = {
# read PSD images # read PSD images
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"8BPS" return prefix[:4] == b"8BPS"

View File

@ -13,7 +13,7 @@ from . import Image, ImageFile
from ._binary import i32be as i32 from ._binary import i32be as i32
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] == b"qoif" return prefix[:4] == b"qoif"

View File

@ -277,7 +277,7 @@ PREFIXES = [
] ]
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:4] in PREFIXES return prefix[:4] in PREFIXES

View File

@ -23,7 +23,7 @@ _VP8_MODES_BY_IDENTIFIER = {
} }
def _accept(prefix): def _accept(prefix: bytes) -> bool | str:
is_riff_file_format = prefix[:4] == b"RIFF" is_riff_file_format = prefix[:4] == b"RIFF"
is_webp_file = prefix[8:12] == b"WEBP" is_webp_file = prefix[8:12] == b"WEBP"
is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER is_valid_vp8_mode = prefix[12:16] in _VP8_MODES_BY_IDENTIFIER
@ -34,6 +34,7 @@ def _accept(prefix):
"image file could not be identified because WEBP support not installed" "image file could not be identified because WEBP support not installed"
) )
return True return True
return False
class WebPImageFile(ImageFile.ImageFile): class WebPImageFile(ImageFile.ImageFile):

View File

@ -65,7 +65,7 @@ if hasattr(Image.core, "drawwmf"):
# Read WMF file # Read WMF file
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return ( return (
prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00" prefix[:6] == b"\xd7\xcd\xc6\x9a\x00\x00" or prefix[:4] == b"\x01\x00\x00\x00"
) )

View File

@ -24,7 +24,7 @@ from ._binary import o8
xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)') xpm_head = re.compile(b'"([0-9]*) ([0-9]*) ([0-9]*) ([0-9]*)')
def _accept(prefix): def _accept(prefix: bytes) -> bool:
return prefix[:9] == b"/* XPM */" return prefix[:9] == b"/* XPM */"