From 75d99ba7ae789c0e60acb5c4ca38f3448b766b31 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 28 Sep 2026 17:43:58 +0200 Subject: [PATCH 1/2] fix: Coerce node and edge types before syncing updates --- docs/releases.md | 8 ++++++++ src/panel_reactflow/base.py | 18 +++++++++++------- tests/test_core.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/docs/releases.md b/docs/releases.md index fb7df46..06d1baa 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -73,6 +73,14 @@ an error boundary keeps a malformed graph from blanking the canvas. `nodes` and `edges` once ([#72](https://github.com/panel-extensions/panel-reactflow/pull/72)). +- **Serialization error when updating `node_types`** — assigning + `NodeType` or `EdgeType` descriptors with a `schema` class after + construction synced them to the browser before they were converted to + JSON descriptors, so the server failed with `can't serialize + ParameterizedMetaclass`. They are now converted before syncing, and + updating `node_types` and `edge_types` together no longer raises a + `TypeError`. + ## Version 0.4.1 A small enhancement release adding viewport zoom controls. diff --git a/src/panel_reactflow/base.py b/src/panel_reactflow/base.py index 9958121..c5b7523 100644 --- a/src/panel_reactflow/base.py +++ b/src/panel_reactflow/base.py @@ -2277,8 +2277,12 @@ def _process_param_change(self, params): params["nodes"] = nodes if "edges" in params: params["edges"] = [self._edge_payload(edge) for edge in params["edges"]] - # node_types / edge_types are now JSON-serializable descriptors - # and intentionally synced to the frontend. + # Panel syncs a new value before `_normalize_specs` replaces it, so + # schema classes must be coerced here to stay out of the payload. + if "node_types" in params: + params["node_types"] = _coerce_spec_map(params["node_types"]) + if "edge_types" in params: + params["edge_types"] = _coerce_spec_map(params["edge_types"], edge=True) # Pop Python-only editor registries and internal state. params.pop("node_editors", None) params.pop("edge_editors", None) @@ -3642,11 +3646,11 @@ def _update_selection_from_graph(self, *_: param.parameterized.Event) -> None: {"type": "selection_changed", "nodes": selection["nodes"], "edges": selection["edges"]}, ) - def _normalize_specs(self, event: param.parameterized.Event) -> None: - is_edge = event.name == "edge_types" - normalized = _coerce_spec_map(event.new, edge=is_edge) - if normalized != event.new: - setattr(self, event.name, normalized) + def _normalize_specs(self, *events: param.parameterized.Event) -> None: + for event in events: + normalized = _coerce_spec_map(event.new, edge=event.name == "edge_types") + if normalized != event.new: + setattr(self, event.name, normalized) def _normalize_nodes(self, event: param.parameterized.Event) -> None: """Normalize nodes list by converting NodeSpec objects to dicts.""" diff --git a/tests/test_core.py b/tests/test_core.py index 6ac9d37..042c5b8 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,9 +1,10 @@ """Tests for ReactFlow model creation.""" +import param from panel.pane import Markdown from panel.viewable import Viewer -from panel_reactflow import Edge, Node, ReactFlow +from panel_reactflow import Edge, EdgeType, Node, NodeType, ReactFlow def test_reactflow_add_node_with_arbitrary_object(document, comm) -> None: @@ -121,3 +122,35 @@ def editor(self, data, schema, *, id, type, on_patch): by_id = {node["id"]: node for node in model.data.nodes} assert by_id["n1"]["data"]["view_idx"] == 0 assert by_id["n2"]["data"].get("view_idx") is None + + +class _Config(param.Parameterized): + x = param.Integer(default=1) + + +def test_updated_node_types_reach_model_as_descriptors(document, comm) -> None: + flow = ReactFlow() + model = flow.get_root(document, comm=comm) + synced = [] + model.data.on_change("node_types", lambda attr, old, new: synced.append(new)) + + flow.node_types = {"a": NodeType(type="a", schema=_Config)} + + assert synced + assert all(isinstance(value["a"], dict) for value in synced) + assert "x" in model.data.node_types["a"]["schema"]["properties"] + + +def test_node_and_edge_types_updated_together(document, comm) -> None: + flow = ReactFlow() + model = flow.get_root(document, comm=comm) + + flow.param.update( + node_types={"a": NodeType(type="a", schema=_Config)}, + edge_types={"e": EdgeType(type="e", schema=_Config)}, + ) + + assert isinstance(flow.node_types["a"], dict) + assert isinstance(flow.edge_types["e"], dict) + assert isinstance(model.data.node_types["a"], dict) + assert isinstance(model.data.edge_types["e"], dict) From 265a3d914c622da62a2a1b4e256bf7cfa38a680b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 28 Sep 2026 17:44:15 +0200 Subject: [PATCH 2/2] docs: Link release note to #76 --- docs/releases.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/releases.md b/docs/releases.md index 06d1baa..9c68522 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -79,7 +79,8 @@ an error boundary keeps a malformed graph from blanking the canvas. JSON descriptors, so the server failed with `can't serialize ParameterizedMetaclass`. They are now converted before syncing, and updating `node_types` and `edge_types` together no longer raises a - `TypeError`. + `TypeError` + ([#76](https://github.com/panel-extensions/panel-reactflow/pull/76)). ## Version 0.4.1