Skip to content

Commit 6d7df1e

Browse files
Mike ProsserMike Prosser
authored andcommitted
feat: implement set_value_if_changed method to optimize value updates and add tests
1 parent e77345e commit 6d7df1e

File tree

7 files changed

+145
-8
lines changed

7 files changed

+145
-8
lines changed

src/nipanel/_streamlit_panel_initializer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,4 @@ def _sync_session_state(panel: StreamlitPanelValueAccessor) -> None:
8484
for key in st.session_state.keys():
8585
value = st.session_state[key]
8686
if is_supported_type(value):
87-
panel.set_value(str(key), value)
87+
panel.set_value_if_changed(str(key), value)

src/nipanel/_streamlit_panel_value_accessor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import collections
34
from typing import final
45

56
import grpc
@@ -17,6 +18,8 @@ class StreamlitPanelValueAccessor(PanelValueAccessor):
1718
This class should only be used within a Streamlit script.
1819
"""
1920

21+
__slots__ = ["_last_values"]
22+
2023
def __init__(
2124
self,
2225
panel_id: str,
@@ -43,3 +46,19 @@ def __init__(
4346
grpc_channel_pool=grpc_channel_pool,
4447
grpc_channel=grpc_channel,
4548
)
49+
self._last_values: collections.defaultdict[str, object] = collections.defaultdict(
50+
lambda: object()
51+
)
52+
53+
def set_value_if_changed(self, value_id: str, value: object) -> None:
54+
"""Set the value for a control on the panel only if it has changed since the last call.
55+
56+
This method helps reduce unnecessary updates when the value hasn't changed.
57+
58+
Args:
59+
value_id: The id of the value
60+
value: The value to set
61+
"""
62+
if value != self._last_values[value_id]:
63+
self.set_value(value_id, value)
64+
self._last_values[value_id] = value

src/nipanel/controls/_enum_selectbox.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55

66
import streamlit as st
77

8-
from nipanel._panel_value_accessor import PanelValueAccessor
8+
from nipanel._streamlit_panel_value_accessor import StreamlitPanelValueAccessor
99

1010
T = TypeVar("T", bound=Enum)
1111

1212

1313
def enum_selectbox(
14-
panel: PanelValueAccessor,
14+
panel: StreamlitPanelValueAccessor,
1515
label: str,
1616
value: T,
1717
key: str,
@@ -49,5 +49,5 @@ def enum_selectbox(
4949
label, options=options, format_func=format_func, index=default_index, disabled=disabled
5050
)
5151
enum_value = enum_class[box_tuple[0]]
52-
panel.set_value(key, enum_value)
52+
panel.set_value_if_changed(key, enum_value)
5353
return enum_value

src/nipanel/controls/_flag_checkboxes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55

66
import streamlit as st
77

8-
from nipanel._panel_value_accessor import PanelValueAccessor
8+
from nipanel._streamlit_panel_value_accessor import StreamlitPanelValueAccessor
99

1010
T = TypeVar("T", bound=Flag)
1111

1212

1313
def flag_checkboxes(
14-
panel: PanelValueAccessor,
14+
panel: StreamlitPanelValueAccessor,
1515
label: str,
1616
value: T,
1717
key: str,
@@ -69,5 +69,5 @@ def flag_checkboxes(
6969
selected_flags &= ~flag
7070

7171
# Store the selected flags in the panel
72-
panel.set_value(key, selected_flags)
72+
panel.set_value_if_changed(key, selected_flags)
7373
return selected_flags

tests/unit/test_streamlit_panel.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def test___enum_type___set_value___gets_same_value(
297297
fake_panel_channel: grpc.Channel,
298298
value_payload: enum.Enum,
299299
) -> None:
300-
"""Test that set_value() and get_value() work for builtin scalar types."""
300+
"""Test that set_value() and get_value() work for enum types."""
301301
panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel)
302302

303303
value_id = "test_id"
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import grpc
2+
3+
from nipanel import StreamlitPanelValueAccessor
4+
from tests.types import MyIntEnum
5+
from tests.utils._fake_python_panel_service import FakePythonPanelService
6+
7+
8+
def test___no_previous_value___set_value_if_changed___sets_value(
9+
fake_panel_channel: grpc.Channel,
10+
) -> None:
11+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
12+
13+
accessor.set_value_if_changed("test_id", "test_value")
14+
15+
assert accessor.get_value("test_id") == "test_value"
16+
17+
18+
def test___set_value_if_changed___set_same_value___does_not_set_value_again(
19+
fake_panel_channel: grpc.Channel,
20+
fake_python_panel_service: FakePythonPanelService,
21+
) -> None:
22+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
23+
accessor.set_value_if_changed("test_id", "test_value")
24+
initial_set_count = fake_python_panel_service.servicer.set_count
25+
26+
accessor.set_value_if_changed("test_id", "test_value")
27+
28+
assert fake_python_panel_service.servicer.set_count == initial_set_count
29+
assert accessor.get_value("test_id") == "test_value"
30+
31+
32+
def test___set_value_if_changed___set_different_value___sets_new_value(
33+
fake_panel_channel: grpc.Channel,
34+
) -> None:
35+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
36+
accessor.set_value_if_changed("test_id", "test_value")
37+
38+
accessor.set_value_if_changed("test_id", "new_value")
39+
40+
assert accessor.get_value("test_id") == "new_value"
41+
42+
43+
def test___set_value_if_changed___different_value_ids___tracks_separately(
44+
fake_panel_channel: grpc.Channel,
45+
) -> None:
46+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
47+
48+
accessor.set_value_if_changed("id1", "value1")
49+
accessor.set_value_if_changed("id2", "value2")
50+
51+
accessor.set_value_if_changed("id1", "value1")
52+
accessor.set_value_if_changed("id2", "new_value2")
53+
54+
assert accessor.get_value("id1") == "value1"
55+
assert accessor.get_value("id2") == "new_value2"
56+
57+
58+
def test___set_value_if_changed_with_list_value___set_same_value___does_not_set_value_again(
59+
fake_panel_channel: grpc.Channel,
60+
fake_python_panel_service: FakePythonPanelService,
61+
) -> None:
62+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
63+
accessor.set_value_if_changed("test_id", [1, 2, 3])
64+
initial_set_count = fake_python_panel_service.servicer.set_count
65+
66+
accessor.set_value_if_changed("test_id", [1, 2, 3])
67+
68+
assert fake_python_panel_service.servicer.set_count == initial_set_count
69+
assert accessor.get_value("test_id") == [1, 2, 3]
70+
71+
72+
def test___set_value_if_changed_with_list_value___set_different_value___sets_new_value(
73+
fake_panel_channel: grpc.Channel,
74+
fake_python_panel_service: FakePythonPanelService,
75+
) -> None:
76+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
77+
accessor.set_value_if_changed("test_id", [1, 2, 3])
78+
initial_set_count = fake_python_panel_service.servicer.set_count
79+
80+
accessor.set_value_if_changed("test_id", [1, 2, 4])
81+
82+
assert fake_python_panel_service.servicer.set_count > initial_set_count
83+
assert accessor.get_value("test_id") == [1, 2, 4]
84+
85+
86+
def test___set_value_if_changed_with_enum_value___set_same_value___does_not_set_value_again(
87+
fake_panel_channel: grpc.Channel,
88+
fake_python_panel_service: FakePythonPanelService,
89+
) -> None:
90+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
91+
accessor.set_value_if_changed("test_id", MyIntEnum.VALUE20)
92+
initial_set_count = fake_python_panel_service.servicer.set_count
93+
94+
accessor.set_value_if_changed("test_id", MyIntEnum.VALUE20)
95+
96+
assert fake_python_panel_service.servicer.set_count == initial_set_count
97+
assert accessor.get_value("test_id") == 20 # Enums are stored as their values
98+
99+
100+
def test___set_value_if_changed_with_enum_value___set_different_value___sets_new_value(
101+
fake_panel_channel: grpc.Channel,
102+
fake_python_panel_service: FakePythonPanelService,
103+
) -> None:
104+
accessor = StreamlitPanelValueAccessor("panel_id", grpc_channel=fake_panel_channel)
105+
accessor.set_value_if_changed("test_id", MyIntEnum.VALUE20)
106+
initial_set_count = fake_python_panel_service.servicer.set_count
107+
108+
accessor.set_value_if_changed("test_id", MyIntEnum.VALUE30)
109+
110+
assert fake_python_panel_service.servicer.set_count > initial_set_count
111+
assert accessor.get_value("test_id") == 30 # New enum value should be set

tests/utils/_fake_python_panel_servicer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(self) -> None:
2626
self._panel_is_running: dict[str, bool] = {}
2727
self._panel_value_ids: dict[str, dict[str, Any]] = {}
2828
self._fail_next_start_panel = False
29+
self._set_count: int = 0
2930
self._notification_count: int = 0
3031
self._python_path: str = ""
3132

@@ -70,6 +71,7 @@ def SetValue(self, request: SetValueRequest, context: Any) -> SetValueResponse:
7071
"""Trivial implementation for testing."""
7172
self._init_panel(request.panel_id)
7273
self._panel_value_ids[request.panel_id][request.value_id] = request.value
74+
self._set_count += 1
7375
if request.notify:
7476
self._notification_count += 1
7577
return SetValueResponse()
@@ -78,6 +80,11 @@ def fail_next_start_panel(self) -> None:
7880
"""Set whether the StartPanel method should fail the next time it is called."""
7981
self._fail_next_start_panel = True
8082

83+
@property
84+
def set_count(self) -> int:
85+
"""Get the total number of times SetValue was called."""
86+
return self._set_count
87+
8188
@property
8289
def notification_count(self) -> int:
8390
"""Get the number of notifications sent from SetValue."""

0 commit comments

Comments
 (0)