From 27068be3fc1b702e61807a68335422ff1d19abc5 Mon Sep 17 00:00:00 2001 From: mrsyb Date: Sat, 11 Apr 2026 20:58:05 +0800 Subject: [PATCH] fix: accept 'links' key from NetworkX <=3.1 alongside 'edges' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NetworkX changed its node_link_data() default edge key from 'links' (<=3.1) to 'edges' (>=3.2). Any data serialised with an older NetworkX version — or any serialiser that follows the original JSON Graph Format spec — uses 'links', causing graphify to silently produce graphs with zero edges. Changes: - validate.py: fall back to data['links'] when data['edges'] is absent - build.py: normalise 'links' -> 'edges' at the top of build_from_json() Both fixes are backward-compatible: data already using 'edges' is unaffected. Tests added: - test_build_from_json_with_links_key (old-style 'links' key) - test_build_from_json_with_edges_key (new-style 'edges' key, current NX) - test_links_key_accepted_as_edges (validate accepts 'links') - test_edges_key_takes_priority_over_links (priority order is correct) --- graphify/build.py | 3 +++ graphify/validate.py | 9 +++++---- tests/test_build.py | 28 ++++++++++++++++++++++++++++ tests/test_validate.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/graphify/build.py b/graphify/build.py index 3c3d80ca6..d5ed87e0d 100644 --- a/graphify/build.py +++ b/graphify/build.py @@ -32,6 +32,9 @@ def build_from_json(extraction: dict, *, directed: bool = False) -> nx.Graph: directed=True produces a DiGraph that preserves edge direction (source→target). directed=False (default) produces an undirected Graph for backward compatibility. """ + # Normalise: NetworkX node_link_data() serialises edges as "links"; remap to "edges". + if "edges" not in extraction and "links" in extraction: + extraction = dict(extraction, edges=extraction["links"]) errors = validate_extraction(extraction) # Dangling edges (stdlib/external imports) are expected - only warn about real schema errors. real_errors = [e for e in errors if "does not match any node id" not in e] diff --git a/graphify/validate.py b/graphify/validate.py index 2c3727777..ee4d84f8c 100644 --- a/graphify/validate.py +++ b/graphify/validate.py @@ -36,14 +36,15 @@ def validate_extraction(data: dict) -> list[str]: f"'{node['file_type']}' - must be one of {sorted(VALID_FILE_TYPES)}" ) - # Edges - if "edges" not in data: + # Edges - accept both "edges" (graphify schema) and "links" (NetworkX node_link_data default) + edge_list = data.get("edges") if "edges" in data else data.get("links") + if edge_list is None: errors.append("Missing required key 'edges'") - elif not isinstance(data["edges"], list): + elif not isinstance(edge_list, list): errors.append("'edges' must be a list") else: node_ids = {n["id"] for n in data.get("nodes", []) if isinstance(n, dict) and "id" in n} - for i, edge in enumerate(data["edges"]): + for i, edge in enumerate(edge_list): if not isinstance(edge, dict): errors.append(f"Edge {i} must be an object") continue diff --git a/tests/test_build.py b/tests/test_build.py index 54a7fa451..c3451c034 100644 --- a/tests/test_build.py +++ b/tests/test_build.py @@ -39,3 +39,31 @@ def test_build_merges_multiple_extractions(): G = build([ext1, ext2]) assert G.number_of_nodes() == 2 assert G.number_of_edges() == 1 + + +def test_build_from_json_with_links_key(): + """NetworkX <= 3.1 used "links" as the edge key in node_link_data(). + build_from_json() must accept "links" as a fallback for "edges" so that + data produced by older NetworkX versions (or any serialiser that uses + "links") is handled correctly.""" + data = { + "directed": False, "multigraph": False, "graph": {}, + "nodes": [{"id": "n1"}, {"id": "n2"}], + "links": [{"source": "n1", "target": "n2"}], # old NetworkX key + } + built = build_from_json(data) + assert built.number_of_nodes() == 2 + assert built.number_of_edges() == 1 # was 0 before the fix + + +def test_build_from_json_with_edges_key(): + """NetworkX >= 3.2 uses "edges" as the default key — must still work.""" + import networkx as nx + G = nx.Graph() + G.add_node("n1", label="A") + G.add_node("n2", label="B") + G.add_edge("n1", "n2") + nx_data = nx.node_link_data(G) + built = build_from_json(nx_data) + assert built.number_of_nodes() == 2 + assert built.number_of_edges() == 1 diff --git a/tests/test_validate.py b/tests/test_validate.py index 396e90c8c..1604f9eda 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -85,3 +85,32 @@ def test_assert_valid_raises_on_errors(): def test_assert_valid_passes_silently(): assert_valid(VALID) # should not raise + + +def test_links_key_accepted_as_edges(): + """validate_extraction() must accept 'links' as a fallback for 'edges' + to support data produced by NetworkX node_link_data().""" + data = { + "nodes": [ + {"id": "n1", "label": "Foo", "file_type": "code", "source_file": "foo.py"}, + {"id": "n2", "label": "Bar", "file_type": "code", "source_file": "bar.py"}, + ], + "links": [ + {"source": "n1", "target": "n2", "relation": "calls", + "confidence": "EXTRACTED", "source_file": "foo.py", "weight": 1.0}, + ], + } + errors = validate_extraction(data) + assert errors == [], f"Unexpected errors: {errors}" + +def test_edges_key_takes_priority_over_links(): + """If both 'edges' and 'links' are present, 'edges' must be used.""" + data = { + "nodes": [ + {"id": "n1", "label": "Foo", "file_type": "code", "source_file": "foo.py"}, + ], + "edges": [], # empty — should be used + "links": "not-a-list", # invalid — should be ignored + } + errors = validate_extraction(data) + assert errors == [], f"Unexpected errors: {errors}"