Added type hints

This commit is contained in:
Andrew Murray 2024-02-10 19:50:45 +11:00
parent 6782a07b8e
commit 19a6edeecc
13 changed files with 171 additions and 100 deletions

View File

@ -141,16 +141,6 @@ warn_redundant_casts = true
warn_unreachable = true warn_unreachable = true
warn_unused_ignores = true warn_unused_ignores = true
exclude = [ exclude = [
'^src/PIL/_tkinter_finder.py$',
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$', '^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
'^src/PIL/ImageQt.py$',
'^src/PIL/ImImagePlugin.py$',
'^src/PIL/MicImagePlugin.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$',
] ]

View File

@ -270,13 +270,17 @@ class D3DFMT(IntEnum):
# Backward compatibility layer # Backward compatibility layer
module = sys.modules[__name__] module = sys.modules[__name__]
for item in DDSD: for item in DDSD:
assert item.name is not None
setattr(module, "DDSD_" + item.name, item.value) setattr(module, "DDSD_" + item.name, item.value)
for item in DDSCAPS: for item1 in DDSCAPS:
setattr(module, "DDSCAPS_" + item.name, item.value) assert item1.name is not None
for item in DDSCAPS2: setattr(module, "DDSCAPS_" + item1.name, item1.value)
setattr(module, "DDSCAPS2_" + item.name, item.value) for item2 in DDSCAPS2:
for item in DDPF: assert item2.name is not None
setattr(module, "DDPF_" + item.name, item.value) 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_FOURCC = DDPF.FOURCC
DDS_RGB = DDPF.RGB DDS_RGB = DDPF.RGB

View File

@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]:
for i in ["32S"]: for i in ["32S"]:
OPEN[f"L {i} image"] = ("I", f"I;{i}") OPEN[f"L {i} image"] = ("I", f"I;{i}")
OPEN[f"L*{i} image"] = ("I", f"I;{i}") OPEN[f"L*{i} image"] = ("I", f"I;{i}")
for i in range(2, 33): for j in range(2, 33):
OPEN[f"L*{i} image"] = ("F", f"F;{i}") OPEN[f"L*{j} image"] = ("F", f"F;{j}")
# -------------------------------------------------------------------- # --------------------------------------------------------------------

View File

@ -26,6 +26,7 @@
from __future__ import annotations from __future__ import annotations
import abc
import atexit import atexit
import builtins import builtins
import io import io
@ -40,11 +41,8 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from pathlib import Path from pathlib import Path
from types import ModuleType
try: from typing import IO, TYPE_CHECKING, Any
from defusedxml import ElementTree
except ImportError:
ElementTree = None
# 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.
@ -60,6 +58,12 @@ from . import (
from ._binary import i32le, o32be, o32le from ._binary import i32le, o32be, o32le
from ._util import DeferredError, is_path from ._util import DeferredError, is_path
ElementTree: ModuleType | None
try:
from defusedxml import ElementTree
except ImportError:
ElementTree = None
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -110,6 +114,7 @@ except ImportError as v:
USE_CFFI_ACCESS = False USE_CFFI_ACCESS = False
cffi: ModuleType | None
try: try:
import cffi import cffi
except ImportError: except ImportError:
@ -211,14 +216,22 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
# Registries # Registries
ID = [] if TYPE_CHECKING:
OPEN = {} from . import ImageFile # pragma: no cover
MIME = {} ID: list[str] = []
SAVE = {} OPEN: dict[
SAVE_ALL = {} str,
EXTENSION = {} tuple[
DECODERS = {} Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
ENCODERS = {} 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 # Modes
@ -2383,12 +2396,12 @@ class Image:
may have been created, and may contain partial data. may have been created, and may contain partial data.
""" """
filename = "" filename: str | bytes = ""
open_fp = False open_fp = False
if isinstance(fp, Path): if isinstance(fp, Path):
filename = str(fp) filename = str(fp)
open_fp = True open_fp = True
elif is_path(fp): elif isinstance(fp, (str, bytes)):
filename = fp filename = fp
open_fp = True open_fp = True
elif fp == sys.stdout: elif fp == sys.stdout:
@ -2398,7 +2411,7 @@ class Image:
pass pass
if not filename and hasattr(fp, "name") and is_path(fp.name): if not filename and hasattr(fp, "name") and is_path(fp.name):
# only set the name for metadata purposes # only set the name for metadata purposes
filename = fp.name filename = os.path.realpath(os.fspath(fp.name))
# may mutate self! # may mutate self!
self._ensure_mutable() self._ensure_mutable()
@ -2409,7 +2422,8 @@ class Image:
preinit() 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 not format:
if ext not in EXTENSION: if ext not in EXTENSION:
@ -2451,7 +2465,7 @@ class Image:
if open_fp: if open_fp:
fp.close() fp.close()
def seek(self, frame) -> Image: def seek(self, frame) -> None:
""" """
Seeks to the given frame in this sequence file. If you seek Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an beyond the end of the sequence, the method raises an
@ -2511,10 +2525,9 @@ class Image:
self.load() self.load()
if self.im.bands == 1: if self.im.bands == 1:
ims = [self.copy()] return (self.copy(),)
else: else:
ims = map(self._new, self.im.split()) return tuple(map(self._new, self.im.split()))
return tuple(ims)
def getchannel(self, channel): def getchannel(self, channel):
""" """
@ -2871,7 +2884,14 @@ class ImageTransformHandler:
(for use with :py:meth:`~PIL.Image.Image.transform`) (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 # pragma: no cover
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -3243,11 +3263,9 @@ def open(fp, mode="r", formats=None) -> Image:
raise TypeError(msg) raise TypeError(msg)
exclusive_fp = False exclusive_fp = False
filename = "" filename: str | bytes = ""
if isinstance(fp, Path): if is_path(fp):
filename = str(fp.resolve()) filename = os.path.realpath(os.fspath(fp))
elif is_path(fp):
filename = fp
if filename: if filename:
fp = builtins.open(filename, "rb") fp = builtins.open(filename, "rb")
@ -3421,7 +3439,11 @@ def merge(mode, bands):
# Plugin registry # 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 Register an image file plugin. This function should not be used
in application code. in application code.
@ -3631,7 +3653,13 @@ _apply_env_variables()
atexit.register(core.clear_cache) atexit.register(core.clear_cache)
class Exif(MutableMapping): if TYPE_CHECKING:
_ExifBase = MutableMapping[int, Any] # pragma: no cover
else:
_ExifBase = MutableMapping
class Exif(_ExifBase):
""" """
This class provides read and write access to EXIF image data:: This class provides read and write access to EXIF image data::

View File

@ -19,19 +19,26 @@ from __future__ import annotations
import sys import sys
from io import BytesIO from io import BytesIO
from typing import Callable
from . import Image from . import Image
from ._util import is_path from ._util import is_path
qt_version: str | None
qt_versions = [ qt_versions = [
["6", "PyQt6"], ["6", "PyQt6"],
["side6", "PySide6"], ["side6", "PySide6"],
] ]
# If a version has already been imported, attempt it first # 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) qt_versions.sort(key=lambda version: version[1] in sys.modules, reverse=True)
for qt_version, qt_module in qt_versions: for version, qt_module in qt_versions:
try: try:
QBuffer: type
QIODevice: type
QImage: type
QPixmap: type
qRgba: Callable[[int, int, int, int], int]
if qt_module == "PyQt6": if qt_module == "PyQt6":
from PyQt6.QtCore import QBuffer, QIODevice from PyQt6.QtCore import QBuffer, QIODevice
from PyQt6.QtGui import QImage, QPixmap, qRgba from PyQt6.QtGui import QImage, QPixmap, qRgba
@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions:
except (ImportError, RuntimeError): except (ImportError, RuntimeError):
continue continue
qt_is_installed = True qt_is_installed = True
qt_version = version
break break
else: else:
qt_is_installed = False qt_is_installed = False

View File

@ -8,6 +8,7 @@ import os
import re import re
import time import time
import zlib 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 # see 7.9.2.2 Text String Type on page 86 and D.3 PDFDocEncoding Character Set
@ -239,12 +240,18 @@ class PdfName:
return bytes(result) return bytes(result)
class PdfArray(list): class PdfArray(List[Any]):
def __bytes__(self): def __bytes__(self):
return b"[ " + b" ".join(pdf_repr(x) for x in self) + b" ]" 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] # pragma: no cover
else:
_DictBase = collections.UserDict
class PdfDict(_DictBase):
def __setattr__(self, key, value): def __setattr__(self, key, value):
if key == "data": if key == "data":
collections.UserDict.__setattr__(self, key, value) collections.UserDict.__setattr__(self, key, value)

View File

@ -25,6 +25,7 @@ import sys
from ._deprecate import deprecate from ._deprecate import deprecate
FFI: type
try: try:
from cffi import FFI from cffi import FFI

View File

@ -50,6 +50,7 @@ import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import TYPE_CHECKING, Any, Callable
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -306,6 +307,13 @@ _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
def _delegate(op):
def delegate(self, *args):
return getattr(self._val, op)(*args)
return delegate
class IFDRational(Rational): class IFDRational(Rational):
"""Implements a rational class where 0/0 is a legal value to match """Implements a rational class where 0/0 is a legal value to match
the in the wild use of exif rationals. the in the wild use of exif rationals.
@ -391,12 +399,6 @@ class IFDRational(Rational):
self._numerator = _numerator self._numerator = _numerator
self._denominator = _denominator self._denominator = _denominator
def _delegate(op):
def delegate(self, *args):
return getattr(self._val, op)(*args)
return delegate
""" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
'truediv', 'rtruediv', 'floordiv', 'rfloordiv', 'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
'mod','rmod', 'pow','rpow', 'pos', 'neg', 'mod','rmod', 'pow','rpow', 'pos', 'neg',
@ -436,7 +438,50 @@ class IFDRational(Rational):
__int__ = _delegate("__int__") __int__ = _delegate("__int__")
class ImageFileDirectory_v2(MutableMapping): def _register_loader(idx, size):
def decorator(func):
from .TiffTags import TYPES
if func.__name__.startswith("load_"):
TYPES[idx] = func.__name__[5:].replace("_", " ")
_load_dispatch[idx] = size, func # noqa: F821
return func
return decorator
def _register_writer(idx):
def decorator(func):
_write_dispatch[idx] = func # noqa: F821
return func
return decorator
def _register_basic(idx_fmt_name):
from .TiffTags import TYPES
idx, fmt, name = idx_fmt_name
TYPES[idx] = name
size = struct.calcsize("=" + fmt)
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
self._unpack(f"{len(data) // size}{fmt}", data)
),
)
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821
b"".join(self._pack(fmt, value) for value in values)
)
if TYPE_CHECKING:
_IFDv2Base = MutableMapping[int, Any] # pragma: no cover
else:
_IFDv2Base = MutableMapping
class ImageFileDirectory_v2(_IFDv2Base):
"""This class represents a TIFF tag directory. To speed things up, we """This class represents a TIFF tag directory. To speed things up, we
don't decode tags unless they're asked for. don't decode tags unless they're asked for.
@ -497,6 +542,9 @@ class ImageFileDirectory_v2(MutableMapping):
""" """
_load_dispatch: dict[int, Callable[[ImageFileDirectory_v2, bytes, bool], Any]] = {}
_write_dispatch: dict[int, Callable[..., Any]] = {}
def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None): def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
"""Initialize an ImageFileDirectory. """Initialize an ImageFileDirectory.
@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping):
prefix = property(lambda self: self._prefix) prefix = property(lambda self: self._prefix)
offset = property(lambda self: self._offset) offset = property(lambda self: self._offset)
legacy_api = property(lambda self: self._legacy_api)
@property
def legacy_api(self):
return self._legacy_api
@legacy_api.setter @legacy_api.setter
def legacy_api(self, value): def legacy_api(self, value):
@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping):
def _pack(self, fmt, *values): def _pack(self, fmt, *values):
return struct.pack(self._endian + fmt, *values) return struct.pack(self._endian + fmt, *values)
def _register_loader(idx, size):
def decorator(func):
from .TiffTags import TYPES
if func.__name__.startswith("load_"):
TYPES[idx] = func.__name__[5:].replace("_", " ")
_load_dispatch[idx] = size, func # noqa: F821
return func
return decorator
def _register_writer(idx):
def decorator(func):
_write_dispatch[idx] = func # noqa: F821
return func
return decorator
def _register_basic(idx_fmt_name):
from .TiffTags import TYPES
idx, fmt, name = idx_fmt_name
TYPES[idx] = name
size = struct.calcsize("=" + fmt)
_load_dispatch[idx] = ( # noqa: F821
size,
lambda self, data, legacy_api=True: (
self._unpack(f"{len(data) // size}{fmt}", data)
),
)
_write_dispatch[idx] = lambda self, *values: ( # noqa: F821
b"".join(self._pack(fmt, value) for value in values)
)
list( list(
map( map(
_register_basic, _register_basic,
@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
tagdata = property(lambda self: self._tagdata) tagdata = property(lambda self: self._tagdata)
# defined in ImageFileDirectory_v2 # defined in ImageFileDirectory_v2
tagtype: dict tagtype: dict[int, int]
"""Dictionary of tag types""" """Dictionary of tag types"""
@classmethod @classmethod
@ -1835,11 +1852,11 @@ def _save(im, fp, filename):
tags = list(atts.items()) tags = list(atts.items())
tags.sort() tags.sort()
a = (rawmode, compression, _fp, filename, tags, types) a = (rawmode, compression, _fp, filename, tags, types)
e = Image._getencoder(im.mode, "libtiff", a, encoderconfig) encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
e.setimage(im.im, (0, 0) + im.size) encoder.setimage(im.im, (0, 0) + im.size)
while True: while True:
# undone, change to self.decodermaxblock: # undone, change to self.decodermaxblock:
errcode, data = e.encode(16 * 1024)[1:] errcode, data = encoder.encode(16 * 1024)[1:]
if not _fp: if not _fp:
fp.write(data) fp.write(data)
if errcode: if errcode:

View File

@ -22,7 +22,7 @@ from collections import namedtuple
class TagInfo(namedtuple("_TagInfo", "value name type length enum")): class TagInfo(namedtuple("_TagInfo", "value name type length enum")):
__slots__ = [] __slots__: list[str] = []
def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None): def __new__(cls, value=None, name="unknown", type=None, length=None, enum=None):
return super().__new__(cls, value, name, type, length, enum or {}) return super().__new__(cls, value, name, type, length, enum or {})
@ -437,7 +437,7 @@ _populate()
## ##
# Map type numbers to type names -- defined in ImageFileDirectory. # Map type numbers to type names -- defined in ImageFileDirectory.
TYPES = {} TYPES: dict[int, str] = {}
# #
# These tags are handled by default in libtiff, without # These tags are handled by default in libtiff, without

5
src/PIL/_imaging.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -5,7 +5,8 @@ from __future__ import annotations
import sys import sys
import tkinter import tkinter
from tkinter import _tkinter as tk
tk = getattr(tkinter, "_tkinter")
try: try:
if hasattr(sys, "pypy_find_executable"): if hasattr(sys, "pypy_find_executable"):

5
src/PIL/_webp.pyi Normal file
View File

@ -0,0 +1,5 @@
from __future__ import annotations
from typing import Any
def __getattr__(name: str) -> Any: ...

View File

@ -33,9 +33,14 @@ commands =
[testenv:mypy] [testenv:mypy]
skip_install = true skip_install = true
deps = deps =
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython ipython
mypy==1.7.1 mypy==1.7.1
numpy numpy
packaging
types-cffi
types-defusedxml
extras = extras =
typing typing
commands = commands =