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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,6 @@ cython_debug/

# PyPI configuration file
.pypirc

# VS Code settings
.vscode/launch.json
44 changes: 44 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,50 @@ poetry run sphinx-build docs docs/_build --builder html --fail-on-warning
start docs\_build\index.html
```

# Debugging on the streamlit side

Debugging the measurement script can be done using standard Python debugging techniques. However, debugging the Streamlit script—or any code invoked by the Streamlit script—is more complex because it runs in a separate process launched by the PythonPanelServer. To debug the Streamlit script, you can use debugpy to attach the Visual Studio Code debugger as follows:

## Instrument Streamlit script to debug

To enable debugpy debugging, include this code in your streamlit script:

```python
import debugpy # type: ignore

try:
debugpy.listen(("localhost", 5678))
debugpy.wait_for_client()
except RuntimeError as e:
if "debugpy.listen() has already been called on this process" not in str(e):
raise
```

The `debugpy.listen()` function opens a port that allows the debugger to attach to the running process. You can specify any available port, as long as it matches the port configured in the launch.json file shown below. Since calling listen() more than once will raise an exception, it is wrapped in a try block to prevent the script from crashing if it is rerun.

The `debugpy.wait_for_client()` function pauses script execution until the debugger is attached. This is helpful if you need to debug initialization code, but you can omit this line if it is not required.

The `import debugpy` statement includes a type suppression comment to satisfy mypy.

## Add debugpy configuration in launch.json

You will also need this configuration in your launch.json:

```json
{
"name": "Attach to Streamlit at localhost:5678",
"type": "debugpy",
"request": "attach",
"connect": {
"host": "localhost",
"port": 5678
},
"justMyCode": false
}
```

After running your measurement script and allowing the PythonPanelServer to launch Streamlit with your Streamlit script, you can attach the debugger by clicking the **Attach to Streamlit at localhost:5678** button in the VS Code **Run and Debug** tab. Once attached, you can set breakpoints and use all standard debugging features in your Streamlit script, as well as in any nipanel code invoked by the Streamlit script.

# Developer Certificate of Origin (DCO)

Developer's Certificate of Origin 1.1
Expand Down
2 changes: 1 addition & 1 deletion examples/sample/sample_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import nipanel

panel = nipanel.StreamlitPanelValueAccessor(panel_id="sample_panel")
panel = nipanel.initialize_panel()

st.title("Sample Panel")

Expand Down
193 changes: 114 additions & 79 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ni-measurement-plugin-sdk = {version=">=2.3"}
typing-extensions = ">=4.13.2"
streamlit = ">=1.24"
nitypes = {version=">=0.1.0dev1", allow-prereleases=true}
debugpy = "^1.8.1"

[tool.poetry.group.dev.dependencies]
types-grpcio = ">=1.0"
Expand Down
3 changes: 2 additions & 1 deletion src/nipanel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

from nipanel._panel import Panel
from nipanel._streamlit_panel import StreamlitPanel
from nipanel._streamlit_panel_initializer import initialize_panel
from nipanel._streamlit_panel_value_accessor import StreamlitPanelValueAccessor

__all__ = ["Panel", "StreamlitPanel", "StreamlitPanelValueAccessor"]
__all__ = ["Panel", "StreamlitPanel", "StreamlitPanelValueAccessor", "initialize_panel"]

# Hide that it was defined in a helper file
Panel.__module__ = __name__
Expand Down
37 changes: 37 additions & 0 deletions src/nipanel/_streamlit_panel_initializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from typing import cast

import streamlit as st

from nipanel._streamlit_panel_value_accessor import StreamlitPanelValueAccessor

PANEL_ACCESSOR_KEY = "StreamlitPanelValueAccessor"


def initialize_panel() -> StreamlitPanelValueAccessor:
"""Initialize and return the Streamlit panel value accessor.

This function retrieves the Streamlit panel value accessor for the current Streamlit script.
It is typically used within a Streamlit script to access and manipulate panel values.
The accessor will be cached in the Streamlit session state to ensure that it is reused across
reruns of the script.

Returns:
A StreamlitPanelValueAccessor instance for the current panel.
"""
if PANEL_ACCESSOR_KEY not in st.session_state:
st.session_state[PANEL_ACCESSOR_KEY] = _initialize_panel_from_base_path()

panel = cast(StreamlitPanelValueAccessor, st.session_state[PANEL_ACCESSOR_KEY])
# TODO: declare the refresh component here
return panel


def _initialize_panel_from_base_path() -> StreamlitPanelValueAccessor:
"""Validate and parse the Streamlit base URL path and return a StreamlitPanelValueAccessor."""
base_url_path = st.get_option("server.baseUrlPath")
if not base_url_path.startswith("/"):
raise ValueError("Invalid or missing Streamlit server.baseUrlPath option.")
panel_id = base_url_path.split("/")[-1]
if not panel_id:
raise ValueError(f"Panel ID is empty in baseUrlPath: '{base_url_path}'")
return StreamlitPanelValueAccessor(panel_id)