From 8d7ccb08bf6eaaa11ddc6285bf042cbbc399c68d Mon Sep 17 00:00:00 2001 From: Felix Goudreault Date: Tue, 7 Feb 2023 14:05:19 -0500 Subject: [PATCH 1/4] Fix issue #25164 --- lib/matplotlib/backends/_backend_tk.py | 2 ++ lib/matplotlib/tests/test_backend_tk.py | 36 ++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index e29b5a7be320..21e29378c81d 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -747,6 +747,8 @@ def _recolor_icon(image, color): # Use the high-resolution (48x48 px) icon if it exists and is needed with Image.open(path_large if (size > 24 and path_large.exists()) else path_regular) as im: + # assure a RGBA image as foreground color is RGB + im = im.convert("RGBA") image = ImageTk.PhotoImage(im.resize((size, size)), master=self) button._ntimage = image diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index eefaefbb023f..5271e0381654 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -4,11 +4,19 @@ import platform import subprocess import sys +import tempfile +import warnings + +from PIL import Image import pytest -from matplotlib.testing import subprocess_run_helper +import matplotlib from matplotlib import _c_internal_utils +from matplotlib.backend_tools import ToolToggleBase +from matplotlib.testing import subprocess_run_helper +import matplotlib.pyplot as plt + _test_timeout = 60 # A reasonably safe value for slower architectures. @@ -177,6 +185,32 @@ def test_never_update(): # checks them. +@pytest.mark.backend('TkAgg', skip_on_importerror=True) +@_isolated_tk_test(success_count=0) +def test_toolbar_button_la_mode_icon(): + # test that icon in LA mode can be used for buttons + # see GH#25164 + # tweaking toolbar raises an UserWarning + with warnings.catch_warnings(): + warnings.simplefilter("ignore", UserWarning) + matplotlib.rcParams["toolbar"] = "toolmanager" + + # create an icon in LA mode + with tempfile.TemporaryDirectory() as tempdir: + img = Image.new("LA", (26, 26)) + tmp_img_path = os.path.join(tempdir, "test_la_icon.png") + img.save(tmp_img_path) + + class CustomTool(ToolToggleBase): + image = tmp_img_path + + fig = plt.figure() + toolmanager = fig.canvas.manager.toolmanager + toolbar = fig.canvas.manager.toolbar + toolmanager.add_tool("test", CustomTool) + toolbar.add_tool("test", "group") + + @_isolated_tk_test(success_count=2) def test_missing_back_button(): import matplotlib.pyplot as plt From cb89b0121e295e51baeb9a5d741025aed0e1d163 Mon Sep 17 00:00:00 2001 From: Felix Goudreault Date: Wed, 8 Feb 2023 13:29:56 -0500 Subject: [PATCH 2/4] Generalize test for multiple backends. --- lib/matplotlib/tests/test_backend_tk.py | 33 -------------- lib/matplotlib/tests/test_backend_tools.py | 51 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 5271e0381654..f3257acd5bd3 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -4,18 +4,11 @@ import platform import subprocess import sys -import tempfile -import warnings - -from PIL import Image import pytest -import matplotlib from matplotlib import _c_internal_utils -from matplotlib.backend_tools import ToolToggleBase from matplotlib.testing import subprocess_run_helper -import matplotlib.pyplot as plt _test_timeout = 60 # A reasonably safe value for slower architectures. @@ -185,32 +178,6 @@ def test_never_update(): # checks them. -@pytest.mark.backend('TkAgg', skip_on_importerror=True) -@_isolated_tk_test(success_count=0) -def test_toolbar_button_la_mode_icon(): - # test that icon in LA mode can be used for buttons - # see GH#25164 - # tweaking toolbar raises an UserWarning - with warnings.catch_warnings(): - warnings.simplefilter("ignore", UserWarning) - matplotlib.rcParams["toolbar"] = "toolmanager" - - # create an icon in LA mode - with tempfile.TemporaryDirectory() as tempdir: - img = Image.new("LA", (26, 26)) - tmp_img_path = os.path.join(tempdir, "test_la_icon.png") - img.save(tmp_img_path) - - class CustomTool(ToolToggleBase): - image = tmp_img_path - - fig = plt.figure() - toolmanager = fig.canvas.manager.toolmanager - toolbar = fig.canvas.manager.toolbar - toolmanager.add_tool("test", CustomTool) - toolbar.add_tool("test", "group") - - @_isolated_tk_test(success_count=2) def test_missing_back_button(): import matplotlib.pyplot as plt diff --git a/lib/matplotlib/tests/test_backend_tools.py b/lib/matplotlib/tests/test_backend_tools.py index cc05a1a98f78..2d37a9606a00 100644 --- a/lib/matplotlib/tests/test_backend_tools.py +++ b/lib/matplotlib/tests/test_backend_tools.py @@ -1,6 +1,17 @@ +import os +import subprocess +import tempfile +from PIL import Image + import pytest -from matplotlib.backend_tools import ToolHelpBase +import matplotlib +from matplotlib.backend_tools import ToolHelpBase, ToolToggleBase +import matplotlib.pyplot as plt +from matplotlib.testing import subprocess_run_helper +from .test_backends_interactive import ( + _get_testable_interactive_backends, _test_timeout, + ) @pytest.mark.parametrize('rc_shortcut,expected', [ @@ -18,3 +29,41 @@ ]) def test_format_shortcut(rc_shortcut, expected): assert ToolHelpBase.format_shortcut(rc_shortcut) == expected + + +def _test_toolbar_button_la_mode_icon_inside_subprocess(): + matplotlib.rcParams["toolbar"] = "toolmanager" + # create an icon in LA mode + with tempfile.TemporaryDirectory() as tempdir: + img = Image.new("LA", (26, 26)) + tmp_img_path = os.path.join(tempdir, "test_la_icon.png") + img.save(tmp_img_path) + + class CustomTool(ToolToggleBase): + image = tmp_img_path + description = "" # gtk3 backend does not allow None + + fig = plt.figure() + toolmanager = fig.canvas.manager.toolmanager + toolbar = fig.canvas.manager.toolbar + toolmanager.add_tool("test", CustomTool) + toolbar.add_tool("test", "group") + + +@pytest.mark.parametrize( + "env", + _get_testable_interactive_backends(), + ) +def test_toolbar_button_la_mode_icon(env): + # test that icon in LA mode can be used for buttons + # see GH#25164 + try: + # run inside subprocess for a self-contained environment + proc = subprocess_run_helper( + _test_toolbar_button_la_mode_icon_inside_subprocess, + timeout=_test_timeout, + extra_env=env, + ) + except subprocess.CalledProcessError as err: + pytest.fail( + f"subprocess failed to test intended behavior: {err.stderr}") From 6ff693a7880922fb715ea9d1ebdcb5fd68532246 Mon Sep 17 00:00:00 2001 From: Felix Goudreault Date: Wed, 8 Feb 2023 17:37:40 -0500 Subject: [PATCH 3/4] Fixed similar bug in wx backend. --- lib/matplotlib/backends/backend_wx.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index d41b98a47266..d4eef8e22705 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1112,7 +1112,9 @@ def _icon(name): *name*, including the extension and relative to Matplotlib's "images" data directory. """ - image = np.array(PIL.Image.open(cbook._get_data_path("images", name))) + pilimg = PIL.Image.open(cbook._get_data_path("images", name)) + # ensure RGBA as wx BitMap expects RGBA format + image = np.array(pilimg.convert("RGBA")) try: dark = wx.SystemSettings.GetAppearance().IsDark() except AttributeError: # wxpython < 4.1 From 66a8dd5ef37c216809557455b0bdc3390ffef275 Mon Sep 17 00:00:00 2001 From: Felix Goudreault Date: Thu, 9 Feb 2023 10:56:10 -0500 Subject: [PATCH 4/4] Moved LA file mode for toolbar icon test in test_backends_interactive.py --- lib/matplotlib/tests/test_backend_tools.py | 51 +------------------ .../tests/test_backends_interactive.py | 43 +++++++++++++--- 2 files changed, 38 insertions(+), 56 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_tools.py b/lib/matplotlib/tests/test_backend_tools.py index 2d37a9606a00..cc05a1a98f78 100644 --- a/lib/matplotlib/tests/test_backend_tools.py +++ b/lib/matplotlib/tests/test_backend_tools.py @@ -1,17 +1,6 @@ -import os -import subprocess -import tempfile -from PIL import Image - import pytest -import matplotlib -from matplotlib.backend_tools import ToolHelpBase, ToolToggleBase -import matplotlib.pyplot as plt -from matplotlib.testing import subprocess_run_helper -from .test_backends_interactive import ( - _get_testable_interactive_backends, _test_timeout, - ) +from matplotlib.backend_tools import ToolHelpBase @pytest.mark.parametrize('rc_shortcut,expected', [ @@ -29,41 +18,3 @@ ]) def test_format_shortcut(rc_shortcut, expected): assert ToolHelpBase.format_shortcut(rc_shortcut) == expected - - -def _test_toolbar_button_la_mode_icon_inside_subprocess(): - matplotlib.rcParams["toolbar"] = "toolmanager" - # create an icon in LA mode - with tempfile.TemporaryDirectory() as tempdir: - img = Image.new("LA", (26, 26)) - tmp_img_path = os.path.join(tempdir, "test_la_icon.png") - img.save(tmp_img_path) - - class CustomTool(ToolToggleBase): - image = tmp_img_path - description = "" # gtk3 backend does not allow None - - fig = plt.figure() - toolmanager = fig.canvas.manager.toolmanager - toolbar = fig.canvas.manager.toolbar - toolmanager.add_tool("test", CustomTool) - toolbar.add_tool("test", "group") - - -@pytest.mark.parametrize( - "env", - _get_testable_interactive_backends(), - ) -def test_toolbar_button_la_mode_icon(env): - # test that icon in LA mode can be used for buttons - # see GH#25164 - try: - # run inside subprocess for a self-contained environment - proc = subprocess_run_helper( - _test_toolbar_button_la_mode_icon_inside_subprocess, - timeout=_test_timeout, - extra_env=env, - ) - except subprocess.CalledProcessError as err: - pytest.fail( - f"subprocess failed to test intended behavior: {err.stderr}") diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 498aa3b48c06..fa5566876919 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -7,13 +7,17 @@ import signal import subprocess import sys +import tempfile import time import urllib.request +from PIL import Image + import pytest import matplotlib as mpl from matplotlib import _c_internal_utils +from matplotlib.backend_tools import ToolToggleBase from matplotlib.testing import subprocess_run_helper as _run_helper @@ -71,6 +75,24 @@ def _get_testable_interactive_backends(): _test_timeout = 60 # A reasonably safe value for slower architectures. +def _test_toolbar_button_la_mode_icon(fig): + # test a toolbar button icon using an image in LA mode (GH issue 25174) + # create an icon in LA mode + with tempfile.TemporaryDirectory() as tempdir: + img = Image.new("LA", (26, 26)) + tmp_img_path = os.path.join(tempdir, "test_la_icon.png") + img.save(tmp_img_path) + + class CustomTool(ToolToggleBase): + image = tmp_img_path + description = "" # gtk3 backend does not allow None + + toolmanager = fig.canvas.manager.toolmanager + toolbar = fig.canvas.manager.toolbar + toolmanager.add_tool("test", CustomTool) + toolbar.add_tool("test", "group") + + # The source of this function gets extracted and run in another process, so it # must be fully self-contained. # Using a timer not only allows testing of timers (on other backends), but is @@ -122,7 +144,6 @@ def check_alt_backend(alt_backend): if importlib.util.find_spec("cairocffi"): check_alt_backend(backend[:-3] + "cairo") check_alt_backend("svg") - mpl.use(backend, force=True) fig, ax = plt.subplots() @@ -130,6 +151,10 @@ def check_alt_backend(alt_backend): type(fig.canvas).__module__, f"matplotlib.backends.backend_{backend}") + if mpl.rcParams["toolbar"] == "toolmanager": + # test toolbar button icon LA mode see GH issue 25174 + _test_toolbar_button_la_mode_icon(fig) + ax.plot([0, 1], [2, 3]) if fig.canvas.toolbar: # i.e toolbar2. fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2) @@ -168,11 +193,17 @@ def test_interactive_backend(env, toolbar): pytest.skip("toolmanager is not implemented for macosx.") if env["MPLBACKEND"] == "wx": pytest.skip("wx backend is deprecated; tests failed on appveyor") - proc = _run_helper(_test_interactive_impl, - json.dumps({"toolbar": toolbar}), - timeout=_test_timeout, - extra_env=env) - + try: + proc = _run_helper( + _test_interactive_impl, + json.dumps({"toolbar": toolbar}), + timeout=_test_timeout, + extra_env=env, + ) + except subprocess.CalledProcessError as err: + pytest.fail( + "Subprocess failed to test intended behavior\n" + + str(err.stderr)) assert proc.stdout.count("CloseEvent") == 1