From 8a8919d4cb50aab988a480b017bdb50a0a104387 Mon Sep 17 00:00:00 2001 From: prafulgulani555 <59774145+prafulgulani555@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:20:28 +0530 Subject: [PATCH 1/4] Blocked set_clim() callbacks to prevent inconsistent state (#29522) --- lib/matplotlib/colorizer.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 4aebe7d0f5dc..db98edd02702 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -261,16 +261,24 @@ def set_clim(self, vmin=None, vmax=None): .. ACCEPTS: (vmin: float, vmax: float) """ # If the norm's limits are updated self.changed() will be called - # through the callbacks attached to the norm + # through the callbacks attached to the norm, this causes an inconsistent + # state, to prevent this blocked context manager is used if vmax is None: try: vmin, vmax = vmin except (TypeError, ValueError): pass - if vmin is not None: - self.norm.vmin = colors._sanitize_extrema(vmin) - if vmax is not None: - self.norm.vmax = colors._sanitize_extrema(vmax) + + # Blocked context manager prevents callbacks from being triggered + # until both vmin and vmax are updated + with self.norm.callbacks.blocked(signal='changed'): + if vmin is not None: + self.norm.vmin = colors._sanitize_extrema(vmin) + if vmax is not None: + self.norm.vmax = colors._sanitize_extrema(vmax) + + # self.changed() will now emit a update signal after both the limits are set + self.changed() def get_clim(self): """ From 0c40fe14596b46468b625ca4e49343d32c5901ee Mon Sep 17 00:00:00 2001 From: prafulgulani555 <59774145+prafulgulani555@users.noreply.github.com> Date: Tue, 11 Feb 2025 22:35:42 +0530 Subject: [PATCH 2/4] added test for set_clim() callback count --- lib/matplotlib/colorizer.py | 7 +++++-- lib/matplotlib/tests/test_colorbar.py | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index db98edd02702..412b49251248 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -269,6 +269,8 @@ def set_clim(self, vmin=None, vmax=None): except (TypeError, ValueError): pass + orig_vmin_vmax = self.norm.vmin, self.norm.vmax + # Blocked context manager prevents callbacks from being triggered # until both vmin and vmax are updated with self.norm.callbacks.blocked(signal='changed'): @@ -277,8 +279,9 @@ def set_clim(self, vmin=None, vmax=None): if vmax is not None: self.norm.vmax = colors._sanitize_extrema(vmax) - # self.changed() will now emit a update signal after both the limits are set - self.changed() + # self.changed() will now emit a update signal if the limits are changed + if orig_vmin_vmax != (self.norm.vmin, self.norm.vmax): + self.changed() def get_clim(self): """ diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index b53f50385aaa..a7b65c839af2 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -18,6 +18,8 @@ from matplotlib.ticker import FixedLocator, LogFormatter, StrMethodFormatter from matplotlib.testing.decorators import check_figures_equal +import unittest.mock + def _get_cmap_norms(): """ @@ -660,6 +662,26 @@ def test_colorbar_scale_reset(): assert pcm.norm.vmax == z.max() +def test_set_clim_emits_single_callback(): + data = np.array([[1, 2], [3, 4]]) + fig, ax = plt.subplots() + image = ax.imshow(data, cmap='viridis') + + callback = unittest.mock.Mock() + image.norm.callbacks.connect('changed', callback) + + # Initial callback count should be zero + assert callback.call_count == 0 + + # Call set_clim() to update the limits + image.set_clim(1, 5) + + # Assert that only one "changed" callback is sent after calling set_clim() + callback.assert_called_once() + + plt.close(fig) + + def test_colorbar_get_ticks_2(): plt.rcParams['_internal.classic_mode'] = False fig, ax = plt.subplots() From bf5370d8d97f5547ef338bd69f099cde02065a80 Mon Sep 17 00:00:00 2001 From: prafulgulani555 <59774145+prafulgulani555@users.noreply.github.com> Date: Wed, 12 Feb 2025 21:51:20 +0530 Subject: [PATCH 3/4] Relocated test_set_clim_emits_single_callback to test_colors.py --- lib/matplotlib/colorizer.py | 4 ++-- lib/matplotlib/tests/test_colorbar.py | 22 ---------------------- lib/matplotlib/tests/test_colors.py | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 412b49251248..b4223f389804 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -279,9 +279,9 @@ def set_clim(self, vmin=None, vmax=None): if vmax is not None: self.norm.vmax = colors._sanitize_extrema(vmax) - # self.changed() will now emit a update signal if the limits are changed + # emit a update signal if the limits are changed if orig_vmin_vmax != (self.norm.vmin, self.norm.vmax): - self.changed() + self.norm.callbacks.process('changed') def get_clim(self): """ diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index a7b65c839af2..b53f50385aaa 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -18,8 +18,6 @@ from matplotlib.ticker import FixedLocator, LogFormatter, StrMethodFormatter from matplotlib.testing.decorators import check_figures_equal -import unittest.mock - def _get_cmap_norms(): """ @@ -662,26 +660,6 @@ def test_colorbar_scale_reset(): assert pcm.norm.vmax == z.max() -def test_set_clim_emits_single_callback(): - data = np.array([[1, 2], [3, 4]]) - fig, ax = plt.subplots() - image = ax.imshow(data, cmap='viridis') - - callback = unittest.mock.Mock() - image.norm.callbacks.connect('changed', callback) - - # Initial callback count should be zero - assert callback.call_count == 0 - - # Call set_clim() to update the limits - image.set_clim(1, 5) - - # Assert that only one "changed" callback is sent after calling set_clim() - callback.assert_called_once() - - plt.close(fig) - - def test_colorbar_get_ticks_2(): plt.rcParams['_internal.classic_mode'] = False fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 24c4ebba4920..7481d617e154 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1605,6 +1605,26 @@ def test_norm_deepcopy(): assert norm2.vmin == norm.vmin +def test_set_clim_emits_single_callback(): + data = np.array([[1, 2], [3, 4]]) + fig, ax = plt.subplots() + image = ax.imshow(data, cmap='viridis') + + callback = unittest.mock.Mock() + image.norm.callbacks.connect('changed', callback) + + # Initial callback count should be zero + assert callback.call_count == 0 + + # Call set_clim() to update the limits + image.set_clim(1, 5) + + # Assert that only one "changed" callback is sent after calling set_clim() + callback.assert_called_once() + + plt.close(fig) + + def test_norm_callback(): increment = unittest.mock.Mock(return_value=None) From 49a0ab2db7ae0e1dc9b481fd07c5ed28e506b4f3 Mon Sep 17 00:00:00 2001 From: prafulgulani555 <59774145+prafulgulani555@users.noreply.github.com> Date: Sat, 22 Feb 2025 15:46:30 +0530 Subject: [PATCH 4/4] test case corrections --- lib/matplotlib/tests/test_colors.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 7481d617e154..8d0f3467f045 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1613,8 +1613,7 @@ def test_set_clim_emits_single_callback(): callback = unittest.mock.Mock() image.norm.callbacks.connect('changed', callback) - # Initial callback count should be zero - assert callback.call_count == 0 + callback.assert_not_called() # Call set_clim() to update the limits image.set_clim(1, 5) @@ -1622,8 +1621,6 @@ def test_set_clim_emits_single_callback(): # Assert that only one "changed" callback is sent after calling set_clim() callback.assert_called_once() - plt.close(fig) - def test_norm_callback(): increment = unittest.mock.Mock(return_value=None)