Skip to content
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
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ exclude_also =
if DEBUG:
# Don't complain about compatibility code for missing optional dependencies
except ImportError
if TYPE_CHECKING:
@abc.abstractmethod

[run]
omit =
Expand Down
10 changes: 0 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,6 @@ warn_redundant_casts = true
warn_unreachable = true
warn_unused_ignores = true
exclude = [
'^src/PIL/_tkinter_finder.py$',
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
'^src/PIL/ImageQt.py$',
'^src/PIL/ImImagePlugin.py$',
'^src/PIL/MicImagePlugin.py$',
'^src/PIL/PdfParser.py$',
'^src/PIL/PyAccess.py$',
'^src/PIL/TiffImagePlugin.py$',
'^src/PIL/TiffTags.py$',
'^src/PIL/WebPImagePlugin.py$',
]
16 changes: 10 additions & 6 deletions src/PIL/DdsImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,17 @@ class D3DFMT(IntEnum):
# Backward compatibility layer
module = sys.modules[__name__]
for item in DDSD:
assert item.name is not None
setattr(module, "DDSD_" + item.name, item.value)
for item in DDSCAPS:
setattr(module, "DDSCAPS_" + item.name, item.value)
for item in DDSCAPS2:
setattr(module, "DDSCAPS2_" + item.name, item.value)
for item in DDPF:
setattr(module, "DDPF_" + item.name, item.value)
for item1 in DDSCAPS:
assert item1.name is not None
setattr(module, "DDSCAPS_" + item1.name, item1.value)
for item2 in DDSCAPS2:
assert item2.name is not None
setattr(module, "DDSCAPS2_" + item2.name, item2.value)
for item3 in DDPF:
assert item3.name is not None
setattr(module, "DDPF_" + item3.name, item3.value)

DDS_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB
Expand Down
4 changes: 2 additions & 2 deletions src/PIL/ImImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@
for i in ["32S"]:
OPEN[f"L {i} image"] = ("I", f"I;{i}")
OPEN[f"L*{i} image"] = ("I", f"I;{i}")
for i in range(2, 33):
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
for j in range(2, 33):
OPEN[f"L*{j} image"] = ("F", f"F;{j}")


# --------------------------------------------------------------------
Expand Down
87 changes: 57 additions & 30 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

from __future__ import annotations

import abc
import atexit
import builtins
import io
Expand All @@ -40,11 +41,8 @@
from collections.abc import Callable, MutableMapping
from enum import IntEnum
from pathlib import Path

try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
from types import ModuleType
from typing import IO, TYPE_CHECKING, Any

# VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0.
Expand All @@ -60,6 +58,12 @@
from ._binary import i32le, o32be, o32le
from ._util import DeferredError, is_path

ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -110,6 +114,7 @@ class DecompressionBombError(Exception):


USE_CFFI_ACCESS = False
cffi: ModuleType | None
try:
import cffi
except ImportError:
Expand Down Expand Up @@ -211,14 +216,22 @@ class Quantize(IntEnum):
# --------------------------------------------------------------------
# Registries

ID = []
OPEN = {}
MIME = {}
SAVE = {}
SAVE_ALL = {}
EXTENSION = {}
DECODERS = {}
ENCODERS = {}
if TYPE_CHECKING:
from . import ImageFile
ID: list[str] = []
OPEN: dict[
str,
tuple[
Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
Callable[[bytes], bool] | None,
],
] = {}
MIME: dict[str, str] = {}
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] = {}

# --------------------------------------------------------------------
# Modes
Expand Down Expand Up @@ -2383,12 +2396,12 @@ def save(self, fp, format=None, **params) -> None:
may have been created, and may contain partial data.
"""

filename = ""
filename: str | bytes = ""
open_fp = False
if isinstance(fp, Path):
filename = str(fp)
open_fp = True
elif is_path(fp):
elif isinstance(fp, (str, bytes)):
filename = fp
open_fp = True
elif fp == sys.stdout:
Expand All @@ -2398,7 +2411,7 @@ def save(self, fp, format=None, **params) -> None:
pass
if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes
filename = fp.name
filename = os.path.realpath(os.fspath(fp.name))

# may mutate self!
self._ensure_mutable()
Expand All @@ -2409,7 +2422,8 @@ def save(self, fp, format=None, **params) -> None:

preinit()

ext = os.path.splitext(filename)[1].lower()
filename_ext = os.path.splitext(filename)[1].lower()
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext

if not format:
if ext not in EXTENSION:
Expand Down Expand Up @@ -2451,7 +2465,7 @@ def save(self, fp, format=None, **params) -> None:
if open_fp:
fp.close()

def seek(self, frame) -> Image:
def seek(self, frame) -> None:
"""
Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an
Expand Down Expand Up @@ -2511,10 +2525,8 @@ def split(self) -> tuple[Image, ...]:

