diff --git a/protos/ni/pythonpanel/v1/python_panel_service.proto b/protos/ni/pythonpanel/v1/python_panel_service.proto index 70cc9a5..ea2508b 100644 --- a/protos/ni/pythonpanel/v1/python_panel_service.proto +++ b/protos/ni/pythonpanel/v1/python_panel_service.proto @@ -108,6 +108,9 @@ message SetValueRequest { // The value google.protobuf.Any value = 3; + + // Notify other clients of this new value + bool notify = 4; } message SetValueResponse { diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.py b/src/ni/pythonpanel/v1/python_panel_service_pb2.py index d6f5c8c..d75bf54 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.py +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.py @@ -14,7 +14,7 @@ from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"@\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"Z\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\"\x12\n\x10SetValueResponse2\xdb\x03\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,ni/pythonpanel/v1/python_panel_service.proto\x12\x11ni.pythonpanel.v1\x1a\x19google/protobuf/any.proto\"@\n\x11StartPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x19\n\x11panel_script_path\x18\x02 \x01(\t\"\'\n\x12StartPanelResponse\x12\x11\n\tpanel_url\x18\x01 \x01(\t\"3\n\x10StopPanelRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\r\n\x05reset\x18\x02 \x01(\x08\"\x13\n\x11StopPanelResponse\"\x18\n\x16\x45numeratePanelsRequest\"J\n\x10PanelInformation\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x11\n\tpanel_url\x18\x02 \x01(\t\x12\x11\n\tvalue_ids\x18\x03 \x03(\t\"N\n\x17\x45numeratePanelsResponse\x12\x33\n\x06panels\x18\x01 \x03(\x0b\x32#.ni.pythonpanel.v1.PanelInformation\"5\n\x0fGetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\"7\n\x10GetValueResponse\x12#\n\x05value\x18\x01 \x01(\x0b\x32\x14.google.protobuf.Any\"j\n\x0fSetValueRequest\x12\x10\n\x08panel_id\x18\x01 \x01(\t\x12\x10\n\x08value_id\x18\x02 \x01(\t\x12#\n\x05value\x18\x03 \x01(\x0b\x32\x14.google.protobuf.Any\x12\x0e\n\x06notify\x18\x04 \x01(\x08\"\x12\n\x10SetValueResponse2\xdb\x03\n\x12PythonPanelService\x12Y\n\nStartPanel\x12$.ni.pythonpanel.v1.StartPanelRequest\x1a%.ni.pythonpanel.v1.StartPanelResponse\x12V\n\tStopPanel\x12#.ni.pythonpanel.v1.StopPanelRequest\x1a$.ni.pythonpanel.v1.StopPanelResponse\x12h\n\x0f\x45numeratePanels\x12).ni.pythonpanel.v1.EnumeratePanelsRequest\x1a*.ni.pythonpanel.v1.EnumeratePanelsResponse\x12S\n\x08GetValue\x12\".ni.pythonpanel.v1.GetValueRequest\x1a#.ni.pythonpanel.v1.GetValueResponse\x12S\n\x08SetValue\x12\".ni.pythonpanel.v1.SetValueRequest\x1a#.ni.pythonpanel.v1.SetValueResponseB\x9a\x01\n\x15\x63om.ni.pythonpanel.v1B\x17PythonPanelServiceProtoP\x01Z\rpythonpanelv1\xf8\x01\x01\xa2\x02\x04NIPP\xaa\x02\"NationalInstruments.PythonPanel.V1\xca\x02\x11NI\\PythonPanel\\V1\xea\x02\x13NI::PythonPanel::V1b\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ni.pythonpanel.v1.python_panel_service_pb2', globals()) @@ -41,9 +41,9 @@ _GETVALUERESPONSE._serialized_start=512 _GETVALUERESPONSE._serialized_end=567 _SETVALUEREQUEST._serialized_start=569 - _SETVALUEREQUEST._serialized_end=659 - _SETVALUERESPONSE._serialized_start=661 - _SETVALUERESPONSE._serialized_end=679 - _PYTHONPANELSERVICE._serialized_start=682 - _PYTHONPANELSERVICE._serialized_end=1157 + _SETVALUEREQUEST._serialized_end=675 + _SETVALUERESPONSE._serialized_start=677 + _SETVALUERESPONSE._serialized_end=695 + _PYTHONPANELSERVICE._serialized_start=698 + _PYTHONPANELSERVICE._serialized_end=1173 # @@protoc_insertion_point(module_scope) diff --git a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi index 135cbf7..1fe9b49 100644 --- a/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi +++ b/src/ni/pythonpanel/v1/python_panel_service_pb2.pyi @@ -179,10 +179,13 @@ class SetValueRequest(google.protobuf.message.Message): PANEL_ID_FIELD_NUMBER: builtins.int VALUE_ID_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int + NOTIFY_FIELD_NUMBER: builtins.int panel_id: builtins.str """Unique ID of the panel""" value_id: builtins.str """Unique ID of the value""" + notify: builtins.bool + """Notify other clients of this new value""" @property def value(self) -> google.protobuf.any_pb2.Any: """The value""" @@ -193,9 +196,10 @@ class SetValueRequest(google.protobuf.message.Message): panel_id: builtins.str = ..., value_id: builtins.str = ..., value: google.protobuf.any_pb2.Any | None = ..., + notify: builtins.bool = ..., ) -> None: ... def HasField(self, field_name: typing.Literal["value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing.Literal["panel_id", b"panel_id", "value", b"value", "value_id", b"value_id"]) -> None: ... + def ClearField(self, field_name: typing.Literal["notify", b"notify", "panel_id", b"panel_id", "value", b"value", "value_id", b"value_id"]) -> None: ... global___SetValueRequest = SetValueRequest diff --git a/src/nipanel/_panel_client.py b/src/nipanel/_panel_client.py index 993711a..911d03f 100644 --- a/src/nipanel/_panel_client.py +++ b/src/nipanel/_panel_client.py @@ -100,16 +100,19 @@ def enumerate_panels(self) -> dict[str, tuple[str, list[str]]]: panel.panel_id: (panel.panel_url, list(panel.value_ids)) for panel in response.panels } - def set_value(self, panel_id: str, value_id: str, value: object) -> None: + def set_value(self, panel_id: str, value_id: str, value: object, notify: bool) -> None: """Set the value for the control with value_id. Args: panel_id: The ID of the panel. value_id: The ID of the control. value: The value to set. + notify: Whether to notify other clients of the new value. """ new_any = to_any(value) - set_value_request = SetValueRequest(panel_id=panel_id, value_id=value_id, value=new_any) + set_value_request = SetValueRequest( + panel_id=panel_id, value_id=value_id, value=new_any, notify=notify + ) self._invoke_with_retry(self._get_stub().SetValue, set_value_request) def get_value(self, panel_id: str, value_id: str) -> object: diff --git a/src/nipanel/_panel_value_accessor.py b/src/nipanel/_panel_value_accessor.py index 6059d03..afada1c 100644 --- a/src/nipanel/_panel_value_accessor.py +++ b/src/nipanel/_panel_value_accessor.py @@ -12,7 +12,7 @@ class PanelValueAccessor(ABC): """This class allows you to access values for a panel's controls.""" - __slots__ = ["_panel_client", "_panel_id", "__weakref__"] + __slots__ = ["_panel_client", "_panel_id", "_notify_on_set_value", "__weakref__"] def __init__( self, @@ -20,6 +20,7 @@ def __init__( panel_id: str, provided_interface: str, service_class: str, + notify_on_set_value: bool = True, discovery_client: DiscoveryClient | None = None, grpc_channel_pool: GrpcChannelPool | None = None, grpc_channel: grpc.Channel | None = None, @@ -33,6 +34,7 @@ def __init__( grpc_channel=grpc_channel, ) self._panel_id = panel_id + self._notify_on_set_value = notify_on_set_value @property def panel_id(self) -> str: @@ -57,4 +59,6 @@ def set_value(self, value_id: str, value: object) -> None: value_id: The id of the value value: The value """ - self._panel_client.set_value(self._panel_id, value_id, value) + self._panel_client.set_value( + self._panel_id, value_id, value, notify=self._notify_on_set_value + ) diff --git a/src/nipanel/_streamlit_panel_value_accessor.py b/src/nipanel/_streamlit_panel_value_accessor.py index c00ff24..dc9e1d2 100644 --- a/src/nipanel/_streamlit_panel_value_accessor.py +++ b/src/nipanel/_streamlit_panel_value_accessor.py @@ -12,7 +12,10 @@ @final class StreamlitPanelValueAccessor(PanelValueAccessor): - """This class provides access to values for a Streamlit panel's controls.""" + """This class provides access to values for a Streamlit panel's controls. + + This class should only be used within a Streamlit script. + """ def __init__( self, @@ -35,6 +38,7 @@ def __init__( panel_id=panel_id, provided_interface=STREAMLIT_PYTHON_PANEL_SERVICE, service_class=STREAMLIT_PYTHON_PANEL_SERVICE, + notify_on_set_value=False, discovery_client=discovery_client, grpc_channel_pool=grpc_channel_pool, grpc_channel=grpc_channel, diff --git a/tests/unit/test_panel_client.py b/tests/unit/test_panel_client.py index ce59d67..560f6d9 100644 --- a/tests/unit/test_panel_client.py +++ b/tests/unit/test_panel_client.py @@ -63,7 +63,7 @@ def test___set_value___enumerate_panels_shows_value( ) -> None: client = create_panel_client(fake_panel_channel) - client.set_value("panel1", "val1", "value1") + client.set_value("panel1", "val1", "value1", notify=False) assert client.enumerate_panels() == {"panel1": ("", ["val1"])} @@ -71,7 +71,7 @@ def test___set_value___enumerate_panels_shows_value( def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None: client = create_panel_client(fake_panel_channel) - client.set_value("panel1", "val1", "value1") + client.set_value("panel1", "val1", "value1", notify=False) assert client.get_value("panel1", "val1") == "value1" diff --git a/tests/unit/test_streamlit_panel.py b/tests/unit/test_streamlit_panel.py index 9adf971..94f9107 100644 --- a/tests/unit/test_streamlit_panel.py +++ b/tests/unit/test_streamlit_panel.py @@ -63,6 +63,32 @@ def test___panel___accessor_set_value___panel_gets_same_value( assert panel.get_value(value_id) == string_value +def test___panel___set_value___notifies( + fake_python_panel_service: FakePythonPanelService, + fake_panel_channel: grpc.Channel, +) -> None: + service = fake_python_panel_service + panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) + assert service.servicer.notification_count == 0 + + panel.set_value("value_id", "string_value") + + assert service.servicer.notification_count == 1 + + +def test___accessor___set_value___does_not_notify( + fake_python_panel_service: FakePythonPanelService, + fake_panel_channel: grpc.Channel, +) -> None: + service = fake_python_panel_service + accessor = StreamlitPanelValueAccessor("my_panel", grpc_channel=fake_panel_channel) + assert service.servicer.notification_count == 0 + + accessor.set_value("value_id", "string_value") + + assert service.servicer.notification_count == 0 + + def test___first_start_will_fail___start_panel___panel_is_functional( fake_python_panel_service: FakePythonPanelService, fake_panel_channel: grpc.Channel, diff --git a/tests/utils/_fake_python_panel_servicer.py b/tests/utils/_fake_python_panel_servicer.py index 3397273..79e4872 100644 --- a/tests/utils/_fake_python_panel_servicer.py +++ b/tests/utils/_fake_python_panel_servicer.py @@ -26,6 +26,7 @@ def __init__(self) -> None: self._panel_is_running: dict[str, bool] = {} self._panel_value_ids: dict[str, dict[str, Any]] = {} self._fail_next_start_panel = False + self._notification_count: int = 0 def StartPanel( # noqa: N802 self, request: StartPanelRequest, context: Any @@ -65,12 +66,19 @@ def SetValue(self, request: SetValueRequest, context: Any) -> SetValueResponse: """Trivial implementation for testing.""" self._init_panel(request.panel_id) self._panel_value_ids[request.panel_id][request.value_id] = request.value + if request.notify: + self._notification_count += 1 return SetValueResponse() def fail_next_start_panel(self) -> None: """Set whether the StartPanel method should fail the next time it is called.""" self._fail_next_start_panel = True + @property + def notification_count(self) -> int: + """Get the number of notifications sent from SetValue.""" + return self._notification_count + def _init_panel(self, panel_id: str) -> None: if panel_id not in self._panel_ids: self._panel_ids.append(panel_id)