From 2af58bb96d2d413d34617434e4475a85ca4d9a1a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 Feb 2026 11:34:38 +0000 Subject: [PATCH 1/2] Ensure children can be updated if not added initially --- src/panel_reactflow/base.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/panel_reactflow/base.py b/src/panel_reactflow/base.py index 7c67bc0..7cf180e 100644 --- a/src/panel_reactflow/base.py +++ b/src/panel_reactflow/base.py @@ -1366,18 +1366,15 @@ def _get_children(self, data_model, doc, root, parent, comm) -> tuple[dict[str, children: dict[str, list[UIElement] | UIElement | None] = {} old_models: list[UIElement] = [] - if views: - views, view_models = self._get_child_model(views, doc, root, parent, comm) - children["_views"] = views - old_models += view_models - if node_editors: - editor_models, editor_old = self._get_child_model(node_editors, doc, root, parent, comm) - children["_node_editor_views"] = editor_models - old_models += editor_old - if edge_editors: - edge_models, edge_old = self._get_child_model(edge_editors, doc, root, parent, comm) - children["_edge_editor_views"] = edge_models - old_models += edge_old + views, view_models = self._get_child_model(views, doc, root, parent, comm) + children["_views"] = views + old_models += view_models + editor_models, editor_old = self._get_child_model(node_editors, doc, root, parent, comm) + children["_node_editor_views"] = editor_models + old_models += editor_old + edge_models, edge_old = self._get_child_model(edge_editors, doc, root, parent, comm) + children["_edge_editor_views"] = edge_models + old_models += edge_old for name in ("top_panel", "bottom_panel", "left_panel", "right_panel"): panels = list(getattr(self, name, []) or []) if panels: From a1eaccdb9ab39c51e636cbdac5b9427a5e084c27 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 12 Feb 2026 11:42:48 +0000 Subject: [PATCH 2/2] Add tests --- tests/test_api.py | 56 ------------------------------- tests/test_core.py | 83 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 78 insertions(+), 61 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 7ad1113..c38fb42 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -147,62 +147,6 @@ def test_reactflow_add_node_with_view() -> None: assert events[-1]["type"] == "node_added" -def test_view_idx_updates_on_remove_node(document, comm) -> None: - flow = ReactFlow() - flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "data": {}, "view": pn.pane.Markdown("A")}) - flow.add_node({"id": "n2", "position": {"x": 1, "y": 1}, "data": {}, "view": pn.pane.Markdown("B")}) - flow.add_node({"id": "n3", "position": {"x": 2, "y": 2}, "data": {}}) - - model = flow.get_root(document, comm=comm) - - flow.remove_node("n1") - - remaining = {node["id"]: node for node in model.data.nodes} - assert remaining["n2"]["data"]["view_idx"] == 0 - assert remaining["n3"]["data"].get("view_idx") is None - - -def test_reactflow_add_node_with_viewer(document, comm) -> None: - """Test that Viewer objects with __panel__() method work as node views.""" - - class MyViewer(pn.viewable.Viewer): - def __panel__(self): - return pn.pane.Markdown("Hello from Viewer!") - - flow = ReactFlow() - my_viewer = MyViewer() - flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Viewer Node", "data": {}, "view": my_viewer}) - - # This should not raise AttributeError about '_models' - _ = flow.get_root(document, comm=comm) - assert len(flow.nodes) == 1 - assert flow.nodes[0]["id"] == "n1" - - -def test_reactflow_add_node_with_arbitrary_object(document, comm) -> None: - """Test that arbitrary objects (e.g., HoloViews) work as node views via pn.panel(). - - This addresses issue #13 where objects without __panel__() method - (like HoloViews Curve objects) would raise AttributeError. - """ - - class MockPlot: - """Mock object simulating HoloViews/hvplot objects (no __panel__ method).""" - - def __repr__(self): - return "MockPlot(data)" - - flow = ReactFlow() - mock_plot = MockPlot() - flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Plot Node", "data": {}, "view": mock_plot}) - - # This should not raise AttributeError about '_models' - # The object should be converted via pn.panel() - _ = flow.get_root(document, comm=comm) - assert len(flow.nodes) == 1 - assert flow.nodes[0]["id"] == "n1" - - def test_reactflow_events_and_selection() -> None: flow = ReactFlow() events = [] diff --git a/tests/test_core.py b/tests/test_core.py index 1520e00..ba4eb9e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,8 +1,81 @@ -"""Core tests module.""" +"""Tests for ReactFlow model creation.""" -import panel_reactflow # noqa +from panel.pane import Markdown +from panel.viewable import Viewer +from panel_reactflow import ReactFlow -def test_example(): - """Test example.""" - assert panel_reactflow + +def test_reactflow_add_node_with_arbitrary_object(document, comm) -> None: + """Test that arbitrary objects (e.g., HoloViews) work as node views via pn.panel(). + + This addresses issue #13 where objects without __panel__() method + (like HoloViews Curve objects) would raise AttributeError. + """ + + class MockPlot: + """Mock object simulating HoloViews/hvplot objects (no __panel__ method).""" + + def __repr__(self): + return "MockPlot(data)" + + flow = ReactFlow() + mock_plot = MockPlot() + flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Plot Node", "data": {}, "view": mock_plot}) + + # This should not raise AttributeError about '_models' + # The object should be converted via pn.panel() + _ = flow.get_root(document, comm=comm) + assert len(flow.nodes) == 1 + assert flow.nodes[0]["id"] == "n1" + + +def test_view_idx_updates_on_remove_node(document, comm) -> None: + flow = ReactFlow() + flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "data": {}, "view": Markdown("A")}) + flow.add_node({"id": "n2", "position": {"x": 1, "y": 1}, "data": {}, "view": Markdown("B")}) + flow.add_node({"id": "n3", "position": {"x": 2, "y": 2}, "data": {}}) + + model = flow.get_root(document, comm=comm) + + flow.remove_node("n1") + + remaining = {node["id"]: node for node in model.data.nodes} + assert remaining["n2"]["data"]["view_idx"] == 0 + assert remaining["n3"]["data"].get("view_idx") is None + + +def test_reactflow_add_node_with_viewer(document, comm) -> None: + """Test that Viewer objects with __panel__() method work as node views.""" + + class MyViewer(Viewer): + def __panel__(self): + return Markdown("Hello from Viewer!") + + flow = ReactFlow() + my_viewer = MyViewer() + flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Viewer Node", "data": {}, "view": my_viewer}) + + # This should not raise AttributeError about '_models' + _ = flow.get_root(document, comm=comm) + assert len(flow.nodes) == 1 + assert flow.nodes[0]["id"] == "n1" + + +def test_reactflow_add_node_dynamically_creates_views(document, comm): + flow = ReactFlow() + model = flow.get_root(document, comm=comm) + assert model.children == [ + "_views", + "_node_editor_views", + "_edge_editor_views", + "top_panel", + "bottom_panel", + "left_panel", + "right_panel", + ] + + flow.add_node({"id": "n1", "position": {"x": 0, "y": 0}, "label": "Viewer Node", "data": {}, "view": Markdown("foo")}) + + assert len(model.data._views) == 1 + assert len(model.data._node_editor_views) == 1