self.load()
if self.im.bands == 1:
ims = [self.copy()]
else:
ims = map(self._new, self.im.split())
return tuple(ims)
return (self.copy(),)
return tuple(map(self._new, self.im.split()))

def getchannel(self, channel):
"""
Expand Down Expand Up @@ -2871,7 +2883,14 @@ class ImageTransformHandler:
(for use with :py:meth:`~PIL.Image.Image.transform`)
"""

pass
@abc.abstractmethod
def transform(
self,
size: tuple[int, int],
image: Image,
**options: dict[str, str | int | tuple[int, ...] | list[int]],
) -> Image:
pass


# --------------------------------------------------------------------
Expand Down Expand Up @@ -3243,11 +3262,9 @@ def open(fp, mode="r", formats=None) -> Image:
raise TypeError(msg)

exclusive_fp = False
filename = ""
if isinstance(fp, Path):
filename = str(fp.resolve())
elif is_path(fp):
filename = fp
filename: str | bytes = ""
if is_path(fp):
filename = os.path.realpath(os.fspath(fp))

if filename:
fp = builtins.open(filename, "rb")
Expand Down Expand Up @@ -3421,7 +3438,11 @@ def merge(mode, bands):
# Plugin registry


def register_open(id, factory, accept=None) -> None:
def register_open(
id,
factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
accept: Callable[[bytes], bool] | None = None,
) -> None:
"""
Register an image file plugin. This function should not be used
in application code.
Expand Down Expand Up @@ -3631,7 +3652,13 @@ def _apply_env_variables(env=None):
atexit.register(core.clear_cache)


class Exif(MutableMapping):
if TYPE_CHECKING:
_ExifBase = MutableMapping[int, Any]
else:
_ExifBase = MutableMapping


class Exif(_ExifBase):
"""
This class provides read and write access to EXIF image data::

Expand Down
12 changes: 10 additions & 2 deletions src/PIL/ImageQt.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,26 @@

import sys
from io import BytesIO
from typing import Callable

from . import Image
from ._util import is_path

qt_version: str | None
qt_versions = [
["6", "PyQt6"],
["side6", "PySide6"],
]

# If a version has already been imported, attempt it first
qt_versions.sort(key=lambda qt_version: qt_version[1] in sys.modules, reverse=True)
for qt_version, qt_module in qt_versions:
qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
for version, qt_module in qt_versions:
try:
QBuffer: type
QIODevice: type
QImage: type
QPixmap: type
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice
from PyQt6.QtGui import QImage, QPixmap, qRgba
Expand All @@ -41,6 +48,7 @@
except (ImportError, RuntimeError):
continue
qt_is_installed = True
qt_version = version
break
else:
qt_is_installed = False
Expand Down
2 changes: 1 addition & 1 deletion src/PIL/ImageShow.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class UnixViewer(Viewer):

@abc.abstractmethod
def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
pass # pragma: no cover
pass

def get_command(self, file: str, **options: Any) -> str:
command = self.get_command_ex(file, **options)[0]
Expand Down
11 changes: 9 additions & 2 deletions src/PIL/PdfParser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import time
import zlib
from typing import TYPE_CHECKING, Any, List, Union


# see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
Expand Down Expand Up @@ -239,12 +240,18 @@ def __bytes__(self):
return bytes(result)


class PdfArray(list):
class PdfArray(List[Any]):
def __bytes__(self):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]"


class PdfDict(collections.UserDict):
if TYPE_CHECKING:
_DictBase = collections.UserDict[Union[str, bytes], Any]
else:
_DictBase = collections.UserDict


class PdfDict(_DictBase):
def __setattr__(self, key, value):
if key == "data":
collections.UserDict.__setattr__(self, key, value)
Expand Down
1 change: 1 addition & 0 deletions src/PIL/PyAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from ._deprecate import deprecate

FFI: type
try:
from cffi import FFI

Expand Down
Loading