From 5b1dd82c1bb5d3c8eaf976adbb0a7e73c889b4db Mon Sep 17 00:00:00 2001 From: wyzula-jan Date: Tue, 16 Jun 2026 12:01:58 +0200 Subject: [PATCH] feat(ring_progress_bar): add set_progress_state for one-call ring + label updates --- bec_widgets/cli/client.py | 29 +++++++++ .../ring_progress_bar/ring_progress_bar.py | 39 ++++++++++++ tests/unit_tests/test_ring_progress_bar.py | 59 +++++++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py index 92c108e8..849c204a 100644 --- a/bec_widgets/cli/client.py +++ b/bec_widgets/cli/client.py @@ -5285,6 +5285,35 @@ class RingProgressBar(RPCBase): text(str): Text for the center label. """ + @rpc_call + def set_progress_state( + self, + values: list[float | None] | dict[int, float | None] | None = None, + center_label: str | None = None, + **kwargs, + ): + """ + Update several rings and the center label in a single call. + + Updating each ring value and the center label individually is one RPC + round-trip per call; over a long-running scan that is a steady stream of + round-trips, and each one also triggers a server-side registry + broadcast. ``set_progress_state`` applies all of them within a single + method call, i.e. one RPC round-trip and one broadcast. + + Args: + values: Ring values to apply. Either a list aligned to ring index, + where ``None`` leaves that ring unchanged (e.g. + ``[12.0, None, 80.0]`` updates rings 0 and 2), or a + ``{index: value}`` mapping to update only specific rings. + ``None`` leaves all ring values unchanged. + center_label: New center-label text, or ``None`` to leave the + current text unchanged. + **kwargs: Ignored. Absorbs RPC-layer pass-through keyword arguments + (e.g. ``_rpc_wait_for_response``) so the call does not fail when + extra control kwargs are supplied by the client. + """ + class SBBMonitor(RPCBase): """A widget to display the SBB monitor website.""" diff --git a/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py b/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py index e427caa4..3d184969 100644 --- a/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py +++ b/bec_widgets/widgets/progress/ring_progress_bar/ring_progress_bar.py @@ -393,6 +393,7 @@ class RingProgressBar(BECWidget, QWidget): "remove_ring", "set_gap", "set_center_label", + "set_progress_state", ] def __init__(self, parent: QWidget | None = None, client=None, **kwargs): @@ -511,6 +512,44 @@ class RingProgressBar(BECWidget, QWidget): """ self.center_label = text + def set_progress_state( + self, + values: list[float | None] | dict[int, float | None] | None = None, + center_label: str | None = None, + **kwargs, + ): + """Update several rings and the center label in a single call. + + Updating each ring value and the center label individually is one RPC + round-trip per call; over a long-running scan that is a steady stream of + round-trips, and each one also triggers a server-side registry + broadcast. ``set_progress_state`` applies all of them within a single + method call, i.e. one RPC round-trip and one broadcast. + + Args: + values: Ring values to apply. Either a list aligned to ring index, + where ``None`` leaves that ring unchanged (e.g. + ``[12.0, None, 80.0]`` updates rings 0 and 2), or a + ``{index: value}`` mapping to update only specific rings. + ``None`` leaves all ring values unchanged. + center_label: New center-label text, or ``None`` to leave the + current text unchanged. + **kwargs: Ignored. Absorbs RPC-layer pass-through keyword arguments + (e.g. ``_rpc_wait_for_response``) so the call does not fail when + extra control kwargs are supplied by the client. + """ + rings = self.rings + if isinstance(values, dict): + for index, value in values.items(): + if value is not None and 0 <= int(index) < len(rings): + rings[int(index)].set_value(value) + elif values is not None: + for ring, value in zip(rings, values): + if value is not None: + ring.set_value(value) + if center_label is not None: + self.center_label = center_label + @property def rings(self) -> list[Ring]: return self.ring_progress_bar.rings diff --git a/tests/unit_tests/test_ring_progress_bar.py b/tests/unit_tests/test_ring_progress_bar.py index 5abf7bb7..92f93349 100644 --- a/tests/unit_tests/test_ring_progress_bar.py +++ b/tests/unit_tests/test_ring_progress_bar.py @@ -741,3 +741,62 @@ def test_leave_event_clears_hover_and_hides_tooltip(qtbot, container): assert ring._hovered is False assert container._last_hover_global_pos is None container._hover_tooltip.hide.assert_called() + + +def _ensure_three_rings(ring_progress_bar): + while len(ring_progress_bar.rings) < 3: + ring_progress_bar.add_ring() + rings = ring_progress_bar.rings + for ring in rings: + ring.set_value(1.0) # sentinel so "unchanged" assertions are meaningful + return rings + + +def test_set_progress_state_in_user_access(): + assert "set_progress_state" in RingProgressBar.USER_ACCESS + + +def test_set_progress_state_list_updates_selected_rings_and_label(ring_progress_bar): + rings = _ensure_three_rings(ring_progress_bar) + ring_progress_bar.set_progress_state([12.0, None, 80.0], center_label="batched") + assert rings[0].config.value == 12.0 # updated + assert rings[1].config.value == 1.0 # None -> unchanged + assert rings[2].config.value == 80.0 # updated + assert ring_progress_bar.center_label == "batched" + + +def test_set_progress_state_accepts_index_mapping(ring_progress_bar): + rings = _ensure_three_rings(ring_progress_bar) + ring_progress_bar.set_progress_state({0: 5.0, 2: 95.0}) + assert rings[0].config.value == 5.0 + assert rings[1].config.value == 1.0 # untouched index + assert rings[2].config.value == 95.0 + + +def test_set_progress_state_label_only_leaves_rings_untouched(ring_progress_bar): + rings = _ensure_three_rings(ring_progress_bar) + ring_progress_bar.set_progress_state(center_label="only label") + assert [r.config.value for r in rings] == [1.0, 1.0, 1.0] + assert ring_progress_bar.center_label == "only label" + + +def test_set_progress_state_ignores_rpc_passthrough_kwargs(ring_progress_bar): + _ensure_three_rings(ring_progress_bar) + # the client may pass control kwargs (e.g. _rpc_wait_for_response); must not raise + ring_progress_bar.set_progress_state([3.0], center_label="kw", _rpc_wait_for_response=False) + assert ring_progress_bar.rings[0].config.value == 3.0 + assert ring_progress_bar.center_label == "kw" + + +def test_set_progress_state_no_rings_is_noop(ring_progress_bar): + # a freshly created RingProgressBar has no rings; must not raise + assert ring_progress_bar.rings == [] + ring_progress_bar.set_progress_state([1.0, 2.0], center_label="safe") + assert ring_progress_bar.center_label == "safe" + + +def test_set_progress_state_clamps_and_ignores_out_of_range_index(ring_progress_bar): + rings = _ensure_three_rings(ring_progress_bar) + # value above max clamps to 100; out-of-range index is ignored, not an error + ring_progress_bar.set_progress_state({0: 150.0, 99: 42.0}) + assert rings[0].config.value == 100.0