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)