Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions graphify/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
9 changes: 5 additions & 4 deletions graphify/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
29 changes: 29 additions & 0 deletions tests/test_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"