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

Added type hints to ImageMath #7707

Merged
merged 3 commits into from
Jan 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion pyproject.toml
Expand Up @@ -146,7 +146,6 @@ exclude = [
'^src/PIL/DdsImagePlugin.py$',
'^src/PIL/FpxImagePlugin.py$',
'^src/PIL/Image.py$',
'^src/PIL/ImageMath.py$',
'^src/PIL/ImageMorph.py$',
'^src/PIL/ImageQt.py$',
'^src/PIL/ImageShow.py$',
Expand Down
4 changes: 2 additions & 2 deletions src/PIL/Image.py
Expand Up @@ -873,7 +873,7 @@ def verify(self):

def convert(
self, mode=None, matrix=None, dither=None, palette=Palette.WEB, colors=256
):
) -> Image:
"""
Returns a converted copy of this image. For the "P" mode, this
method translates pixels through the palette. If mode is
Expand Down Expand Up @@ -1305,7 +1305,7 @@ def getbands(self):
"""
return ImageMode.getmode(self.mode).bands

def getbbox(self, *, alpha_only=True):
def getbbox(self, *, alpha_only=True) -> tuple[int, int, int, int]:
radarhere marked this conversation as resolved.
Show resolved Hide resolved
"""
Calculates the bounding box of the non-zero regions in the
image.
Expand Down
136 changes: 73 additions & 63 deletions src/PIL/ImageMath.py
Expand Up @@ -17,17 +17,18 @@
from __future__ import annotations

import builtins
from typing import Any

from . import Image, _imagingmath


class _Operand:
"""Wraps an image operand, providing standard operators"""

def __init__(self, im):
def __init__(self, im: Image.Image):
self.im = im

def __fixup(self, im1):
def __fixup(self, im1: _Operand | float) -> Image.Image:
# convert image to suitable mode
if isinstance(im1, _Operand):
# argument was an image.
Expand All @@ -45,122 +46,131 @@
else:
return Image.new("F", self.im.size, im1)

def apply(self, op, im1, im2=None, mode=None):
im1 = self.__fixup(im1)
def apply(
self,
op: str,
im1: _Operand | float,
im2: _Operand | float | None = None,
mode: str | None = None,
) -> _Operand:
im_1 = self.__fixup(im1)
if im2 is None:
# unary operation
out = Image.new(mode or im1.mode, im1.size, None)
im1.load()
out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
try:
op = getattr(_imagingmath, op + "_" + im1.mode)
op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
_imagingmath.unop(op, out.im.id, im1.im.id)
_imagingmath.unop(op, out.im.id, im_1.im.id)
else:
# binary operation
im2 = self.__fixup(im2)
if im1.mode != im2.mode:
im_2 = self.__fixup(im2)
if im_1.mode != im_2.mode:
# convert both arguments to floating point
if im1.mode != "F":
im1 = im1.convert("F")
if im2.mode != "F":
im2 = im2.convert("F")
if im1.size != im2.size:
if im_1.mode != "F":
im_1 = im_1.convert("F")

Check warning on line 73 in src/PIL/ImageMath.py

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L73

Added line #L73 was not covered by tests
if im_2.mode != "F":
im_2 = im_2.convert("F")
if im_1.size != im_2.size:
# crop both arguments to a common size
size = (min(im1.size[0], im2.size[0]), min(im1.size[1], im2.size[1]))
if im1.size != size:
im1 = im1.crop((0, 0) + size)
if im2.size != size:
im2 = im2.crop((0, 0) + size)
out = Image.new(mode or im1.mode, im1.size, None)
im1.load()
im2.load()
size = (
min(im_1.size[0], im_2.size[0]),
min(im_1.size[1], im_2.size[1]),
)
if im_1.size != size:
im_1 = im_1.crop((0, 0) + size)
if im_2.size != size:
im_2 = im_2.crop((0, 0) + size)
out = Image.new(mode or im_1.mode, im_1.size, None)
im_1.load()
im_2.load()
try:
op = getattr(_imagingmath, op + "_" + im1.mode)
op = getattr(_imagingmath, op + "_" + im_1.mode)
except AttributeError as e:
msg = f"bad operand type for '{op}'"
raise TypeError(msg) from e
_imagingmath.binop(op, out.im.id, im1.im.id, im2.im.id)
_imagingmath.binop(op, out.im.id, im_1.im.id, im_2.im.id)
return _Operand(out)

# unary operators
def __bool__(self):
def __bool__(self) -> bool:
# an image is "true" if it contains at least one non-zero pixel
return self.im.getbbox() is not None

def __abs__(self):
def __abs__(self) -> _Operand:
return self.apply("abs", self)

def __pos__(self):
def __pos__(self) -> _Operand:
return self

def __neg__(self):
def __neg__(self) -> _Operand:
return self.apply("neg", self)

# binary operators
def __add__(self, other):
def __add__(self, other: _Operand | float) -> _Operand:
return self.apply("add", self, other)

def __radd__(self, other):
def __radd__(self, other: _Operand | float) -> _Operand:
return self.apply("add", other, self)

def __sub__(self, other):
def __sub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", self, other)

def __rsub__(self, other):
def __rsub__(self, other: _Operand | float) -> _Operand:
return self.apply("sub", other, self)

def __mul__(self, other):
def __mul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", self, other)

def __rmul__(self, other):
def __rmul__(self, other: _Operand | float) -> _Operand:
return self.apply("mul", other, self)

def __truediv__(self, other):
def __truediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", self, other)

def __rtruediv__(self, other):
def __rtruediv__(self, other: _Operand | float) -> _Operand:
return self.apply("div", other, self)

def __mod__(self, other):
def __mod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", self, other)

def __rmod__(self, other):
def __rmod__(self, other: _Operand | float) -> _Operand:
return self.apply("mod", other, self)

def __pow__(self, other):
def __pow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", self, other)

def __rpow__(self, other):
def __rpow__(self, other: _Operand | float) -> _Operand:
return self.apply("pow", other, self)

# bitwise
def __invert__(self):
def __invert__(self) -> _Operand:
return self.apply("invert", self)

def __and__(self, other):
def __and__(self, other: _Operand | float) -> _Operand:
return self.apply("and", self, other)

def __rand__(self, other):
def __rand__(self, other: _Operand | float) -> _Operand:
return self.apply("and", other, self)

def __or__(self, other):
def __or__(self, other: _Operand | float) -> _Operand:
return self.apply("or", self, other)

def __ror__(self, other):
def __ror__(self, other: _Operand | float) -> _Operand:
return self.apply("or", other, self)

def __xor__(self, other):
def __xor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", self, other)

def __rxor__(self, other):
def __rxor__(self, other: _Operand | float) -> _Operand:
return self.apply("xor", other, self)

def __lshift__(self, other):
def __lshift__(self, other: _Operand | float) -> _Operand:
return self.apply("lshift", self, other)

def __rshift__(self, other):
def __rshift__(self, other: _Operand | float) -> _Operand:
return self.apply("rshift", self, other)

# logical
Expand All @@ -170,46 +180,46 @@
def __ne__(self, other):
return self.apply("ne", self, other)

def __lt__(self, other):
def __lt__(self, other: _Operand | float) -> _Operand:
return self.apply("lt", self, other)

def __le__(self, other):
def __le__(self, other: _Operand | float) -> _Operand:
return self.apply("le", self, other)

def __gt__(self, other):
def __gt__(self, other: _Operand | float) -> _Operand:
return self.apply("gt", self, other)

def __ge__(self, other):
def __ge__(self, other: _Operand | float) -> _Operand:
return self.apply("ge", self, other)


# conversions
def imagemath_int(self):
def imagemath_int(self: _Operand) -> _Operand:
return _Operand(self.im.convert("I"))


def imagemath_float(self):
def imagemath_float(self: _Operand) -> _Operand:
return _Operand(self.im.convert("F"))


# logical
def imagemath_equal(self, other):
def imagemath_equal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("eq", self, other, mode="I")


def imagemath_notequal(self, other):
def imagemath_notequal(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("ne", self, other, mode="I")


def imagemath_min(self, other):
def imagemath_min(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("min", self, other)


def imagemath_max(self, other):
def imagemath_max(self: _Operand, other: _Operand | float | None) -> _Operand:
return self.apply("max", self, other)


def imagemath_convert(self, mode):
def imagemath_convert(self: _Operand, mode: str) -> _Operand:
return _Operand(self.im.convert(mode))


Expand All @@ -219,7 +229,7 @@
ops[k[10:]] = v


def eval(expression, _dict={}, **kw):
def eval(expression: str, _dict: dict[str, Any] = {}, **kw: Any) -> Any:
"""
Evaluates an image expression.

Expand Down Expand Up @@ -247,7 +257,7 @@

compiled_code = compile(expression, "<string>", "eval")

def scan(code):
def scan(code) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What type is code?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've pushed a commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code's type is CodeType :D

for const in code.co_consts:
if type(const) is type(compiled_code):
scan(const)
Expand Down
5 changes: 5 additions & 0 deletions src/PIL/_imagingmath.pyi
@@ -0,0 +1,5 @@
from __future__ import annotations

from typing import Any

def __getattr__(name: str) -> Any: ...