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_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$',
]

View File

@ -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

View File

@ -93,8 +93,8 @@ for i in ["16", "16L", "16B"]:
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}")
# --------------------------------------------------------------------

View File

@ -26,6 +26,7 @@
from __future__ import annotations
import abc
import atexit
import builtins
import io
@ -40,11 +41,8 @@ import warnings
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.
@ -60,6 +58,12 @@ from . import (
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__)
@ -110,6 +114,7 @@ except ImportError as v:
USE_CFFI_ACCESS = False
cffi: ModuleType | None
try:
import cffi
except ImportError:
@ -211,14 +216,22 @@ if hasattr(core, "DEFAULT_STRATEGY"):
# --------------------------------------------------------------------
# Registries
ID = []
OPEN = {}
MIME = {}
SAVE = {}
SAVE_ALL = {}
EXTENSION = {}
DECODERS = {}
ENCODERS = {}
if TYPE_CHECKING:
from . import ImageFile # pragma: no cover
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
@ -2383,12 +2396,12 @@ class Image:
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:
@ -2398,7 +2411,7 @@ class Image:
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()
@ -2409,7 +2422,8 @@ class Image:
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:
@ -2451,7 +2465,7 @@ class Image:
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
@ -2511,10 +2525,9 @@ class Image:
self.load()
if self.im.bands == 1:
ims = [self.copy()]
return (self.copy(),)
else:
ims = map(self._new, self.im.split())
return tuple(ims)
return tuple(map(self._new, self.im.split()))
def getchannel(self, channel):
"""
@ -2871,7 +2884,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 # pragma: no cover
# --------------------------------------------------------------------
@ -3243,11 +3263,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")
@ -3421,7 +3439,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.
@ -3631,7 +3653,13 @@ _apply_env_variables()
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::

View File

@ -19,19 +19,26 @@ from __future__ import annotations
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
@ -41,6 +48,7 @@ for qt_version, qt_module in qt_versions:
except (ImportError, RuntimeError):
continue
qt_is_installed = True
qt_version = version
break
else:
qt_is_installed = False

View File

@ -8,6 +8,7 @@ import os
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
@ -239,12 +240,18 @@ class PdfName:
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] # pragma: no cover
else:
_DictBase = collections.UserDict
class PdfDict(_DictBase):
def __setattr__(self, key, value):
if key == "data":
collections.UserDict.__setattr__(self, key, value)

View File

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

View File

@ -50,6 +50,7 @@ import warnings
from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational
from typing import TYPE_CHECKING, Any, Callable
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
@ -306,6 +307,13 @@ _load_dispatch = {}
_write_dispatch = {}
def _delegate(op):
def delegate(self, *args):
return getattr(self._val, op)(*args)
return delegate
class IFDRational(Rational):
"""Implements a rational class where 0/0 is a legal value to match
the in the wild use of exif rationals.
@ -391,12 +399,6 @@ class IFDRational(Rational):
self._numerator = _numerator
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',
'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
'mod','rmod', 'pow','rpow', 'pos', 'neg',
@ -436,7 +438,50 @@ class IFDRational(Rational):
__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
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):
"""Initialize an ImageFileDirectory.
@ -531,7 +579,10 @@ class ImageFileDirectory_v2(MutableMapping):
prefix = property(lambda self: self._prefix)
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
def legacy_api(self, value):
@ -674,40 +725,6 @@ class ImageFileDirectory_v2(MutableMapping):
def _pack(self, 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(
map(
_register_basic,
@ -995,7 +1012,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
tagdata = property(lambda self: self._tagdata)
# defined in ImageFileDirectory_v2
tagtype: dict
tagtype: dict[int, int]
"""Dictionary of tag types"""
@classmethod
@ -1835,11 +1852,11 @@ def _save(im, fp, filename):
tags = list(atts.items())
tags.sort()
a = (rawmode, compression, _fp, filename, tags, types)
e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
e.setimage(im.im, (0, 0) + im.size)
encoder = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
encoder.setimage(im.im, (0, 0) + im.size)
while True:
# undone, change to self.decodermaxblock:
errcode, data = e.encode(16 * 1024)[1:]
errcode, data = encoder.encode(16 * 1024)[1:]
if not _fp:
fp.write(data)
if errcode:

View File

@ -22,7 +22,7 @@ from collections import namedtuple
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):
return super().__new__(cls, value, name, type, length, enum or {})
@ -437,7 +437,7 @@ _populate()
##
# Map type numbers to type names -- defined in ImageFileDirectory.
TYPES = {}
TYPES: dict[int, str] = {}
#
# 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 tkinter
from tkinter import _tkinter as tk
tk = getattr(tkinter, "_tkinter")
try:
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]
skip_install = true
deps =
IceSpringPySideStubs-PyQt6
IceSpringPySideStubs-PySide6
ipython
mypy==1.7.1
numpy
packaging
types-cffi
types-defusedxml
extras =
typing
commands =