diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3e99ede --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file 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 b9fb3ab..ccfbc30 100644 --- a/src/nipanel/__init__.py +++ b/src/nipanel/__init__.py @@ -1 +1,10 @@ """The NI Panel.""" + +from nipanel._panel import Panel +from nipanel._streamlit_panel import StreamlitPanel + +__all__ = ["Panel", "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 new file mode 100644 index 0000000..faa4acc --- /dev/null +++ b/src/nipanel/_panel.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import sys +from abc import ABC, abstractmethod +from types import TracebackType +from typing import Optional, Type, TYPE_CHECKING + +from ni.pythonpanel.v1.python_panel_service_pb2_grpc import PythonPanelServiceStub + +if TYPE_CHECKING: + if sys.version_info >= (3, 11): + from typing import Self + else: + from typing_extensions import Self + + +class Panel(ABC): + """This class allows you to connect to a panel and specify values for its controls.""" + + _stub: PythonPanelServiceStub | None + _panel_id: str + _panel_uri: str + + __slots__ = ["_stub", "_panel_id", "_panel_uri", "__weakref__"] + + def __init__(self, panel_id: str, panel_uri: str) -> None: + """Initialize the panel.""" + self._panel_id = panel_id + 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() + return self + + 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 + + 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 + self._resolve_service_location() + + 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) -> object: + """Get the value for a control on the panel. + + Args: + value_id: The id of the value + + Returns: + The value + """ + # 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: + """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 + + @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..344f20e --- /dev/null +++ b/src/nipanel/_streamlit_panel.py @@ -0,0 +1,23 @@ +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, panel_id: str, streamlit_script_uri: str) -> None: + """Create a panel using a Streamlit script for the user interface. + + Args: + 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__(panel_id, streamlit_script_uri) + + def _resolve_service_location(self) -> str: + # TODO: AB#3095680 - resolve to the Streamlit PythonPanelService + return "" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..7c0b89d --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for the `nipanel` package.""" diff --git a/tests/unit/test_panel.py b/tests/unit/test_panel.py new file mode 100644 index 0000000..ec14b35 --- /dev/null +++ b/tests/unit/test_panel.py @@ -0,0 +1,27 @@ +import nipanel + + +def test___streamlit_panel___has_panel_id_and_panel_uri() -> None: + 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("my_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.StreamlitPanel("my_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" 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