From bf640547c0cd6a4b0fb7634e127430a30d9c5b49 Mon Sep 17 00:00:00 2001 From: Praful Gulani <59774145+prafulgulani@users.noreply.github.com> Date: Fri, 28 Feb 2025 20:54:41 +0530 Subject: [PATCH] Backport PR #29590: Blocked set_clim() callbacks to prevent inconsistent state (#29522) --- lib/matplotlib/colorizer.py | 21 ++++++++++++++++----- lib/matplotlib/tests/test_colors.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 4aebe7d0f5dc..b4223f389804 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -261,16 +261,27 @@ 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) + + 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'): + if vmin is not None: + self.norm.vmin = colors._sanitize_extrema(vmin) + if vmax is not None: + self.norm.vmax = colors._sanitize_extrema(vmax) + + # emit a update signal if the limits are changed + if orig_vmin_vmax != (self.norm.vmin, self.norm.vmax): + self.norm.callbacks.process('changed') def get_clim(self): """ diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index cc6cb1bb11a7..1d13b940026b 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1553,6 +1553,23 @@ 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) + + callback.assert_not_called() + + # 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() + + def test_norm_callback(): increment = unittest.mock.Mock(return_value=None)
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: