fix(export): stabilize graph JSON collection order

This commit is contained in:
hjotha
2026-08-11 15:18:33 +01:00
committed by safishamsi
parent 21e75cfae4
commit 36b47ba25d
2 changed files with 42 additions and 1 deletions
+10 -1
View File
@@ -292,6 +292,10 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *,
data = json_graph.node_link_data(G, edges="links")
except TypeError:
data = json_graph.node_link_data(G)
def _json_sort_key(item: dict) -> str:
return json.dumps(item, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
for node in data["nodes"]:
cid = node_community.get(node["id"])
node["community"] = cid
@@ -311,6 +315,8 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *,
if true_src is not None and true_tgt is not None:
link["source"] = true_src
link["target"] = true_tgt
data["nodes"].sort(key=_json_sort_key)
data["links"].sort(key=_json_sort_key)
if "hyperedges" not in getattr(G, "graph", {}):
# Hardening (#2485): a graph with NO hyperedges key at all was built by
# a path that never engaged hyperedge metadata — distinct from an
@@ -339,7 +345,10 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *,
f"extraction if this is unexpected.",
file=sys.stderr,
)
data["hyperedges"] = getattr(G, "graph", {}).get("hyperedges", [])
hyperedges = sorted(getattr(G, "graph", {}).get("hyperedges", []), key=_json_sort_key)
if isinstance(data.get("graph"), dict) and "hyperedges" in data["graph"]:
data["graph"]["hyperedges"] = hyperedges
data["hyperedges"] = hyperedges
commit = built_at_commit if built_at_commit is not None else _git_head()
if commit:
data["built_at_commit"] = commit
+32
View File
@@ -40,6 +40,38 @@ def test_to_json_nodes_have_community():
for node in data["nodes"]:
assert "community" in node
def test_to_json_sorts_graph_collections_across_insertion_order(tmp_path):
import networkx as nx
nodes = [("b", {"label": "Beta"}), ("a", {"label": "Alpha"}), ("c", {"label": "Gamma"})]
links = [
("b", "c", {"relation": "uses", "_src": "b", "_tgt": "c"}),
("a", "b", {"relation": "calls", "_src": "a", "_tgt": "b"}),
]
hyperedges = [
{"id": "h2", "nodes": ["b", "c"]},
{"id": "h1", "nodes": ["a", "b"]},
]
def make_graph(reverse=False):
graph = nx.Graph()
graph.add_nodes_from(reversed(nodes) if reverse else nodes)
graph.add_edges_from(reversed(links) if reverse else links)
graph.graph["hyperedges"] = list(reversed(hyperedges)) if reverse else hyperedges
return graph
outputs = [tmp_path / "first.json", tmp_path / "second.json"]
for output, reverse in zip(outputs, (False, True)):
assert to_json(
make_graph(reverse),
{0: ["a", "b"], 1: ["c"]},
str(output),
built_at_commit="fixed",
)
assert outputs[0].read_bytes() == outputs[1].read_bytes()
def test_to_cypher_creates_file():
G = make_graph()
with tempfile.TemporaryDirectory() as tmp: