Skip to content

Commit

Permalink
Keep existing behaviour for eval()
Browse files Browse the repository at this point in the history
  • Loading branch information
radarhere committed Oct 12, 2024
1 parent 828fa50 commit aa30dc5
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 20 deletions.
8 changes: 5 additions & 3 deletions docs/releasenotes/11.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ which reached end-of-life in October 2024.
ImageMath comparison operations
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Comparison operations in :py:mod:`~PIL.ImageMath` will now return booleans
instead of images. ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``equal()`` and
``notequal()`` are all affected::
In :py:meth:`~PIL.ImageMath.lambda_eval` and :py:meth:`~PIL.ImageMath.unsafe_eval`,
comparison operations will now return booleans instead of images. ``==``, ``!=``,
``<``, ``<=``, ``>``, ``>=``, ``equal()`` and ``notequal()`` are all affected::

from PIL import Image, ImageMath
A = Image.new("L", (1, 1))
Expand All @@ -24,6 +24,8 @@ instead of images. ``==``, ``!=``, ``<``, ``<=``, ``>``, ``>=``, ``equal()`` and
# It will now return True
ImageMath.lambda_eval(lambda args: args["A"] == args["A"], A=A))

The deprecated ``ImageMath.eval()`` remains unchanged.

Python 3.12 on macOS <= 10.12
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
120 changes: 103 additions & 17 deletions src/PIL/ImageMath.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,30 @@ def lambda_eval(
return out


def _unsafe_eval(
expression: str,
args: dict[str, Any],
) -> Any:
compiled_code = compile(expression, "<string>", "eval")

def scan(code: CodeType) -> None:
for const in code.co_consts:
if type(const) is type(compiled_code):
scan(const)

for name in code.co_names:
if name not in args and name != "abs":
msg = f"'{name}' not allowed"
raise ValueError(msg)

scan(compiled_code)
out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
try:
return out.im
except AttributeError:
return out


def unsafe_eval(
expression: str,
options: dict[str, Any] = {},
Expand Down Expand Up @@ -323,24 +347,64 @@ def unsafe_eval(
if isinstance(v, Image.Image):
args[k] = _Operand(v)

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

def scan(code: CodeType) -> None:
for const in code.co_consts:
if type(const) is type(compiled_code):
scan(const)

for name in code.co_names:
if name not in args and name != "abs":
msg = f"'{name}' not allowed"
raise ValueError(msg)
class _Operand_Eval(_Operand):
def apply(
self,
op: str,
im1: _Operand | float,
im2: _Operand | float | None = None,
mode: str | None = None,
) -> _Operand:
operand = super().apply(op, im1, im2, mode)
return _Operand_Eval(operand.im)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L361-L362

Added lines #L361 - L362 were not covered by tests

scan(compiled_code)
out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
try:
return out.im
except AttributeError:
return out
def __eq__(self, other: _Operand_Eval | float | None) -> _Operand: # type: ignore[override]
return self.apply("eq", self, other)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L365

Added line #L365 was not covered by tests

def __ne__(self, other: _Operand_Eval | float | None) -> _Operand: # type: ignore[override]
return self.apply("ne", self, other)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L368

Added line #L368 was not covered by tests

def __lt__(self, other: _Operand_Eval | float) -> _Operand: # type: ignore[override]
return self.apply("lt", self, other)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L371

Added line #L371 was not covered by tests

def __le__(self, other: _Operand_Eval | float) -> _Operand: # type: ignore[override]
return self.apply("le", self, other)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L374

Added line #L374 was not covered by tests

def __gt__(self, other: _Operand_Eval | float) -> _Operand: # type: ignore[override]
return self.apply("gt", self, other)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L377

Added line #L377 was not covered by tests

def __ge__(self, other: _Operand_Eval | float) -> _Operand: # type: ignore[override]
return self.apply("ge", self, other)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L380

Added line #L380 was not covered by tests


def imagemath_int_eval(self: _Operand) -> _Operand:
operand = _Operand(self.im.convert("I"))
return _Operand_Eval(operand.im)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L384-L385

Added lines #L384 - L385 were not covered by tests


def imagemath_float_eval(self: _Operand) -> _Operand:
operand = _Operand(self.im.convert("F"))
return _Operand_Eval(operand.im)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L389-L390

Added lines #L389 - L390 were not covered by tests


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

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L396

Added line #L396 was not covered by tests


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

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L402

Added line #L402 was not covered by tests


def imagemath_convert_eval(self: _Operand, mode: str) -> _Operand_Eval:
operand = _Operand(self.im.convert(mode))
return _Operand_Eval(operand.im)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L406-L407

Added lines #L406 - L407 were not covered by tests


def eval(
Expand All @@ -358,7 +422,7 @@ def eval(
can either use a dictionary, or one or more keyword
arguments.
:return: The evaluated expression. This is usually an image object, but can
also be an integer, a floating point value, a boolean, or a pixel tuple,
also be an integer, a floating point value, or a pixel tuple,
depending on the expression.
.. deprecated:: 10.3.0
Expand All @@ -369,4 +433,26 @@ def eval(
12,
"ImageMath.lambda_eval or ImageMath.unsafe_eval",
)
return unsafe_eval(expression, _dict, **kw)

# build execution namespace
args: dict[str, Any] = {
"int": imagemath_int_eval,
"float": imagemath_float_eval,
"equal": imagemath_equal_eval,
"notequal": imagemath_notequal_eval,
"min": imagemath_min,
"max": imagemath_max,
"convert": imagemath_convert_eval,
}
for k in list(_dict.keys()) + list(kw.keys()):
if "__" in k or hasattr(builtins, k):
msg = f"'{k}' not allowed"
raise ValueError(msg)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L448-L450

Added lines #L448 - L450 were not covered by tests

args.update(_dict)
args.update(kw)
for k, v in args.items():
if isinstance(v, Image.Image):
args[k] = _Operand_Eval(v)

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

View check run for this annotation

Codecov / codecov/patch

src/PIL/ImageMath.py#L456

Added line #L456 was not covered by tests

return _unsafe_eval(expression, args)

0 comments on commit aa30dc5

Please sign in to comment.