mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-27 16:06:11 +00:00
fix(export): coerce non-scalar attrs in graphml; harden pipe-close flush
#1831 — `graphify export graphml` crashed on any dict/list-valued attribute (per-node metadata dict, graph-level hyperedges list) because nx.write_graphml only accepts scalars; a real ~2,300-node graph failed every export and left a 0-byte .graphml behind. to_graphml now coerces None->"" and JSON-serializes non-scalars across graph/node/edge scopes (int/float/bool/str pass through), and writes atomically via a temp file so a failed export can't leave a partial file. Closes #1830. #1807 followup — adopt @varuntej07's explicit in-guard sys.stdout.flush() from #1811: piped stdout is block-buffered, so a small fully-buffered output would only flush at interpreter shutdown (outside the guard), where a closed-pipe reader escapes as a noisy shutdown error and nonzero exit. Flushing inside the try closes that gap. Closes #1811. Reported by @hofmockel (#1831) and @varuntej07 (#1807/#1811). Co-Authored-By: hofmockel <hofmockel@users.noreply.github.com> Co-Authored-By: varuntej07 <varuntej07@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
hofmockel
varuntej07
Claude Opus 4.8
parent
5f57c46b92
commit
a46eee49ef
@@ -464,6 +464,11 @@ def main() -> None:
|
||||
agent harnesses read a successful query as a command failure (#1807)."""
|
||||
try:
|
||||
_run_cli()
|
||||
# Flush explicitly, inside the guard. Piped stdout is block-buffered, so a
|
||||
# small fully-buffered output would otherwise only flush at interpreter
|
||||
# shutdown — outside this try — where a reader that closed the pipe surfaces
|
||||
# as a noisy "Exception ignored on flushing sys.stdout" and a nonzero exit.
|
||||
sys.stdout.flush()
|
||||
except BrokenPipeError:
|
||||
_silence_broken_pipe()
|
||||
except OSError as exc:
|
||||
|
||||
+34
-6
@@ -911,16 +911,44 @@ def to_graphml(
|
||||
for _, _, attrs in H.edges(data=True):
|
||||
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 "".
|
||||
# nx.write_graphml only accepts scalar attribute values: None raises, and a
|
||||
# dict/list value (e.g. a per-node `metadata` dict, or the graph-level
|
||||
# `hyperedges` list set by attach_hyperedges()) raises
|
||||
# "GraphML does not support type <class 'dict'/'list'> as data values" (#1831).
|
||||
# Coerce None -> "" and non-scalars -> a JSON string, across all three scopes.
|
||||
def _graphml_safe(val):
|
||||
if val is None:
|
||||
return ""
|
||||
if isinstance(val, bool) or isinstance(val, (int, float, str)):
|
||||
return val # GraphML-native scalars pass through unchanged
|
||||
try:
|
||||
return json.dumps(val, default=str, sort_keys=True)
|
||||
except (TypeError, ValueError):
|
||||
return str(val)
|
||||
|
||||
for key, val in list(H.graph.items()):
|
||||
H.graph[key] = _graphml_safe(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] = _graphml_safe(val)
|
||||
for u, v in H.edges():
|
||||
for key, val in list(H.edges[u, v].items()):
|
||||
if val is None:
|
||||
H.edges[u, v][key] = ""
|
||||
nx.write_graphml(H, output_path)
|
||||
H.edges[u, v][key] = _graphml_safe(val)
|
||||
|
||||
# Write atomically: a mid-serialization error otherwise leaves a 0-byte
|
||||
# .graphml on disk that downstream tooling mistakes for a completed export
|
||||
# (#1831). Write to a sibling temp file, then replace on success.
|
||||
out = Path(output_path)
|
||||
tmp = out.with_name(out.name + ".tmp")
|
||||
try:
|
||||
nx.write_graphml(H, str(tmp))
|
||||
os.replace(str(tmp), str(out))
|
||||
finally:
|
||||
if tmp.exists():
|
||||
try:
|
||||
tmp.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def to_svg(
|
||||
|
||||
Reference in New Issue
Block a user