Skip to content

Commit ceb6072

Browse files
committed
Add to_dict() method for StrictMultiDiGraph and corresponding tests for serialization
1 parent 06e1ead commit ceb6072

4 files changed

Lines changed: 185 additions & 1 deletion

File tree

docs/reference/api-full.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ For a curated, example-driven API guide, see **[api.md](api.md)**.
1010
> - **[CLI Reference](cli.md)** - Command-line interface
1111
> - **[DSL Reference](dsl.md)** - YAML syntax guide
1212
13-
**Generated from source code on:** June 13, 2025 at 17:50 UTC
13+
**Generated from source code on:** June 13, 2025 at 18:47 UTC
1414

1515
**Modules auto-discovered:** 39
1616

@@ -1138,6 +1138,8 @@ Inherits from:
11381138
- Returns a SubGraph view of the subgraph induced on `nodes`.
11391139
- `successors(self, n)`
11401140
- Returns an iterator over successor nodes of n.
1141+
- `to_dict(self) -> 'Dict[str, Any]'`
1142+
- Convert the graph to a dictionary representation suitable for JSON serialization.
11411143
- `to_directed(self, as_view=False)`
11421144
- Returns a directed representation of the graph.
11431145
- `to_directed_class(self)`

ngraph/lib/graph.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,3 +298,17 @@ def update_edge_attr(self, key: EdgeID, **attr: Any) -> None:
298298
if key not in self._edges:
299299
raise ValueError(f"Edge with id='{key}' not found.")
300300
self._edges[key][3].update(attr)
301+
302+
def to_dict(self) -> Dict[str, Any]:
303+
"""Convert the graph to a dictionary representation suitable for JSON serialization.
304+
305+
Returns a node-link format dictionary with graph attributes, nodes, and edges.
306+
The format is compatible with visualization libraries like D3.js.
307+
308+
Returns:
309+
Dict[str, Any]: Dictionary containing 'graph', 'nodes', and 'links' keys.
310+
"""
311+
# Import here to avoid circular import
312+
from ngraph.lib.io import graph_to_node_link
313+
314+
return graph_to_node_link(self)

tests/lib/test_graph.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,110 @@ def test_networkx_algorithm():
380380
)
381381
# Expect two equally short paths: A->B->C (10+4=14) and A->BB->C (10+4=14)
382382
assert sorted(all_sp) == sorted([["A", "B", "C"], ["A", "BB", "C"]])
383+
384+
385+
def test_to_dict():
386+
"""Test that to_dict() returns proper node-link format for JSON serialization."""
387+
g = StrictMultiDiGraph()
388+
389+
# Test empty graph
390+
result = g.to_dict()
391+
assert result == {"graph": {}, "nodes": [], "links": []}
392+
393+
# Add nodes with attributes
394+
g.add_node("A", type="router", location="rack1")
395+
g.add_node("B", type="switch", location="rack2")
396+
g.add_node("C", type="server")
397+
398+
# Add edges with attributes
399+
e1 = g.add_edge("A", "B", capacity=100, cost=5)
400+
e2 = g.add_edge("B", "C", capacity=50, cost=2)
401+
e3 = g.add_edge("A", "C", capacity=25) # No cost attribute
402+
403+
result = g.to_dict()
404+
405+
# Verify structure
406+
assert "graph" in result
407+
assert "nodes" in result
408+
assert "links" in result
409+
410+
# Verify graph attributes (should be empty for this test)
411+
assert result["graph"] == {}
412+
413+
# Verify nodes
414+
assert len(result["nodes"]) == 3
415+
node_ids = [node["id"] for node in result["nodes"]]
416+
assert set(node_ids) == {"A", "B", "C"}
417+
418+
# Find specific nodes and verify their attributes
419+
node_a = next(n for n in result["nodes"] if n["id"] == "A")
420+
assert node_a["attr"] == {"type": "router", "location": "rack1"}
421+
422+
node_b = next(n for n in result["nodes"] if n["id"] == "B")
423+
assert node_b["attr"] == {"type": "switch", "location": "rack2"}
424+
425+
node_c = next(n for n in result["nodes"] if n["id"] == "C")
426+
assert node_c["attr"] == {"type": "server"}
427+
428+
# Verify edges
429+
assert len(result["links"]) == 3
430+
431+
# Create a mapping from node ID to index for edge verification
432+
node_indices = {node["id"]: i for i, node in enumerate(result["nodes"])}
433+
434+
# Find specific edges by their keys and verify attributes
435+
edge_by_key = {link["key"]: link for link in result["links"]}
436+
437+
assert e1 in edge_by_key
438+
e1_link = edge_by_key[e1]
439+
assert e1_link["source"] == node_indices["A"]
440+
assert e1_link["target"] == node_indices["B"]
441+
assert e1_link["attr"] == {"capacity": 100, "cost": 5}
442+
443+
assert e2 in edge_by_key
444+
e2_link = edge_by_key[e2]
445+
assert e2_link["source"] == node_indices["B"]
446+
assert e2_link["target"] == node_indices["C"]
447+
assert e2_link["attr"] == {"capacity": 50, "cost": 2}
448+
449+
assert e3 in edge_by_key
450+
e3_link = edge_by_key[e3]
451+
assert e3_link["source"] == node_indices["A"]
452+
assert e3_link["target"] == node_indices["C"]
453+
assert e3_link["attr"] == {"capacity": 25}
454+
455+
456+
def test_to_dict_with_graph_attributes():
457+
"""Test to_dict() with graph-level attributes."""
458+
g = StrictMultiDiGraph(name="test_network", version="1.0")
459+
g.add_node("A")
460+
g.add_node("B")
461+
g.add_edge("A", "B")
462+
463+
result = g.to_dict()
464+
465+
# Verify graph attributes are included
466+
assert result["graph"]["name"] == "test_network"
467+
assert result["graph"]["version"] == "1.0"
468+
assert len(result["nodes"]) == 2
469+
assert len(result["links"]) == 1
470+
471+
472+
def test_to_dict_json_serializable():
473+
"""Test that to_dict() output is JSON serializable."""
474+
import json
475+
476+
g = StrictMultiDiGraph()
477+
g.add_node("A", value=42, active=True)
478+
g.add_node("B", value=3.14, tags=["tag1", "tag2"])
479+
g.add_edge("A", "B", weight=1.5, metadata={"created": "2025-06-13"})
480+
481+
result = g.to_dict()
482+
483+
# Should be able to serialize to JSON without errors
484+
json_str = json.dumps(result)
485+
486+
# Should be able to round-trip
487+
parsed = json.loads(json_str)
488+
assert parsed["nodes"][0]["attr"]["value"] in [42, 3.14] # Could be in any order
489+
assert len(parsed["links"]) == 1

