Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints to Image.py #7924

Merged
merged 3 commits into from
Apr 1, 2024
Merged
Changes from 2 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
102 changes: 56 additions & 46 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
_plugins,
)
from ._binary import i32le, o32be, o32le
from ._typing import TypeGuard
from ._util import DeferredError, is_path

ElementTree: ModuleType | None
Expand Down Expand Up @@ -120,7 +121,7 @@ class DecompressionBombError(Exception):
cffi = None


def isImageType(t):
def isImageType(t: Any) -> TypeGuard[Image]:
"""
Checks if an object is an image object.

Expand Down Expand Up @@ -267,7 +268,7 @@ def getmodebase(mode: str) -> str:
return ImageMode.getmode(mode).basemode


def getmodetype(mode):
def getmodetype(mode: str) -> str:
"""
Gets the storage type mode. Given a mode, this function returns a
single-layer mode suitable for storing individual bands.
Expand All @@ -279,7 +280,7 @@ def getmodetype(mode):
return ImageMode.getmode(mode).basetype


def getmodebandnames(mode):
def getmodebandnames(mode: str) -> tuple[str, ...]:
"""
Gets a list of individual band names. Given a mode, this function returns
a tuple containing the names of individual bands (use
Expand Down Expand Up @@ -311,7 +312,7 @@ def getmodebands(mode: str) -> int:
_initialized = 0


def preinit():
def preinit() -> None:
"""
Explicitly loads BMP, GIF, JPEG, PPM and PPM file format drivers.

Expand Down Expand Up @@ -437,7 +438,7 @@ def _getencoder(mode, encoder_name, args, extra=()):


class _E:
def __init__(self, scale, offset):
def __init__(self, scale, offset) -> None:
self.scale = scale
self.offset = offset

Expand Down Expand Up @@ -508,22 +509,22 @@ def __init__(self):
self._exif = None

@property
def width(self):
def width(self) -> int:
return self.size[0]

@property
def height(self):
def height(self) -> int:
return self.size[1]

@property
def size(self):
def size(self) -> tuple[int, int]:
return self._size

@property
def mode(self):
return self._mode

def _new(self, im):
def _new(self, im: Image):
hugovk marked this conversation as resolved.
Show resolved Hide resolved
new = Image()
new.im = im
new._mode = im.mode
Expand Down Expand Up @@ -556,7 +557,7 @@ def __exit__(self, *args):
self._close_fp()
self.fp = None

def close(self):
def close(self) -> None:
"""
Closes the file pointer, if possible.

Expand Down Expand Up @@ -589,7 +590,7 @@ def _copy(self) -> None:
self.pyaccess = None
self.readonly = 0

def _ensure_mutable(self):
def _ensure_mutable(self) -> None:
if self.readonly:
self._copy()
else:
Expand Down Expand Up @@ -629,7 +630,7 @@ def __eq__(self, other):
and self.tobytes() == other.tobytes()
)

def __repr__(self):
def __repr__(self) -> str:
return "<%s.%s image mode=%s size=%dx%d at 0x%X>" % (
self.__class__.__module__,
self.__class__.__name__,
Expand All @@ -639,7 +640,7 @@ def __repr__(self):
id(self),
)

def _repr_pretty_(self, p, cycle):
def _repr_pretty_(self, p, cycle) -> None:
"""IPython plain text display support"""

# Same as __repr__ but without unpredictable id(self),
Expand Down Expand Up @@ -711,7 +712,7 @@ def __getstate__(self):
im_data = self.tobytes() # load image first
return [self.info, self.mode, self.size, self.getpalette(), im_data]

def __setstate__(self, state):
def __setstate__(self, state) -> None:
Image.__init__(self)
info, mode, size, palette, data = state
self.info = info
Expand Down Expand Up @@ -774,7 +775,7 @@ def tobytes(self, encoder_name: str = "raw", *args) -> bytes:

return b"".join(output)

def tobitmap(self, name="image"):
def tobitmap(self, name: str = "image") -> bytes:
"""
Returns the image converted to an X11 bitmap.

