fix(export): export GraphML from a graph loaded as a MultiGraph

Since #4500, graph.json carries "multigraph": true whenever it holds
parallel edges (imports and calls between the same pair, say), and
graphify export loads it as a MultiGraph. to_graphml's attribute
coercion indexed edges as H.edges[u, v], which needs a key on a
MultiGraph, so graphify export graphml crashed with "not enough values to
unpack" and wrote nothing. Iterate edges(data=True) instead, which works
for both graph types and keeps every parallel edge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Rudy CelekliandClaude Opus 5.5 authored and safishamsi committed 2026-10-20 10:50:37 +01:00
1 parent 8d39c75d02
commit ffa5a6b051
2 files changed
+30 -3

No files matched your search

+6 -3
View File
@@ -1346,9 +1346,12 @@ def to_graphml(
for node_id in H.nodes():
for key, val in list(H.nodes[node_id].items()):
H.nodes[node_id][key] = _graphml_safe(val)
for u, v in H.edges():
for key, val in list(H.edges[u, v].items()):
H.edges[u, v][key] = _graphml_safe(val)
# edges(data=True) rather than H.edges[u, v]: graph.json is loaded as a
# MultiGraph when it carries parallel edges (#4500), and a MultiGraph edge
# view needs a key, so the (u, v) form crashed the whole export.
for *_, attrs in H.edges(data=True):
for key, val in list(attrs.items()):
attrs[key] = _graphml_safe(val)
# Write atomically: a mid-serialization error otherwise leaves a 0-byte
# .graphml on disk that downstream tooling mistakes for a completed export
+24
View File
@@ -282,6 +282,30 @@ def test_to_graphml_tolerates_dict_and_list_attribute_values():
assert not (Path(tmp) / "graph.graphml.tmp").exists()
def test_to_graphml_exports_multigraph_parallel_edges():
"""graph.json is loaded as a MultiGraph when it carries parallel edges
(#4500), e.g. `imports` and `calls` between the same two nodes. The edge
coercion loop indexed H.edges[u, v], which needs a key on a MultiGraph, so
the export crashed; every parallel edge must reach the file, coerced."""
import networkx as nx
G = nx.MultiGraph()
G.add_node("a", label="A")
G.add_node("b", label="B")
G.add_edge("a", "b", relation="imports", confidence="EXTRACTED", note=None)
G.add_edge("a", "b", relation="calls", confidence="INFERRED", ctx={"k": "v"})
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "g.graphml"
to_graphml(G, {0: ["a", "b"]}, str(out))
H = nx.read_graphml(str(out))
assert H.number_of_edges() == 2
edges = [d for _, _, d in H.edges(data=True)]
assert sorted(d["relation"] for d in edges) == ["calls", "imports"]
calls = next(d for d in edges if d["relation"] == "calls")
imports = next(d for d in edges if d["relation"] == "imports")
assert json.loads(calls["ctx"]) == {"k": "v"}
assert imports["note"] == ""
def test_to_graphml_preserves_native_scalar_types():
"""Coercion must leave GraphML-native scalars (int/float/bool/str) untouched,
only stringifying non-scalars (#1831)."""