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
18 changes: 14 additions & 4 deletions graphify/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,14 +912,24 @@ def to_graphml(
for k in [k for k in attrs if k.startswith("_")]:
del attrs[k]
# nx.write_graphml raises ValueError on None attribute values; replace with "".
# It also only accepts scalar attribute values, so dict/list-valued
# attributes (e.g. per-node "metadata", graph-level "hyperedges") must be
# serialized to a JSON string first, or the writer raises TypeError.
def _scrub(val):
if val is None:
return ""
if isinstance(val, (dict, list)):
return json.dumps(val)
return val

for key, val in list(H.graph.items()):
H.graph[key] = _scrub(val)
for node_id in H.nodes():
for key, val in list(H.nodes[node_id].items()):
if val is None:
H.nodes[node_id][key] = ""
H.nodes[node_id][key] = _scrub(val)
for u, v in H.edges():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lookup crashes for MultiGraph/MultiDiGraph because H.edges[u, v] requires an edge key. Iterating the mutable attribute dictionaries directly works for both regular and keyed graphs:

Suggested change
for u, v in H.edges():
for _, _, attrs in H.edges(data=True):
for key, value in list(attrs.items()):
attrs[key] = _scrub(value)

Please cover this with a MultiDiGraph edge containing a dict/list attribute and verify the value after a GraphML round trip.

for key, val in list(H.edges[u, v].items()):
if val is None:
H.edges[u, v][key] = ""
H.edges[u, v][key] = _scrub(val)
nx.write_graphml(H, output_path)


Expand Down
19 changes: 19 additions & 0 deletions tests/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,25 @@ def test_to_graphml_tolerates_none_attribute_values():
content = out.read_text()
assert "<graphml" in content

def test_to_graphml_tolerates_dict_and_list_attribute_values():
"""nx.write_graphml only accepts scalar attribute values and raises
TypeError on a dict or list — e.g. a node's "metadata" dict, or a
graph-level "hyperedges" list (attach_hyperedges()). to_graphml must
JSON-serialize these instead of passing them through unmodified."""
G = make_graph()
communities = cluster(G)
a_node = next(iter(G.nodes()))
G.nodes[a_node]["metadata"] = {"kind": "file"}
if G.number_of_edges():
u, v = next(iter(G.edges()))
G.edges[u, v]["tags"] = ["a", "b"]
G.graph["hyperedges"] = [{"nodes": [a_node], "label": "x"}]
with tempfile.TemporaryDirectory() as tmp:
out = Path(tmp) / "graph.graphml"
to_graphml(G, communities, str(out)) # must not raise
content = out.read_text()
assert "<graphml" in content

def test_to_html_creates_file():
G = make_graph()
communities = cluster(G)
Expand Down