From 9c6fa74f656957166f38eabbb3ac4f4caafcfff2 Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Thu, 9 Jul 2026 14:26:32 +0200 Subject: [PATCH] fix(image): guard layer access during/after teardown --- bec_widgets/widgets/plots/image/image_base.py | 12 +++++++++- tests/unit_tests/test_image_view_next_gen.py | 24 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/bec_widgets/widgets/plots/image/image_base.py b/bec_widgets/widgets/plots/image/image_base.py index 2c80baea..f0899863 100644 --- a/bec_widgets/widgets/plots/image/image_base.py +++ b/bec_widgets/widgets/plots/image/image_base.py @@ -1036,6 +1036,8 @@ class ImageBase(PlotBase): """ # FIXME: this should be made more general + if not self.layer_manager: + return False return self.layer_manager["main"].image.autorange @autorange.setter @@ -1056,6 +1058,8 @@ class ImageBase(PlotBase): enabled(bool): Whether to enable autorange. sync(bool): Whether to synchronize the autorange state across all layers. """ + if not self.layer_manager: + return for layer in self.layer_manager: if not layer.sync.autorange: continue @@ -1077,6 +1081,8 @@ class ImageBase(PlotBase): - "mean": Use the mean value of the image for autoranging. """ + if not self.layer_manager: + return "mean" return self.layer_manager["main"].image.autorange_mode @autorange_mode.setter @@ -1090,6 +1096,8 @@ class ImageBase(PlotBase): # for qt Designer if mode not in ["max", "mean"]: return + if not self.layer_manager: + return for layer in self.layer_manager: if not layer.sync.autorange_mode: continue @@ -1125,6 +1133,8 @@ class ImageBase(PlotBase): """ Synchronize the autorange switch with the current autorange state and mode if changed from outside. """ + if not self.layer_manager: + return action: SwitchableToolBarAction = self.toolbar.components.get_action("image_autorange") # type: ignore with action.signal_blocker(): action.set_default_action(f"{self.layer_manager['main'].image.autorange_mode}") @@ -1133,7 +1143,7 @@ class ImageBase(PlotBase): def _sync_colorbar_levels(self): """Immediately propagate current levels to the active colorbar.""" - if not self._color_bar: + if not self._color_bar or not self.layer_manager: return total_vrange = (0, 0) diff --git a/tests/unit_tests/test_image_view_next_gen.py b/tests/unit_tests/test_image_view_next_gen.py index 5749da44..10c02f08 100644 --- a/tests/unit_tests/test_image_view_next_gen.py +++ b/tests/unit_tests/test_image_view_next_gen.py @@ -1456,4 +1456,26 @@ def test_adjust_image_buffer_coerces_list_and_scalar(qtbot, mocked_client): buf = view.adjust_image_buffer(image, [1, 2, 3]) # python list assert buf.shape == (1, 3) buf = view.adjust_image_buffer(image, np.array(42.0)) # 0-d scalar - assert buf.shape[0] == 2 \ No newline at end of file + assert buf.shape[0] == 2 + + +############################################## +# Teardown safety +############################################## + + +def test_layer_accessors_safe_after_teardown(qtbot, mocked_client): + """Once layer_manager is gone (post-cleanup), signal-driven accessors must + not raise. They used to subscript None or resurrect the 'main' layer.""" + view = create_widget(qtbot, Image, client=mocked_client) + view.enable_full_colorbar = True + view.layer_manager = None # simulate post-cleanup state + + # None of these should raise: + assert view.autorange is False + assert view.autorange_mode == "mean" + view.toggle_autorange(True, "mean") + view._set_autorange(True) + view.autorange_mode = "max" + view._sync_autorange_switch() + view._sync_colorbar_levels()