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}"