From 4634cbc47fea3046f04911fbf63d4226ea69877c Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 21 Apr 2025 11:01:43 -0500 Subject: [PATCH 1/9] first draft --- src/nipanel/nipanel.py | 74 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/nipanel/nipanel.py diff --git a/src/nipanel/nipanel.py b/src/nipanel/nipanel.py new file mode 100644 index 0000000..09ae729 --- /dev/null +++ b/src/nipanel/nipanel.py @@ -0,0 +1,74 @@ +import uuid +import grpc +from google.protobuf import any_pb2 +import ni.pythonpanel.v1.python_panel_service_pb2 as python_panel_service_pb2 +import ni.pythonpanel.v1.python_panel_service_pb2_grpc as python_panel_service_pb2_grpc + +class NiPanel: + """ + This class allows you to access controls on the panel + """ + def __init__(self): + self._stub = None # will be a PythonPanelServiceStub + self.panel_uri = None + self.panel_id = None + + def __enter__(self): + self.connect() + + def __exit__(self): + self.disconnect() + + @classmethod + def streamlit_panel(cls, streamlit_script_path: str): + """ + Create a panel using a streamlit script for the user interface + + Args: + streamlit_script_path: The file path of the streamlit script + + Returns: + NiPanel: A new panel associated with the streamlit script + """ + panel = cls() + panel.panel_uri = streamlit_script_path + panel.panel_id = str(uuid.uuid4()) + return panel + + def connect(self): + """ + Connect to the panel and open it. + """ + # TODO: AB#3095680 - Use gRPC pool management from the Measurement Plugin SDK, create the _stub, and call _stub.Connect + pass + + def disconnect(self): + """ + Disconnect from the panel (does not close the panel). + """ + # TODO: AB#3095680 - Use gRPC pool management from the Measurement Plugin SDK, call _stub.Disconnect + pass + + def get_value(self, value_id: str): + """ + Get the value for a control on the panel + + Args: + value_id: The id of the value + + Returns: + object: The value + """ + # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type to return it + return "placeholder value" + + def set_value(self, value_id: str, value): + """ + Set the value for a control on the panel + + Args: + value_id: The id of the value + value: The value + """ + # TODO: AB#3095681 - Convert the value to an Any and pass it to _stub.SetValue + pass \ No newline at end of file From 9c901fc1891048f2d8d085df118c2ed5c71017e9 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 21 Apr 2025 15:24:42 -0500 Subject: [PATCH 2/9] add basic tests --- .vscode/settings.json | 7 +++++++ src/nipanel/__init__.py | 2 ++ src/nipanel/nipanel.py | 13 ++++++++----- tests/__init__.py | 1 + tests/unit/test_nipanel.py | 17 +++++++++++++++++ tests/unit/test_placeholder.py | 2 -- 6 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 tests/__init__.py create mode 100644 tests/unit/test_nipanel.py delete mode 100644 tests/unit/test_placeholder.py diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/src/nipanel/__init__.py b/src/nipanel/__init__.py index b9fb3ab..8b93945 100644 --- a/src/nipanel/__init__.py +++ b/src/nipanel/__init__.py @@ -1 +1,3 @@ """The NI Panel.""" +from nipanel.nipanel import NiPanel +__all__ = ["NiPanel"] \ No newline at end of file diff --git a/src/nipanel/nipanel.py b/src/nipanel/nipanel.py index 09ae729..92c2f6d 100644 --- a/src/nipanel/nipanel.py +++ b/src/nipanel/nipanel.py @@ -4,19 +4,22 @@ import ni.pythonpanel.v1.python_panel_service_pb2 as python_panel_service_pb2 import ni.pythonpanel.v1.python_panel_service_pb2_grpc as python_panel_service_pb2_grpc + class NiPanel: """ This class allows you to access controls on the panel """ + def __init__(self): - self._stub = None # will be a PythonPanelServiceStub + self._stub = None # will be a PythonPanelServiceStub self.panel_uri = None self.panel_id = None def __enter__(self): self.connect() + return self - def __exit__(self): + def __exit__(self, exc_type, exc_value, exc_traceback): self.disconnect() @classmethod @@ -35,10 +38,10 @@ def streamlit_panel(cls, streamlit_script_path: str): panel.panel_id = str(uuid.uuid4()) return panel - def connect(self): + def connect(self): """ Connect to the panel and open it. - """ + """ # TODO: AB#3095680 - Use gRPC pool management from the Measurement Plugin SDK, create the _stub, and call _stub.Connect pass @@ -71,4 +74,4 @@ def set_value(self, value_id: str, value): value: The value """ # TODO: AB#3095681 - Convert the value to an Any and pass it to _stub.SetValue - pass \ No newline at end of file + pass diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..92809eb --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the `nipanel` package.""" \ No newline at end of file diff --git a/tests/unit/test_nipanel.py b/tests/unit/test_nipanel.py new file mode 100644 index 0000000..5539ffa --- /dev/null +++ b/tests/unit/test_nipanel.py @@ -0,0 +1,17 @@ +from nipanel import NiPanel + +def test_streamlit_panel() -> None: + panel = NiPanel.streamlit_panel("path/to/script") + assert panel.panel_uri == "path/to/script" + assert panel.panel_id is not None + panel.connect() # not implemented, but should not raise an error + panel.set_value("test_id", "test_value") # not implemented, but should not raise an error + assert panel.get_value("test_id") == "placeholder value" # not implemented, but should not raise an error + panel.disconnect() # not implemented, but should not raise an error + +def test_with_streamlit_panel() -> None: + with NiPanel.streamlit_panel("path/to/script") as panel: + assert panel.panel_uri == "path/to/script" + assert panel.panel_id is not None + panel.set_value("test_id", "test_value") # not implemented, but should not raise an error + assert panel.get_value("test_id") == "placeholder value" # not implemented, but should not raise an error \ No newline at end of file diff --git a/tests/unit/test_placeholder.py b/tests/unit/test_placeholder.py deleted file mode 100644 index 60d5ce3..0000000 --- a/tests/unit/test_placeholder.py +++ /dev/null @@ -1,2 +0,0 @@ -def test___placeholder() -> None: - pass From 5511c585d3b80dd7efada6136522069aeafc3c0f Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Mon, 21 Apr 2025 15:46:24 -0500 Subject: [PATCH 3/9] cleanup --- src/nipanel/__init__.py | 4 ++- src/nipanel/nipanel.py | 64 +++++++++++++++++++------------------- tests/__init__.py | 2 +- tests/unit/test_nipanel.py | 14 +++++---- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/nipanel/__init__.py b/src/nipanel/__init__.py index 8b93945..61112b4 100644 --- a/src/nipanel/__init__.py +++ b/src/nipanel/__init__.py @@ -1,3 +1,5 @@ """The NI Panel.""" + from nipanel.nipanel import NiPanel -__all__ = ["NiPanel"] \ No newline at end of file + +__all__ = ["NiPanel"] diff --git a/src/nipanel/nipanel.py b/src/nipanel/nipanel.py index 92c2f6d..8038c6d 100644 --- a/src/nipanel/nipanel.py +++ b/src/nipanel/nipanel.py @@ -1,31 +1,37 @@ +"""NI Panel.""" + import uuid -import grpc -from google.protobuf import any_pb2 -import ni.pythonpanel.v1.python_panel_service_pb2 as python_panel_service_pb2 -import ni.pythonpanel.v1.python_panel_service_pb2_grpc as python_panel_service_pb2_grpc +from types import TracebackType +from typing import Optional, Type class NiPanel: - """ - This class allows you to access controls on the panel - """ + """This class allows you to access controls on the panel.""" - def __init__(self): + def __init__(self) -> None: + """Initialize the NiPanel instance.""" self._stub = None # will be a PythonPanelServiceStub - self.panel_uri = None - self.panel_id = None + self.panel_uri = "" + self.panel_id = "" - def __enter__(self): + def __enter__(self) -> "NiPanel": + """Enter the runtime context related to this object.""" self.connect() return self - def __exit__(self, exc_type, exc_value, exc_traceback): + def __exit__( + self, + exctype: Optional[Type[BaseException]], + excinst: Optional[BaseException], + exctb: Optional[TracebackType], + ) -> Optional[bool]: + """Exit the runtime context related to this object.""" self.disconnect() + return None @classmethod - def streamlit_panel(cls, streamlit_script_path: str): - """ - Create a panel using a streamlit script for the user interface + def streamlit_panel(cls, streamlit_script_path: str) -> "NiPanel": + """Create a panel using a streamlit script for the user interface. Args: streamlit_script_path: The file path of the streamlit script @@ -38,23 +44,18 @@ def streamlit_panel(cls, streamlit_script_path: str): panel.panel_id = str(uuid.uuid4()) return panel - def connect(self): - """ - Connect to the panel and open it. - """ - # TODO: AB#3095680 - Use gRPC pool management from the Measurement Plugin SDK, create the _stub, and call _stub.Connect + def connect(self) -> None: + """Connect to the panel and open it.""" + # TODO: AB#3095680 - Use gRPC pool management, create the _stub, and call _stub.Connect pass - def disconnect(self): - """ - Disconnect from the panel (does not close the panel). - """ - # TODO: AB#3095680 - Use gRPC pool management from the Measurement Plugin SDK, call _stub.Disconnect + def disconnect(self) -> None: + """Disconnect from the panel (does not close the panel).""" + # TODO: AB#3095680 - Use gRPC pool management, call _stub.Disconnect pass - def get_value(self, value_id: str): - """ - Get the value for a control on the panel + def get_value(self, value_id: str) -> object: + """Get the value for a control on the panel. Args: value_id: The id of the value @@ -62,12 +63,11 @@ def get_value(self, value_id: str): Returns: object: The value """ - # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type to return it + # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type return "placeholder value" - def set_value(self, value_id: str, value): - """ - Set the value for a control on the panel + def set_value(self, value_id: str, value: object) -> None: + """Set the value for a control on the panel. Args: value_id: The id of the value diff --git a/tests/__init__.py b/tests/__init__.py index 92809eb..7c0b89d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Tests for the `nipanel` package.""" \ No newline at end of file +"""Tests for the `nipanel` package.""" diff --git a/tests/unit/test_nipanel.py b/tests/unit/test_nipanel.py index 5539ffa..decc781 100644 --- a/tests/unit/test_nipanel.py +++ b/tests/unit/test_nipanel.py @@ -1,17 +1,19 @@ from nipanel import NiPanel + def test_streamlit_panel() -> None: panel = NiPanel.streamlit_panel("path/to/script") assert panel.panel_uri == "path/to/script" assert panel.panel_id is not None - panel.connect() # not implemented, but should not raise an error - panel.set_value("test_id", "test_value") # not implemented, but should not raise an error - assert panel.get_value("test_id") == "placeholder value" # not implemented, but should not raise an error - panel.disconnect() # not implemented, but should not raise an error + panel.connect() + panel.set_value("test_id", "test_value") + assert panel.get_value("test_id") == "placeholder value" + panel.disconnect() + def test_with_streamlit_panel() -> None: with NiPanel.streamlit_panel("path/to/script") as panel: assert panel.panel_uri == "path/to/script" assert panel.panel_id is not None - panel.set_value("test_id", "test_value") # not implemented, but should not raise an error - assert panel.get_value("test_id") == "placeholder value" # not implemented, but should not raise an error \ No newline at end of file + panel.set_value("test_id", "test_value") + assert panel.get_value("test_id") == "placeholder value" From eaf0bc357ac3c661d81b005ed888e09f6f509de0 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 22 Apr 2025 10:11:14 -0500 Subject: [PATCH 4/9] address feedback --- src/nipanel/__init__.py | 7 +++-- src/nipanel/{nipanel.py => _panel.py} | 39 ++++++++++++++++----------- tests/unit/test_nipanel.py | 19 ------------- tests/unit/test_panel.py | 35 ++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 36 deletions(-) rename src/nipanel/{nipanel.py => _panel.py} (67%) delete mode 100644 tests/unit/test_nipanel.py create mode 100644 tests/unit/test_panel.py diff --git a/src/nipanel/__init__.py b/src/nipanel/__init__.py index 61112b4..03263f3 100644 --- a/src/nipanel/__init__.py +++ b/src/nipanel/__init__.py @@ -1,5 +1,8 @@ """The NI Panel.""" -from nipanel.nipanel import NiPanel +from nipanel._panel import Panel -__all__ = ["NiPanel"] +__all__ = ["Panel"] + +# Hide that it was defined in a helper file +Panel.__module__ = __name__ diff --git a/src/nipanel/nipanel.py b/src/nipanel/_panel.py similarity index 67% rename from src/nipanel/nipanel.py rename to src/nipanel/_panel.py index 8038c6d..4cd484d 100644 --- a/src/nipanel/nipanel.py +++ b/src/nipanel/_panel.py @@ -1,20 +1,29 @@ -"""NI Panel.""" +from __future__ import annotations +import sys import uuid from types import TracebackType -from typing import Optional, Type +from typing import Optional, Type, TYPE_CHECKING +from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub -class NiPanel: - """This class allows you to access controls on the panel.""" +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self - def __init__(self) -> None: - """Initialize the NiPanel instance.""" - self._stub = None # will be a PythonPanelServiceStub - self.panel_uri = "" - self.panel_id = "" - def __enter__(self) -> "NiPanel": +class Panel: + """This class allows you to connect to a panel and specify values for its controls.""" + + _stub: PythonPanelServiceStub | None + _panel_uri: str + _panel_id: str + + __slots__ = ["_stub", "_panel_uri", "_panel_id", "__weakref__"] + + def __enter__(self) -> Self: """Enter the runtime context related to this object.""" self.connect() return self @@ -30,18 +39,18 @@ def __exit__( return None @classmethod - def streamlit_panel(cls, streamlit_script_path: str) -> "NiPanel": + def streamlit_panel(cls, streamlit_script_path: str) -> Self: """Create a panel using a streamlit script for the user interface. Args: streamlit_script_path: The file path of the streamlit script Returns: - NiPanel: A new panel associated with the streamlit script + A new panel associated with the streamlit script """ panel = cls() - panel.panel_uri = streamlit_script_path - panel.panel_id = str(uuid.uuid4()) + panel._panel_uri = streamlit_script_path + panel._panel_id = str(uuid.uuid4()) return panel def connect(self) -> None: @@ -61,7 +70,7 @@ def get_value(self, value_id: str) -> object: value_id: The id of the value Returns: - object: The value + The value """ # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type return "placeholder value" diff --git a/tests/unit/test_nipanel.py b/tests/unit/test_nipanel.py deleted file mode 100644 index decc781..0000000 --- a/tests/unit/test_nipanel.py +++ /dev/null @@ -1,19 +0,0 @@ -from nipanel import NiPanel - - -def test_streamlit_panel() -> None: - panel = NiPanel.streamlit_panel("path/to/script") - assert panel.panel_uri == "path/to/script" - assert panel.panel_id is not None - panel.connect() - panel.set_value("test_id", "test_value") - assert panel.get_value("test_id") == "placeholder value" - panel.disconnect() - - -def test_with_streamlit_panel() -> None: - with NiPanel.streamlit_panel("path/to/script") as panel: - assert panel.panel_uri == "path/to/script" - assert panel.panel_id is not None - panel.set_value("test_id", "test_value") - assert panel.get_value("test_id") == "placeholder value" diff --git a/tests/unit/test_panel.py b/tests/unit/test_panel.py new file mode 100644 index 0000000..d1d6cbd --- /dev/null +++ b/tests/unit/test_panel.py @@ -0,0 +1,35 @@ +import nipanel + + +def test___streamlit_panel___uri_and_id_are_set() -> None: + panel = nipanel.Panel.streamlit_panel("path/to/script") + + assert panel._panel_uri == "path/to/script" + assert panel._panel_id is not None + + +def test___two_panels___have_different_ids() -> None: + panel1 = nipanel.Panel.streamlit_panel("path/to/script1") + panel2 = nipanel.Panel.streamlit_panel("path/to/script2") + + assert panel1._panel_id != panel2._panel_id + + +def test___connected_panel___set_value___gets_same_value() -> None: + panel = nipanel.Panel.streamlit_panel("path/to/script") + panel.connect() + + panel.set_value("test_id", "test_value") + + # TODO: AB#3095681 - change asserted value to test_value + assert panel.get_value("test_id") == "placeholder value" + panel.disconnect() + + +def test___with_panel___set_value___gets_same_value() -> None: + with nipanel.Panel.streamlit_panel("path/to/script") as panel: + + panel.set_value("test_id", "test_value") + + # TODO: AB#3095681 - change asserted value to test_value + assert panel.get_value("test_id") == "placeholder value" From 9b5c99fa11774a791b59e04cee5b66dbbbdb4bce Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 22 Apr 2025 10:56:32 -0500 Subject: [PATCH 5/9] refactor to use _StreamlitPanel subclass --- src/nipanel/_panel.py | 74 ++++++++++++++++++++++++++-------------- tests/unit/test_panel.py | 14 -------- 2 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/nipanel/_panel.py b/src/nipanel/_panel.py index 4cd484d..0e41866 100644 --- a/src/nipanel/_panel.py +++ b/src/nipanel/_panel.py @@ -2,6 +2,7 @@ import sys import uuid +from abc import ABC, abstractmethod from types import TracebackType from typing import Optional, Type, TYPE_CHECKING @@ -14,15 +15,9 @@ from typing_extensions import Self -class Panel: +class Panel(ABC): """This class allows you to connect to a panel and specify values for its controls.""" - _stub: PythonPanelServiceStub | None - _panel_uri: str - _panel_id: str - - __slots__ = ["_stub", "_panel_uri", "_panel_id", "__weakref__"] - def __enter__(self) -> Self: """Enter the runtime context related to this object.""" self.connect() @@ -38,31 +33,17 @@ def __exit__( self.disconnect() return None - @classmethod - def streamlit_panel(cls, streamlit_script_path: str) -> Self: - """Create a panel using a streamlit script for the user interface. - - Args: - streamlit_script_path: The file path of the streamlit script - - Returns: - A new panel associated with the streamlit script - """ - panel = cls() - panel._panel_uri = streamlit_script_path - panel._panel_id = str(uuid.uuid4()) - return panel - + @abstractmethod def connect(self) -> None: """Connect to the panel and open it.""" - # TODO: AB#3095680 - Use gRPC pool management, create the _stub, and call _stub.Connect pass + @abstractmethod def disconnect(self) -> None: """Disconnect from the panel (does not close the panel).""" - # TODO: AB#3095680 - Use gRPC pool management, call _stub.Disconnect pass + @abstractmethod def get_value(self, value_id: str) -> object: """Get the value for a control on the panel. @@ -72,9 +53,9 @@ def get_value(self, value_id: str) -> object: Returns: The value """ - # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type - return "placeholder value" + pass + @abstractmethod def set_value(self, value_id: str, value: object) -> None: """Set the value for a control on the panel. @@ -82,5 +63,46 @@ def set_value(self, value_id: str, value: object) -> None: value_id: The id of the value value: The value """ + pass + + @classmethod + def streamlit_panel(cls, streamlit_script_path: str) -> Panel: + """Create a panel using a streamlit script for the user interface. + + Args: + streamlit_script_path: The file path of the Streamlit script. + + Returns: + A new StreamlitPanel instance. + """ + return _StreamlitPanel(streamlit_script_path) + + +class _StreamlitPanel(Panel): + + _stub: PythonPanelServiceStub | None + _streamlit_script_uri: str + _panel_id: str + + __slots__ = ["_stub", "_streamlit_script_uri", "_panel_id"] + + def __init__(self, streamlit_script_uri: str): + self._streamlit_script_uri = streamlit_script_uri + self._panel_id = str(uuid.uuid4()) + self._stub = None # Initialize the gRPC stub + + def connect(self) -> None: + # TODO: AB#3095680 - Use gRPC pool management, create the _stub, and call _stub.Connect + pass + + def disconnect(self) -> None: + # TODO: AB#3095680 - Use gRPC pool management, call _stub.Disconnect + pass + + def get_value(self, value_id: str) -> object: + # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type + return "placeholder value" + + def set_value(self, value_id: str, value: object) -> None: # TODO: AB#3095681 - Convert the value to an Any and pass it to _stub.SetValue pass diff --git a/tests/unit/test_panel.py b/tests/unit/test_panel.py index d1d6cbd..60fb41b 100644 --- a/tests/unit/test_panel.py +++ b/tests/unit/test_panel.py @@ -1,20 +1,6 @@ import nipanel -def test___streamlit_panel___uri_and_id_are_set() -> None: - panel = nipanel.Panel.streamlit_panel("path/to/script") - - assert panel._panel_uri == "path/to/script" - assert panel._panel_id is not None - - -def test___two_panels___have_different_ids() -> None: - panel1 = nipanel.Panel.streamlit_panel("path/to/script1") - panel2 = nipanel.Panel.streamlit_panel("path/to/script2") - - assert panel1._panel_id != panel2._panel_id - - def test___connected_panel___set_value___gets_same_value() -> None: panel = nipanel.Panel.streamlit_panel("path/to/script") panel.connect() From 5034907a91d33fbc2ad9c75c05df85c051aa38f5 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 22 Apr 2025 14:23:53 -0500 Subject: [PATCH 6/9] refactor to make StreamlitPanel public instead of Panel --- .vscode/settings.json | 2 +- pyproject.toml | 4 ++ src/nipanel/__init__.py | 6 +-- src/nipanel/_panel.py | 68 ++++++++++----------------------- src/nipanel/_streamlit_panel.py | 22 +++++++++++ tests/unit/test_panel.py | 16 +++++++- 6 files changed, 65 insertions(+), 53 deletions(-) create mode 100644 src/nipanel/_streamlit_panel.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b38853..3e99ede 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "python.testing.pytestArgs": [ - "tests" + "." ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true diff --git a/pyproject.toml b/pyproject.toml index 2705f90..35e76f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,3 +52,7 @@ ignore_missing_imports = true skips = [ "B101", # assert_used ] + +[tool.pytest.ini_options] +addopts = "--doctest-modules --strict-markers" +testpaths = ["src/nipanel", "tests"] \ No newline at end of file diff --git a/src/nipanel/__init__.py b/src/nipanel/__init__.py index 03263f3..918cbf4 100644 --- a/src/nipanel/__init__.py +++ b/src/nipanel/__init__.py @@ -1,8 +1,8 @@ """The NI Panel.""" -from nipanel._panel import Panel +from nipanel._streamlit_panel import StreamlitPanel -__all__ = ["Panel"] +__all__ = ["StreamlitPanel"] # Hide that it was defined in a helper file -Panel.__module__ = __name__ +StreamlitPanel.__module__ = __name__ diff --git a/src/nipanel/_panel.py b/src/nipanel/_panel.py index 0e41866..f3ce560 100644 --- a/src/nipanel/_panel.py +++ b/src/nipanel/_panel.py @@ -18,6 +18,17 @@ class Panel(ABC): """This class allows you to connect to a panel and specify values for its controls.""" + _stub: PythonPanelServiceStub | None + _panel_uri: str + _panel_id: str + + __slots__ = ["_stub", "_panel_uri", "_panel_id", "__weakref__"] + + def __init__(self, panel_uri: str) -> None: + """Initialize the panel.""" + self._panel_uri = panel_uri + self._panel_id = str(uuid.uuid4()) + def __enter__(self) -> Self: """Enter the runtime context related to this object.""" self.connect() @@ -33,17 +44,16 @@ def __exit__( self.disconnect() return None - @abstractmethod def connect(self) -> None: """Connect to the panel and open it.""" - pass + # TODO: AB#3095680 - Use gRPC pool management, create the _stub, and call _stub.Connect + self._resolve_service_location() - @abstractmethod def disconnect(self) -> None: """Disconnect from the panel (does not close the panel).""" + # TODO: AB#3095680 - Use gRPC pool management, call _stub.Disconnect pass - @abstractmethod def get_value(self, value_id: str) -> object: """Get the value for a control on the panel. @@ -53,9 +63,9 @@ def get_value(self, value_id: str) -> object: Returns: The value """ - pass + # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type + return "placeholder value" - @abstractmethod def set_value(self, value_id: str, value: object) -> None: """Set the value for a control on the panel. @@ -63,46 +73,10 @@ def set_value(self, value_id: str, value: object) -> None: value_id: The id of the value value: The value """ - pass - - @classmethod - def streamlit_panel(cls, streamlit_script_path: str) -> Panel: - """Create a panel using a streamlit script for the user interface. - - Args: - streamlit_script_path: The file path of the Streamlit script. - - Returns: - A new StreamlitPanel instance. - """ - return _StreamlitPanel(streamlit_script_path) - - -class _StreamlitPanel(Panel): - - _stub: PythonPanelServiceStub | None - _streamlit_script_uri: str - _panel_id: str - - __slots__ = ["_stub", "_streamlit_script_uri", "_panel_id"] - - def __init__(self, streamlit_script_uri: str): - self._streamlit_script_uri = streamlit_script_uri - self._panel_id = str(uuid.uuid4()) - self._stub = None # Initialize the gRPC stub - - def connect(self) -> None: - # TODO: AB#3095680 - Use gRPC pool management, create the _stub, and call _stub.Connect - pass - - def disconnect(self) -> None: - # TODO: AB#3095680 - Use gRPC pool management, call _stub.Disconnect - pass - - def get_value(self, value_id: str) -> object: - # TODO: AB#3095681 - get the Any from _stub.GetValue and convert it to the correct type - return "placeholder value" - - def set_value(self, value_id: str, value: object) -> None: # TODO: AB#3095681 - Convert the value to an Any and pass it to _stub.SetValue pass + + @abstractmethod + def _resolve_service_location(self) -> str: + """Resolve the service location for the panel.""" + raise NotImplementedError diff --git a/src/nipanel/_streamlit_panel.py b/src/nipanel/_streamlit_panel.py new file mode 100644 index 0000000..5177b85 --- /dev/null +++ b/src/nipanel/_streamlit_panel.py @@ -0,0 +1,22 @@ +from nipanel._panel import Panel + + +class StreamlitPanel(Panel): + """This class allows you to connect to a Streamlit panel and specify values for its controls.""" + + __slots__ = () + + def __init__(self, streamlit_script_uri: str) -> None: + """Create a panel using a Streamlit script for the user interface. + + Args: + streamlit_script_uri: The file path of the Streamlit script. + + Returns: + A new StreamlitPanel instance. + """ + super().__init__(streamlit_script_uri) + + def _resolve_service_location(self) -> str: + # TODO: AB#3095680 - resolve to the Streamlit PythonPanelService + return "" diff --git a/tests/unit/test_panel.py b/tests/unit/test_panel.py index 60fb41b..05f5d5e 100644 --- a/tests/unit/test_panel.py +++ b/tests/unit/test_panel.py @@ -1,8 +1,20 @@ import nipanel +def test___streamlit_panel___has_panel_id_and_panel_uri() -> None: + panel = nipanel.StreamlitPanel("path/to/script") + assert panel._panel_id is not None + assert panel._panel_uri == "path/to/script" + + +def test___two_panels___have_different_panel_ids() -> None: + panel1 = nipanel.StreamlitPanel("path/to/script1") + panel2 = nipanel.StreamlitPanel("path/to/script2") + assert panel1._panel_id != panel2._panel_id + + def test___connected_panel___set_value___gets_same_value() -> None: - panel = nipanel.Panel.streamlit_panel("path/to/script") + panel = nipanel.StreamlitPanel("path/to/script") panel.connect() panel.set_value("test_id", "test_value") @@ -13,7 +25,7 @@ def test___connected_panel___set_value___gets_same_value() -> None: def test___with_panel___set_value___gets_same_value() -> None: - with nipanel.Panel.streamlit_panel("path/to/script") as panel: + with nipanel.StreamlitPanel("path/to/script") as panel: panel.set_value("test_id", "test_value") From 166774db1167d0db6426baca42f4db45150bee35 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 22 Apr 2025 14:38:54 -0500 Subject: [PATCH 7/9] make Panel public also, for type hints --- src/nipanel/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nipanel/__init__.py b/src/nipanel/__init__.py index 918cbf4..ccfbc30 100644 --- a/src/nipanel/__init__.py +++ b/src/nipanel/__init__.py @@ -1,8 +1,10 @@ """The NI Panel.""" +from nipanel._panel import Panel from nipanel._streamlit_panel import StreamlitPanel -__all__ = ["StreamlitPanel"] +__all__ = ["Panel", "StreamlitPanel"] # Hide that it was defined in a helper file +Panel.__module__ = __name__ StreamlitPanel.__module__ = __name__ From 92d200d63dd23ca5e863ee08873bf9044e5634a1 Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Tue, 22 Apr 2025 17:03:20 -0500 Subject: [PATCH 8/9] panel_id should be specified by the user. Also, lets provide readonly accessors for that and panel_uri. --- src/nipanel/_panel.py | 15 ++++++++++++--- src/nipanel/_streamlit_panel.py | 5 +++-- tests/unit/test_panel.py | 16 +++++----------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/nipanel/_panel.py b/src/nipanel/_panel.py index f3ce560..e9635c7 100644 --- a/src/nipanel/_panel.py +++ b/src/nipanel/_panel.py @@ -1,7 +1,6 @@ from __future__ import annotations import sys -import uuid from abc import ABC, abstractmethod from types import TracebackType from typing import Optional, Type, TYPE_CHECKING @@ -24,10 +23,20 @@ class Panel(ABC): __slots__ = ["_stub", "_panel_uri", "_panel_id", "__weakref__"] - def __init__(self, panel_uri: str) -> None: + def __init__(self, panel_uri: str, panel_id: str) -> None: """Initialize the panel.""" self._panel_uri = panel_uri - self._panel_id = str(uuid.uuid4()) + self._panel_id = panel_id + + @property + def panel_uri(self) -> str: + """Read-only accessor for the panel URI.""" + return self._panel_uri + + @property + def panel_id(self) -> str: + """Read-only accessor for the panel ID.""" + return self._panel_id def __enter__(self) -> Self: """Enter the runtime context related to this object.""" diff --git a/src/nipanel/_streamlit_panel.py b/src/nipanel/_streamlit_panel.py index 5177b85..8a74a2c 100644 --- a/src/nipanel/_streamlit_panel.py +++ b/src/nipanel/_streamlit_panel.py @@ -6,16 +6,17 @@ class StreamlitPanel(Panel): __slots__ = () - def __init__(self, streamlit_script_uri: str) -> None: + def __init__(self, streamlit_script_uri: str, panel_id: str) -> None: """Create a panel using a Streamlit script for the user interface. Args: streamlit_script_uri: The file path of the Streamlit script. + panel_id: A unique identifier for the panel. Returns: A new StreamlitPanel instance. """ - super().__init__(streamlit_script_uri) + super().__init__(streamlit_script_uri, panel_id) def _resolve_service_location(self) -> str: # TODO: AB#3095680 - resolve to the Streamlit PythonPanelService diff --git a/tests/unit/test_panel.py b/tests/unit/test_panel.py index 05f5d5e..49126ad 100644 --- a/tests/unit/test_panel.py +++ b/tests/unit/test_panel.py @@ -2,19 +2,13 @@ def test___streamlit_panel___has_panel_id_and_panel_uri() -> None: - panel = nipanel.StreamlitPanel("path/to/script") - assert panel._panel_id is not None - assert panel._panel_uri == "path/to/script" - - -def test___two_panels___have_different_panel_ids() -> None: - panel1 = nipanel.StreamlitPanel("path/to/script1") - panel2 = nipanel.StreamlitPanel("path/to/script2") - assert panel1._panel_id != panel2._panel_id + panel = nipanel.StreamlitPanel("path/to/script", "my_panel") + assert panel.panel_uri == "path/to/script" + assert panel.panel_id == "my_panel" def test___connected_panel___set_value___gets_same_value() -> None: - panel = nipanel.StreamlitPanel("path/to/script") + panel = nipanel.StreamlitPanel("path/to/script", "my_panel") panel.connect() panel.set_value("test_id", "test_value") @@ -25,7 +19,7 @@ def test___connected_panel___set_value___gets_same_value() -> None: def test___with_panel___set_value___gets_same_value() -> None: - with nipanel.StreamlitPanel("path/to/script") as panel: + with nipanel.StreamlitPanel("path/to/script", "my_panel") as panel: panel.set_value("test_id", "test_value") From f4d4a52f594a945729e6fd20abe1c8baeb02756a Mon Sep 17 00:00:00 2001 From: Mike Prosser Date: Wed, 23 Apr 2025 09:20:53 -0500 Subject: [PATCH 9/9] put panel_id berfore panel_uri --- src/nipanel/_panel.py | 18 +++++++++--------- src/nipanel/_streamlit_panel.py | 6 +++--- tests/unit/test_panel.py | 8 ++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/nipanel/_panel.py b/src/nipanel/_panel.py index e9635c7..faa4acc 100644 --- a/src/nipanel/_panel.py +++ b/src/nipanel/_panel.py @@ -18,26 +18,26 @@ class Panel(ABC): """This class allows you to connect to a panel and specify values for its controls.""" _stub: PythonPanelServiceStub | None - _panel_uri: str _panel_id: str + _panel_uri: str - __slots__ = ["_stub", "_panel_uri", "_panel_id", "__weakref__"] + __slots__ = ["_stub", "_panel_id", "_panel_uri", "__weakref__"] - def __init__(self, panel_uri: str, panel_id: str) -> None: + def __init__(self, panel_id: str, panel_uri: str) -> None: """Initialize the panel.""" - self._panel_uri = panel_uri self._panel_id = panel_id - - @property - def panel_uri(self) -> str: - """Read-only accessor for the panel URI.""" - return self._panel_uri + self._panel_uri = panel_uri @property def panel_id(self) -> str: """Read-only accessor for the panel ID.""" return self._panel_id + @property + 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.connect() diff --git a/src/nipanel/_streamlit_panel.py b/src/nipanel/_streamlit_panel.py index 8a74a2c..344f20e 100644 --- a/src/nipanel/_streamlit_panel.py +++ b/src/nipanel/_streamlit_panel.py @@ -6,17 +6,17 @@ class StreamlitPanel(Panel): __slots__ = () - def __init__(self, streamlit_script_uri: str, panel_id: str) -> None: + def __init__(self, panel_id: str, streamlit_script_uri: str) -> None: """Create a panel using a Streamlit script for the user interface. Args: - streamlit_script_uri: The file path of the Streamlit script. panel_id: A unique identifier for the panel. + streamlit_script_uri: The file path of the Streamlit script. Returns: A new StreamlitPanel instance. """ - super().__init__(streamlit_script_uri, panel_id) + super().__init__(panel_id, streamlit_script_uri) def _resolve_service_location(self) -> str: # TODO: AB#3095680 - resolve to the Streamlit PythonPanelService diff --git a/tests/unit/test_panel.py b/tests/unit/test_panel.py index 49126ad..ec14b35 100644 --- a/tests/unit/test_panel.py +++ b/tests/unit/test_panel.py @@ -2,13 +2,13 @@ def test___streamlit_panel___has_panel_id_and_panel_uri() -> None: - panel = nipanel.StreamlitPanel("path/to/script", "my_panel") - assert panel.panel_uri == "path/to/script" + panel = nipanel.StreamlitPanel("my_panel", "path/to/script") assert panel.panel_id == "my_panel" + assert panel.panel_uri == "path/to/script" def test___connected_panel___set_value___gets_same_value() -> None: - panel = nipanel.StreamlitPanel("path/to/script", "my_panel") + panel = nipanel.StreamlitPanel("my_panel", "path/to/script") panel.connect() panel.set_value("test_id", "test_value") @@ -19,7 +19,7 @@ def test___connected_panel___set_value___gets_same_value() -> None: def test___with_panel___set_value___gets_same_value() -> None: - with nipanel.StreamlitPanel("path/to/script", "my_panel") as panel: + with nipanel.StreamlitPanel("my_panel", "path/to/script") as panel: panel.set_value("test_id", "test_value")