tests/test_results_serialisation.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,3 +340,64 @@ def test_results_complex_nested_structures():
340340
)
341341
except TypeError:
342342
pass # Expected - nested objects in dict don't get auto-converted
343+
344+
345+
def test_results_strict_multi_di_graph_serialization():
346+
"""Test that Results.to_dict() properly converts StrictMultiDiGraph objects."""
347+
from ngraph.lib.graph import StrictMultiDiGraph
348+
349+
res = Results()
350+
351+
# Create a test graph
352+
graph = StrictMultiDiGraph()
353+
graph.add_node("A", type="router", location="datacenter1")
354+
graph.add_node("B", type="switch", location="datacenter2")
355+
edge_id = graph.add_edge("A", "B", capacity=100, cost=5)
356+
357+
# Store graph in results
358+
res.put("build_graph", "graph", graph)
359+
res.put("build_graph", "node_count", 2)
360+
361+
# Convert to dict
362+
d = res.to_dict()
363+
364+
# Verify structure
365+
assert "build_graph" in d
366+
assert "graph" in d["build_graph"]
367+
assert "node_count" in d["build_graph"]
368+
369+
# Verify the graph was converted to node-link format
370+
graph_dict = d["build_graph"]["graph"]
371+
assert isinstance(graph_dict, dict)
372+
assert "nodes" in graph_dict
373+
assert "links" in graph_dict
374+
assert "graph" in graph_dict
375+
376+
# Verify nodes
377+
assert len(graph_dict["nodes"]) == 2
378+
node_ids = [node["id"] for node in graph_dict["nodes"]]
379+
assert set(node_ids) == {"A", "B"}
380+
381+
# Find node A and verify its attributes
382+
node_a = next(n for n in graph_dict["nodes"] if n["id"] == "A")
383+
assert node_a["attr"]["type"] == "router"
384+
assert node_a["attr"]["location"] == "datacenter1"
385+
386+
# Verify edges
387+
assert len(graph_dict["links"]) == 1
388+
edge = graph_dict["links"][0]
389+
assert edge["key"] == edge_id
390+
assert edge["attr"]["capacity"] == 100
391+
assert edge["attr"]["cost"] == 5
392+
393+
# Verify scalar value is unchanged
394+
assert d["build_graph"]["node_count"] == 2
395+
396+
# Verify the result is JSON serializable
397+
json_str = json.dumps(d)
398+
399+
# Verify round-trip
400+
parsed = json.loads(json_str)
401+
assert parsed["build_graph"]["node_count"] == 2
402+
assert len(parsed["build_graph"]["graph"]["nodes"]) == 2
403+
assert len(parsed["build_graph"]["graph"]["links"]) == 1

0 commit comments

Comments
 (0)