Expand Down Expand Up @@ -886,7 +887,12 @@ def verify(self):
pass

def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
self,
mode: str | None = None,
matrix: tuple[float, ...] | None = None,
dither: Dither | None = None,
palette: Palette = Palette.WEB,
colors: int = 256,
) -> Image:
"""
Returns a converted copy of this image. For the "P" mode, this
Expand Down Expand Up @@ -1117,12 +1123,12 @@ def convert_transparency(m, v):

def quantize(
self,
colors=256,
method=None,
kmeans=0,
colors: int = 256,
method: Quantize | None = None,
kmeans: int = 0,
palette=None,
dither=Dither.FLOYDSTEINBERG,
):
dither: Dither = Dither.FLOYDSTEINBERG,
) -> Image:
"""
Convert the image to 'P' mode with the specified number
of colors.
Expand Down Expand Up @@ -1210,7 +1216,7 @@ def copy(self) -> Image:

__copy__ = copy

def crop(self, box=None) -> Image:
def crop(self, box: tuple[int, int, int, int] | None = None) -> Image:
"""
Returns a rectangular region from this image. The box is a
4-tuple defining the left, upper, right, and lower pixel
Expand Down Expand Up @@ -1341,7 +1347,9 @@ def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]:
self.load()
return self.im.getbbox(alpha_only)

def getcolors(self, maxcolors=256):
def getcolors(
self, maxcolors: int = 256
) -> list[tuple[int, int | tuple[int, ...]]] | None:
hugovk marked this conversation as resolved.
Show resolved Hide resolved
"""
Returns a list of colors used in this image.

Expand All @@ -1364,7 +1372,7 @@ def getcolors(self, maxcolors=256):
return out
return self.im.getcolors(maxcolors)

def getdata(self, band=None):
def getdata(self, band: int | None = None):
"""
Returns the contents of this image as a sequence object
containing pixel values. The sequence object is flattened, so
Expand All @@ -1387,7 +1395,7 @@ def getdata(self, band=None):
return self.im.getband(band)
return self.im # could be abused

def getextrema(self):
def getextrema(self) -> tuple[float, float] | tuple[tuple[int, int], ...]:
"""
Gets the minimum and maximum pixel values for each band in
the image.
Expand Down Expand Up @@ -1468,7 +1476,7 @@ def getexif(self) -> Exif:

return self._exif

def _reload_exif(self):
def _reload_exif(self) -> None:
if self._exif is None or not self._exif._loaded:
return
self._exif._loaded = False
Expand Down Expand Up @@ -1605,7 +1613,7 @@ def getpixel(self, xy):
return self.pyaccess.getpixel(xy)
return self.im.getpixel(tuple(xy))

def getprojection(self):
def getprojection(self) -> tuple[list[int], list[int]]:
"""
Get projection to x and y axes

Expand All @@ -1617,7 +1625,7 @@ def getprojection(self):
x, y = self.im.getprojection()
return list(x), list(y)

def histogram(self, mask=None, extrema=None) -> list[int]:
def histogram(self, mask: Image | None = None, extrema=None) -> list[int]:
"""
Returns a histogram for the image. The histogram is returned as a
list of pixel counts, one for each pixel value in the source
Expand Down Expand Up @@ -2463,7 +2471,7 @@ def save(self, fp, format=None, **params) -> None:
if open_fp:
fp.close()

def seek(self, frame) -> None:
def seek(self, frame: int) -> None:
"""
Seeks to the given frame in this sequence file. If you seek
beyond the end of the sequence, the method raises an
Expand All @@ -2485,7 +2493,7 @@ def seek(self, frame) -> None:
msg = "no more images in file"
raise EOFError(msg)

def show(self, title=None):
def show(self, title: str | None = None) -> None:
"""
Displays this image. This method is mainly intended for debugging purposes.

Expand Down Expand Up @@ -2526,7 +2534,7 @@ def split(self) -> tuple[Image, ...]:
return (self.copy(),)
return tuple(map(self._new, self.im.split()))

def getchannel(self, channel):
def getchannel(self, channel: int | str) -> Image:
"""
Returns an image containing a single channel of the source image.

Expand Down Expand Up @@ -2601,13 +2609,13 @@ def thumbnail(self, size, resample=Resampling.BICUBIC, reducing_gap=2.0):

provided_size = tuple(map(math.floor, size))

def preserve_aspect_ratio():
def preserve_aspect_ratio() -> tuple[int, int] | None:
def round_aspect(number, key):
return max(min(math.floor(number), math.ceil(number), key=key), 1)

x, y = provided_size
if x >= self.width and y >= self.height:
return
return None

aspect = self.width / self.height
if x / y >= aspect:
Expand Down Expand Up @@ -2927,7 +2935,9 @@ def _check_size(size):
return True


def new(mode, size, color=0) -> Image:
def new(
mode: str, size: tuple[int, int], color: float | tuple[float, ...] | str | None = 0
) -> Image:
"""
Creates a new image with the given mode and size.

Expand Down Expand Up @@ -3193,7 +3203,7 @@ def fromqpixmap(im):
}


def _decompression_bomb_check(size):
def _decompression_bomb_check(size: tuple[int, int]) -> None:
if MAX_IMAGE_PIXELS is None:
return

Expand Down Expand Up @@ -3335,7 +3345,7 @@ def _open_core(fp, filename, prefix, formats):
# Image processing.


def alpha_composite(im1, im2):
def alpha_composite(im1: Image, im2: Image) -> Image:
"""
Alpha composite im2 over im1.

Expand All @@ -3350,7 +3360,7 @@ def alpha_composite(im1, im2):
return im1._new(core.alpha_composite(im1.im, im2.im))


def blend(im1, im2, alpha):
def blend(im1: Image, im2: Image, alpha: float) -> Image:
"""
Creates a new image by interpolating between two input images, using
a constant alpha::
Expand All @@ -3373,7 +3383,7 @@ def blend(im1, im2, alpha):
return im1._new(core.blend(im1.im, im2.im, alpha))


def composite(image1, image2, mask):
def composite(image1: Image, image2: Image, mask: Image) -> Image:
"""
Create composite image by blending images using a transparency mask.

Expand Down Expand Up @@ -3483,7 +3493,7 @@ def register_save(id: str, driver) -> None:
SAVE[id.upper()] = driver


def register_save_all(id, driver):
def register_save_all(id, driver) -> None:
"""
Registers an image function to save all the frames
of a multiframe format. This function should not be
Expand Down Expand Up @@ -3557,7 +3567,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
# Simple display support.


def _show(image, **options):
def _show(image, **options) -> None:
from . import ImageShow

ImageShow.show(image, **options)
Expand Down Expand Up @@ -3613,7 +3623,7 @@ def radial_gradient(mode):
# Resources


def _apply_env_variables(env=None):
def _apply_env_variables(env=None) -> None:
if env is None:
env = os.environ

Expand Down Expand Up @@ -3928,21 +3938,21 @@ def get_ifd(self, tag):
}
return ifd

def hide_offsets(self):
def hide_offsets(self) -> None:
for tag in (ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo):
if tag in self:
self._hidden_data[tag] = self[tag]
del self[tag]

def __str__(self):
def __str__(self) -> str:
if self._info is not None:
# Load all keys into self._data
for tag in self._info:
self[tag]

return str(self._data)

def __len__(self):
def __len__(self) -> int:
keys = set(self._data)
if self._info is not None:
keys.update(self._info)
Expand All @@ -3954,10 +3964,10 @@ def __getitem__(self, tag):
del self._info[tag]
return self._data[tag]

def __contains__(self, tag):
def __contains__(self, tag) -> bool:
return tag in self._data or (self._info is not None and tag in self._info)

def __setitem__(self, tag, value):
def __setitem__(self, tag, value) -> None:
if self._info is not None and tag in self._info:
del self._info[tag]
self._data[tag] = value
Expand Down
Loading