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

Deprecate ImageMath lambda_eval and unsafe_eval options argument #8242

Merged
merged 2 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
65 changes: 38 additions & 27 deletions Tests/test_imagemath_lambda_eval.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from __future__ import annotations

from typing import Any

import pytest

from PIL import Image, ImageMath


Expand All @@ -19,7 +23,7 @@ def pixel(im: Image.Image | int) -> str | int:
A2 = A.resize((2, 2))
B2 = B.resize((2, 2))

images = {"A": A, "B": B, "F": F, "I": I}
images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}
homm marked this conversation as resolved.
Show resolved Hide resolved


def test_sanity() -> None:
Expand All @@ -30,104 +34,111 @@ def test_sanity() -> None:
== "I 3"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images
lambda args: args["float"](args["A"]) + args["B"], **images
)
)
== "F 3.0"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["int"](args["float"](args["A"]) + args["B"]), images
lambda args: args["int"](args["float"](args["A"]) + args["B"]), **images
)
)
== "I 3"
)


def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.lambda_eval(lambda args: 1, images) == 1


def test_ops() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, images)) == "I -1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] * -1, **images)) == "I -1"

assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] + args["B"], **images))
== "I 3"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] - args["B"], **images))
== "I -1"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] * args["B"], **images))
== "I 2"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] / args["B"], **images))
== "I 0"
)
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, images)) == "I 4"
assert pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 2, **images)) == "I 4"
assert (
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, images))
pixel(ImageMath.lambda_eval(lambda args: args["B"] ** 33, **images))
== "I 2147483647"
)

assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) + args["B"], images
lambda args: args["float"](args["A"]) + args["B"], **images
)
)
== "F 3.0"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) - args["B"], images
lambda args: args["float"](args["A"]) - args["B"], **images
)
)
== "F -1.0"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) * args["B"], images
lambda args: args["float"](args["A"]) * args["B"], **images
)
)
== "F 2.0"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["float"](args["A"]) / args["B"], images
lambda args: args["float"](args["A"]) / args["B"], **images
)
)
== "F 0.5"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, images))
pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 2, **images)
)
== "F 4.0"
)
assert (
pixel(
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, images)
ImageMath.lambda_eval(lambda args: args["float"](args["B"]) ** 33, **images)
)
== "F 8589934592.0"
)


def test_logical() -> None:
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], images)) == 0
assert pixel(ImageMath.lambda_eval(lambda args: not args["A"], **images)) == 0
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] and args["B"], **images))
== "L 2"
)
assert (
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], images))
pixel(ImageMath.lambda_eval(lambda args: args["A"] or args["B"], **images))
== "L 1"
)

Expand All @@ -136,23 +147,23 @@ def test_convert() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "L"), images
lambda args: args["convert"](args["A"] + args["B"], "L"), **images
)
)
== "L 3"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "1"), images
lambda args: args["convert"](args["A"] + args["B"], "1"), **images
)
)
== "1 0"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["convert"](args["A"] + args["B"], "RGB"), images
lambda args: args["convert"](args["A"] + args["B"], "RGB"), **images
)
)
== "RGB (3, 3, 3)"
Expand All @@ -163,21 +174,21 @@ def test_compare() -> None:
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["min"](args["A"], args["B"]), images
lambda args: args["min"](args["A"], args["B"]), **images
)
)
== "I 1"
)
assert (
pixel(
ImageMath.lambda_eval(
lambda args: args["max"](args["A"], args["B"]), images
lambda args: args["max"](args["A"], args["B"]), **images
)
)
== "I 2"
)
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, images)) == "I 0"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 1, **images)) == "I 1"
assert pixel(ImageMath.lambda_eval(lambda args: args["A"] == 2, **images)) == "I 0"


def test_one_image_larger() -> None:
Expand Down
67 changes: 37 additions & 30 deletions Tests/test_imagemath_unsafe_eval.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

from typing import Any

import pytest

from PIL import Image, ImageMath
Expand All @@ -21,40 +23,45 @@ def pixel(im: Image.Image | int) -> str | int:
A2 = A.resize((2, 2))
B2 = B.resize((2, 2))

images = {"A": A, "B": B, "F": F, "I": I}
images: dict[str, Any] = {"A": A, "B": B, "F": F, "I": I}


def test_sanity() -> None:
assert ImageMath.unsafe_eval("1") == 1
assert ImageMath.unsafe_eval("1+A", A=2) == 3
assert pixel(ImageMath.unsafe_eval("A+B", A=A, B=B)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("int(float(A)+B)", **images)) == "I 3"


def test_eval_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.eval("1") == 1


def test_options_deprecated() -> None:
with pytest.warns(DeprecationWarning):
assert ImageMath.unsafe_eval("1", images) == 1


