Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions protos/ni/pythonpanel/v1/python_panel_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ option ruby_package = "NI::PythonPanel::V1";

// Service interface for interacting with python panels
service PythonPanelService {
// Enumerate the panels available in the system
// Enumerate the panels available in the system, including information about the state of the panels and what values they have.
// Status Codes for errors:
rpc EnumeratePanels(EnumeratePanelsRequest) returns (EnumeratePanelsResponse);

Expand Down Expand Up @@ -46,9 +46,20 @@ service PythonPanelService {
message EnumeratePanelsRequest {
}

message PanelInformation {
// Unique ID of the panel
string panel_id = 1;

// Is the panel currently open?
bool is_open = 2;

// IDs of all of the values associated with the panel
repeated string value_ids = 3;
}

message EnumeratePanelsResponse {
// The list of panels available in the system
repeated string panel_ids = 1;
repeated PanelInformation panels = 1;
}

message OpenPanelRequest {
Expand Down
44 changes: 23 additions & 21 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 30 additions & 4 deletions src/ni/pythonpanel/v1/python_panel_service_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,47 @@ class EnumeratePanelsRequest(google.protobuf.message.Message):

global___EnumeratePanelsRequest = EnumeratePanelsRequest

@typing.final
class PanelInformation(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_ID_FIELD_NUMBER: builtins.int
IS_OPEN_FIELD_NUMBER: builtins.int
VALUE_IDS_FIELD_NUMBER: builtins.int
panel_id: builtins.str
"""Unique ID of the panel"""
is_open: builtins.bool
"""Is the panel currently open?"""
@property
def value_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
"""IDs of all of the values associated with the panel"""

def __init__(
self,
*,
panel_id: builtins.str = ...,
is_open: builtins.bool = ...,
value_ids: collections.abc.Iterable[builtins.str] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["is_open", b"is_open", "panel_id", b"panel_id", "value_ids", b"value_ids"]) -> None: ...

global___PanelInformation = PanelInformation

@typing.final
class EnumeratePanelsResponse(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor

PANEL_IDS_FIELD_NUMBER: builtins.int
PANELS_FIELD_NUMBER: builtins.int
@property
def panel_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]:
def panels(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___PanelInformation]:
"""The list of panels available in the system"""

def __init__(
self,
*,
panel_ids: collections.abc.Iterable[builtins.str] | None = ...,
panels: collections.abc.Iterable[global___PanelInformation] | None = ...,
) -> None: ...
def ClearField(self, field_name: typing.Literal["panel_ids", b"panel_ids"]) -> None: ...
def ClearField(self, field_name: typing.Literal["panels", b"panels"]) -> None: ...

global___EnumeratePanelsResponse = EnumeratePanelsResponse

Expand Down
2 changes: 1 addition & 1 deletion src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class PythonPanelServiceServicer(object):
"""

def EnumeratePanels(self, request, context):
"""Enumerate the panels available in the system
"""Enumerate the panels available in the system, including information about the state of the panels and what values they have.
Status Codes for errors:
"""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
Expand Down
6 changes: 3 additions & 3 deletions src/ni/pythonpanel/v1/python_panel_service_pb2_grpc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PythonPanelServiceStub:
ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsRequest,
ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse,
]
"""Enumerate the panels available in the system
"""Enumerate the panels available in the system, including information about the state of the panels and what values they have.
Status Codes for errors:
"""

Expand Down Expand Up @@ -74,7 +74,7 @@ class PythonPanelServiceAsyncStub:
ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsRequest,
ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse,
]
"""Enumerate the panels available in the system
"""Enumerate the panels available in the system, including information about the state of the panels and what values they have.
Status Codes for errors:
"""

Expand Down Expand Up @@ -125,7 +125,7 @@ class PythonPanelServiceServicer(metaclass=abc.ABCMeta):
request: ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsRequest,
context: _ServicerContext,
) -> typing.Union[ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse, collections.abc.Awaitable[ni.pythonpanel.v1.python_panel_service_pb2.EnumeratePanelsResponse]]:
"""Enumerate the panels available in the system
"""Enumerate the panels available in the system, including information about the state of the panels and what values they have.
Status Codes for errors:
"""

Expand Down
24 changes: 24 additions & 0 deletions src/nipanel/_panel.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
from __future__ import annotations

import sys
from abc import ABC
from types import TracebackType
from typing import TYPE_CHECKING

import grpc
from ni_measurement_plugin_sdk_service.discovery import DiscoveryClient
from ni_measurement_plugin_sdk_service.grpc.channelpool import GrpcChannelPool

from nipanel._panel_value_accessor import PanelValueAccessor

if TYPE_CHECKING:
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
Comment on lines +14 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not doing this anymore. Use typing_extensions directly.

#57



class Panel(PanelValueAccessor, ABC):
"""This class allows you to open a panel and specify values for its controls."""
Expand Down Expand Up @@ -41,6 +50,21 @@ def panel_uri(self) -> str:
"""Read-only accessor for the panel URI."""
return self._panel_uri

def __enter__(self) -> Self:
"""Enter the runtime context related to this object."""
self.open_panel()
return self

def __exit__(
self,
exctype: type[BaseException] | None,
excinst: BaseException | None,
exctb: TracebackType | None,
) -> bool | None:
"""Exit the runtime context related to this object."""
self.close_panel(reset=False)
return None

def open_panel(self) -> None:
"""Open the panel."""
self._panel_client.open_panel(self._panel_id, self._panel_uri)
Expand Down
7 changes: 4 additions & 3 deletions src/nipanel/_panel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,18 @@ def close_panel(self, panel_id: str, reset: bool) -> None:
close_panel_request = ClosePanelRequest(panel_id=panel_id, reset=reset)
self._invoke_with_retry(self._get_stub().ClosePanel, close_panel_request)

def enumerate_panels(self) -> list[str]:
def enumerate_panels(self) -> dict[str, tuple[bool, list[str]]]:
"""Enumerate all available panels.

Returns:
A list of panel IDs.
A dictionary mapping panel IDs to a tuple containing a boolean indicating if the panel
is open and a list of value IDs associated with the panel.
"""
enumerate_panels_request = EnumeratePanelsRequest()
response = self._invoke_with_retry(
self._get_stub().EnumeratePanels, enumerate_panels_request
)
return list(response.panel_ids)
return {panel.panel_id: (panel.is_open, list(panel.value_ids)) for panel in response.panels}

def set_value(self, panel_id: str, value_id: str, value: object) -> None:
"""Set the value for the control with value_id.
Expand Down
26 changes: 22 additions & 4 deletions tests/unit/test_panel_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
def test___enumerate_is_empty(fake_panel_channel: grpc.Channel) -> None:
client = create_panel_client(fake_panel_channel)

assert client.enumerate_panels() == []
assert client.enumerate_panels() == {}


def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel) -> None:
Expand All @@ -16,7 +16,10 @@ def test___open_panels___enumerate_has_panels(fake_panel_channel: grpc.Channel)
client.open_panel("panel1", "uri1")
client.open_panel("panel2", "uri2")

assert client.enumerate_panels() == ["panel1", "panel2"]
assert client.enumerate_panels() == {
"panel1": (True, []),
"panel2": (True, []),
}


def test___open_panels___close_panel_1_with_reset___enumerate_has_panel_2(
Expand All @@ -28,7 +31,9 @@ def test___open_panels___close_panel_1_with_reset___enumerate_has_panel_2(

client.close_panel("panel1", reset=True)

assert client.enumerate_panels() == ["panel2"]
assert client.enumerate_panels() == {
"panel2": (True, []),
}


def test___open_panels___close_panel_1_without_reset___enumerate_has_both_panels(
Expand All @@ -40,7 +45,10 @@ def test___open_panels___close_panel_1_without_reset___enumerate_has_both_panels

client.close_panel("panel1", reset=False)

assert client.enumerate_panels() == ["panel1", "panel2"]
assert client.enumerate_panels() == {
"panel1": (False, []),
"panel2": (True, []),
}


def test___get_unset_value_raises_exception(fake_panel_channel: grpc.Channel) -> None:
Expand All @@ -50,6 +58,16 @@ def test___get_unset_value_raises_exception(fake_panel_channel: grpc.Channel) ->
client.get_value("panel1", "unset_id")


def test___set_value___enumerate_panels_shows_value(
fake_panel_channel: grpc.Channel,
) -> None:
client = create_panel_client(fake_panel_channel)

client.set_value("panel1", "val1", "value1")

assert client.enumerate_panels() == {"panel1": (False, ["val1"])}


def test___set_value___gets_value(fake_panel_channel: grpc.Channel) -> None:
client = create_panel_client(fake_panel_channel)

Expand Down
47 changes: 46 additions & 1 deletion tests/unit/test_streamlit_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,10 @@ def test___unopened_panel___set_value___sets_value(

value_id = "test_id"
string_value = "test_value"

panel.set_value(value_id, string_value)

assert panel._panel_client.enumerate_panels() == {"my_panel": (False, [value_id])}


def test___unopened_panel___get_unset_value___raises_exception(
fake_panel_channel: grpc.Channel,
Expand Down Expand Up @@ -192,3 +193,47 @@ def test___unsupported_type___set_value___raises(
value_id = "test_id"
with pytest.raises(TypeError):
panel.set_value(value_id, value_payload)


def test___open_panel___panel_is_open_and_in_memory(
fake_panel_channel: grpc.Channel,
) -> None:
panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel)
assert not is_panel_in_memory(panel)

panel.open_panel()

assert is_panel_in_memory(panel)
assert is_panel_open(panel)


def test___with_panel___opens_and_closes_panel(
fake_panel_channel: grpc.Channel,
) -> None:
panel = StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel)

with panel:
assert is_panel_in_memory(panel)
assert is_panel_open(panel)

assert is_panel_in_memory(panel)
assert not is_panel_open(panel)


def test___with_panel___set_value___gets_same_value(
fake_panel_channel: grpc.Channel,
) -> None:
with StreamlitPanel("my_panel", "path/to/script", grpc_channel=fake_panel_channel) as panel:
value_id = "test_id"
string_value = "test_value"
panel.set_value(value_id, string_value)

assert panel.get_value(value_id) == string_value


def is_panel_in_memory(panel: StreamlitPanel) -> bool:
return panel.panel_id in panel._panel_client.enumerate_panels().keys()


def is_panel_open(panel: StreamlitPanel) -> bool:
return panel._panel_client.enumerate_panels()[panel.panel_id][0]
Loading