diff --git a/graphify/__main__.py b/graphify/__main__.py index 2bad90a..f7903c4 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -2970,13 +2970,18 @@ def main() -> None: p = _ap.ArgumentParser(prog="graphify save-result") p.add_argument("--question", required=True) - p.add_argument("--answer", required=True) + p.add_argument("--answer", default=None) + p.add_argument("--answer-file", dest="answer_file", default=None) p.add_argument("--type", dest="query_type", default="query") p.add_argument("--nodes", nargs="*", default=[]) p.add_argument("--outcome", choices=("useful", "dead_end", "corrected"), default=None) p.add_argument("--correction", default=None) p.add_argument("--memory-dir", default=str(Path(_GRAPHIFY_OUT) / "memory")) opts = p.parse_args(sys.argv[2:]) + if opts.answer_file: + opts.answer = Path(opts.answer_file).read_text(encoding="utf-8").strip() + elif not opts.answer: + p.error("--answer or --answer-file is required") from graphify.ingest import save_query_result as _sqr out = _sqr( diff --git a/graphify/export.py b/graphify/export.py index 052dbcc..2967603 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -1535,6 +1535,15 @@ 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 "". + 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] = "" + 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) diff --git a/tests/test_export.py b/tests/test_export.py index 6a1a439..fb5481f 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -82,6 +82,23 @@ def test_to_graphml_has_community_attribute(): content = out.read_text() assert "community" in content +def test_to_graphml_tolerates_none_attribute_values(): + """nx.write_graphml raises ValueError on a None attribute value; to_graphml + must coerce None -> "" so a node/edge with a null field still exports (#1502).""" + G = make_graph() + communities = cluster(G) + # Inject a None-valued attribute on one node and one edge. + a_node = next(iter(G.nodes())) + G.nodes[a_node]["nullable_field"] = None + if G.number_of_edges(): + u, v = next(iter(G.edges())) + G.edges[u, v]["nullable_field"] = None + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "graph.graphml" + to_graphml(G, communities, str(out)) # must not raise + content = out.read_text() + assert " clean argparse error, not a crash.""" + r = _run(["save-result", "--question", "q", "--outcome", "useful"], tmp_path) + assert r.returncode != 0 + assert "--answer" in (r.stderr + r.stdout) + + def test_cli_reflect_cold_start_writes_empty_lessons(tmp_path): """First run with no graphify-out/memory/ still succeeds and writes a valid doc.""" r = _run(["reflect"], tmp_path)