def test_ops() -> None:
assert pixel(ImageMath.unsafe_eval("-A", images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("-A", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("+B", **images)) == "L 2"

assert pixel(ImageMath.unsafe_eval("A+B", images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A-B", images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("A*B", images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A/B", images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("B**2", images)) == "I 4"
assert pixel(ImageMath.unsafe_eval("B**33", images)) == "I 2147483647"
assert pixel(ImageMath.unsafe_eval("A+B", **images)) == "I 3"
assert pixel(ImageMath.unsafe_eval("A-B", **images)) == "I -1"
assert pixel(ImageMath.unsafe_eval("A*B", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A/B", **images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("B**2", **images)) == "I 4"
assert pixel(ImageMath.unsafe_eval("B**33", **images)) == "I 2147483647"

assert pixel(ImageMath.unsafe_eval("float(A)+B", images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("float(A)-B", images)) == "F -1.0"
assert pixel(ImageMath.unsafe_eval("float(A)*B", images)) == "F 2.0"
assert pixel(ImageMath.unsafe_eval("float(A)/B", images)) == "F 0.5"
assert pixel(ImageMath.unsafe_eval("float(B)**2", images)) == "F 4.0"
assert pixel(ImageMath.unsafe_eval("float(B)**33", images)) == "F 8589934592.0"
assert pixel(ImageMath.unsafe_eval("float(A)+B", **images)) == "F 3.0"
assert pixel(ImageMath.unsafe_eval("float(A)-B", **images)) == "F -1.0"
assert pixel(ImageMath.unsafe_eval("float(A)*B", **images)) == "F 2.0"
assert pixel(ImageMath.unsafe_eval("float(A)/B", **images)) == "F 0.5"
assert pixel(ImageMath.unsafe_eval("float(B)**2", **images)) == "F 4.0"
assert pixel(ImageMath.unsafe_eval("float(B)**33", **images)) == "F 8589934592.0"


@pytest.mark.parametrize(
Expand All @@ -72,33 +79,33 @@ def test_prevent_exec(expression: str) -> None:

def test_prevent_double_underscores() -> None:
with pytest.raises(ValueError):
ImageMath.unsafe_eval("1", {"__": None})
ImageMath.unsafe_eval("1", __=None)


def test_prevent_builtins() -> None:
with pytest.raises(ValueError):
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", {"exec": None})
ImageMath.unsafe_eval("(lambda: exec('exit()'))()", exec=None)


def test_logical() -> None:
assert pixel(ImageMath.unsafe_eval("not A", images)) == 0
assert pixel(ImageMath.unsafe_eval("A and B", images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A or B", images)) == "L 1"
assert pixel(ImageMath.unsafe_eval("not A", **images)) == 0
assert pixel(ImageMath.unsafe_eval("A and B", **images)) == "L 2"
assert pixel(ImageMath.unsafe_eval("A or B", **images)) == "L 1"


def test_convert() -> None:
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", images)) == "L 3"
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", images)) == "1 0"
assert pixel(ImageMath.unsafe_eval("convert(A+B, 'L')", **images)) == "L 3"
assert pixel(ImageMath.unsafe_eval("convert(A+B, '1')", **images)) == "1 0"
assert (
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", images)) == "RGB (3, 3, 3)"
pixel(ImageMath.unsafe_eval("convert(A+B, 'RGB')", **images)) == "RGB (3, 3, 3)"
)


def test_compare() -> None:
assert pixel(ImageMath.unsafe_eval("min(A, B)", images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("max(A, B)", images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A == 1", images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("A == 2", images)) == "I 0"
assert pixel(ImageMath.unsafe_eval("min(A, B)", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("max(A, B)", **images)) == "I 2"
assert pixel(ImageMath.unsafe_eval("A == 1", **images)) == "I 1"
assert pixel(ImageMath.unsafe_eval("A == 2", **images)) == "I 0"


def test_one_image_larger() -> None:
Expand Down
9 changes: 9 additions & 0 deletions docs/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ ImageDraw.getdraw hints parameter

The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.

ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. deprecated:: 11.0.0

The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more keyword
arguments can be used instead.

Removed features
----------------

Expand Down
26 changes: 14 additions & 12 deletions docs/reference/ImageMath.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,21 @@ Example: Using the :py:mod:`~PIL.ImageMath` module
b=im2
)

.. py:function:: lambda_eval(expression, options)
.. py:function:: lambda_eval(expression, options, **kw)

Returns the result of an image function.

:param expression: A function that receives a dictionary.
:param options: Values to add to the function's dictionary, mapping image
names to Image instances. You can use one or more keyword
arguments instead of a dictionary, as shown in the above
example. Note that the names must be valid Python
identifiers.
:param options: Values to add to the function's dictionary. Note that the names
must be valid Python identifiers. Deprecated.
You can instead use one or more keyword arguments, as
shown in the above example.
:param \**kw: Values to add to the function's dictionary, mapping image names to
Image instances.
:return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression.

.. py:function:: unsafe_eval(expression, options)
.. py:function:: unsafe_eval(expression, options, **kw)

Evaluates an image expression.

Expand All @@ -61,11 +62,12 @@ Example: Using the :py:mod:`~PIL.ImageMath` module
:param expression: A string which uses the standard Python expression
syntax. In addition to the standard operators, you can
also use the functions described below.
:param options: Values to add to the function's dictionary, mapping image
names to Image instances. You can use one or more keyword
arguments instead of a dictionary, as shown in the above
example. Note that the names must be valid Python
identifiers.
:param options: Values to add to the evaluation context. Note that the names must
be valid Python identifiers. Deprecated.
You can instead use one or more keyword arguments, as
shown in the above example.
:param \**kw: Values to add to the evaluation context, mapping image names to Image
instances.
:return: An image, an integer value, a floating point value,
or a pixel tuple, depending on the expression.

Expand Down
8 changes: 5 additions & 3 deletions docs/releasenotes/11.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ similarly removed.
Deprecations
============

TODO
^^^^
ImageMath.lambda_eval and ImageMath.unsafe_eval options parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

TODO
The ``options`` parameter in :py:meth:`~PIL.ImageMath.lambda_eval()` and
:py:meth:`~PIL.ImageMath.unsafe_eval()` has been deprecated. One or more
keyword arguments can be used instead.

API Changes
===========
Expand Down
Loading
Loading