* feat(export): GRAPHIFY_VIZ_NODE_LIMIT env var to override MAX_NODES_FOR_VIZ Adds opt-in env var so users with large graphs (>5000 nodes) can either raise the HTML viz limit or disable viz entirely (set to 0) without patching the package. - New _viz_node_limit() helper reads GRAPHIFY_VIZ_NODE_LIMIT, falls back to MAX_NODES_FOR_VIZ on unset/empty/non-integer values. - to_html() now uses the helper; ValueError message references both --no-viz and GRAPHIFY_VIZ_NODE_LIMIT for discoverability. - 7 new tests in test_export.py covering default, raise/lower, zero, invalid, empty, and end-to-end to_html behavior. Addresses option 2 in #541 (env var as default behavior, zero-config for most users, tunable for large repos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> EOF ) * feat(cluster-only): wire --no-viz flag and tolerate ValueError mid-write Before this change, `graphify cluster-only <path>` called to_html() unconditionally, so on graphs >5000 nodes the ValueError fired AFTER graph.json was written but BEFORE the "Done — ..." print, leaving a stale graph.html on disk and no clear signal in the CLI output. This mirrors the watch.py rebuild pattern (already merged): try/except around to_html, delete the stale graph.html when skipping, and emit a "Done" line that accurately reflects which artifacts landed. - Parse "--no-viz" from sys.argv (documented as such in skill.md but previously never wired into the Python CLI). - Wrap to_html() in try/except so core outputs (graph.json + GRAPH_REPORT.md) always land. - Remove stale graph.html when viz is skipped or fails — avoids the silent desync where graph.html lags behind graph.json by days. - Update help text under `cluster-only` to advertise --no-viz. Addresses option 1 in #541 (--no-viz flag, simplest opt-in). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Tayfun Sert,MBA <tayfunsert@hotmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
Tayfun Sert,MBA
parent
3145ca2e52
commit
6c9b5ef63c
+21
-2
@@ -1002,6 +1002,7 @@ def main() -> None:
|
||||
print(" watch <path> watch a folder and rebuild the graph on code changes")
|
||||
print(" update <path> re-extract code files and update the graph (no LLM needed)")
|
||||
print(" cluster-only <path> rerun clustering on an existing graph.json and regenerate report")
|
||||
print(" --no-viz skip graph.html generation (useful for >5000 node graphs / CI)")
|
||||
print(" query \"<question>\" BFS traversal of graph.json for a question")
|
||||
print(" --dfs use depth-first instead of breadth-first")
|
||||
print(" --budget N cap output at N tokens (default 2000)")
|
||||
@@ -1379,6 +1380,7 @@ def main() -> None:
|
||||
|
||||
elif cmd == "cluster-only":
|
||||
watch_path = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".")
|
||||
no_viz = "--no-viz" in sys.argv
|
||||
graph_json = watch_path / "graphify-out" / "graph.json"
|
||||
if not graph_json.exists():
|
||||
print(f"error: no graph found at {graph_json} — run /graphify first", file=sys.stderr)
|
||||
@@ -1407,8 +1409,25 @@ def main() -> None:
|
||||
out = watch_path / "graphify-out"
|
||||
(out / "GRAPH_REPORT.md").write_text(report, encoding="utf-8")
|
||||
to_json(G, communities, str(out / "graph.json"))
|
||||
to_html(G, communities, str(out / "graph.html"), community_labels=labels or None)
|
||||
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.")
|
||||
|
||||
# Mirror watch.py pattern: gate to_html so core outputs (graph.json +
|
||||
# GRAPH_REPORT.md) always land. Honor --no-viz explicitly; otherwise
|
||||
# fall back to ValueError handling so an oversized graph doesn't crash
|
||||
# the CLI mid-write and leave a stale graph.html on disk.
|
||||
html_target = out / "graph.html"
|
||||
if no_viz:
|
||||
if html_target.exists():
|
||||
html_target.unlink()
|
||||
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).")
|
||||
else:
|
||||
try:
|
||||
to_html(G, communities, str(html_target), community_labels=labels or None)
|
||||
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.")
|
||||
except ValueError as viz_err:
|
||||
if html_target.exists():
|
||||
html_target.unlink()
|
||||
print(f"Skipped graph.html: {viz_err}")
|
||||
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.")
|
||||
|
||||
elif cmd == "update":
|
||||
if len(sys.argv) > 2:
|
||||
|
||||
+23
-4
@@ -25,6 +25,22 @@ COMMUNITY_COLORS = [
|
||||
MAX_NODES_FOR_VIZ = 5_000
|
||||
|
||||
|
||||
def _viz_node_limit() -> int:
|
||||
"""Return the effective viz node limit, honoring GRAPHIFY_VIZ_NODE_LIMIT env var.
|
||||
|
||||
Falls back to MAX_NODES_FOR_VIZ when the env var is unset, empty, or non-integer.
|
||||
Set to 0 to disable HTML viz unconditionally (useful for CI runners).
|
||||
"""
|
||||
import os
|
||||
raw = os.environ.get("GRAPHIFY_VIZ_NODE_LIMIT")
|
||||
if raw is None or not raw.strip():
|
||||
return MAX_NODES_FOR_VIZ
|
||||
try:
|
||||
return int(raw)
|
||||
except ValueError:
|
||||
return MAX_NODES_FOR_VIZ
|
||||
|
||||
|
||||
def _html_styles() -> str:
|
||||
return """<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
@@ -395,15 +411,18 @@ def to_html(
|
||||
|
||||
Features: node size by degree, click-to-inspect panel, search box,
|
||||
community filter, physics clustering by community, confidence-styled edges.
|
||||
Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ.
|
||||
Raises ValueError if graph exceeds the effective viz node limit
|
||||
(MAX_NODES_FOR_VIZ by default; overridable via GRAPHIFY_VIZ_NODE_LIMIT env var).
|
||||
|
||||
If member_counts is provided (aggregated community view), node sizes are
|
||||
based on community member counts rather than graph degree.
|
||||
"""
|
||||
if G.number_of_nodes() > MAX_NODES_FOR_VIZ:
|
||||
limit = _viz_node_limit()
|
||||
if G.number_of_nodes() > limit:
|
||||
raise ValueError(
|
||||
f"Graph has {G.number_of_nodes()} nodes - too large for HTML viz. "
|
||||
f"Use --no-viz or reduce input size."
|
||||
f"Graph has {G.number_of_nodes()} nodes - too large for HTML viz "
|
||||
f"(limit: {limit}). Use --no-viz, raise GRAPHIFY_VIZ_NODE_LIMIT, "
|
||||
f"or reduce input size."
|
||||
)
|
||||
|
||||
node_community = _node_community_map(communities)
|
||||
|
||||
+67
-1
@@ -1,9 +1,19 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster
|
||||
from graphify.export import to_json, to_cypher, to_graphml, to_html, to_canvas
|
||||
from graphify.export import (
|
||||
to_json,
|
||||
to_cypher,
|
||||
to_graphml,
|
||||
to_html,
|
||||
to_canvas,
|
||||
_viz_node_limit,
|
||||
MAX_NODES_FOR_VIZ,
|
||||
)
|
||||
|
||||
FIXTURES = Path(__file__).parent / "fixtures"
|
||||
|
||||
@@ -127,6 +137,62 @@ def test_to_html_contains_nodes_and_edges():
|
||||
assert "RAW_EDGES" in content
|
||||
|
||||
|
||||
# --- GRAPHIFY_VIZ_NODE_LIMIT env var --------------------------------------------
|
||||
|
||||
@pytest.fixture
|
||||
def restore_viz_env(monkeypatch):
|
||||
"""Ensure each test runs without GRAPHIFY_VIZ_NODE_LIMIT bleeding across cases."""
|
||||
monkeypatch.delenv("GRAPHIFY_VIZ_NODE_LIMIT", raising=False)
|
||||
yield
|
||||
|
||||
|
||||
def test_viz_node_limit_default(restore_viz_env):
|
||||
assert _viz_node_limit() == MAX_NODES_FOR_VIZ
|
||||
|
||||
|
||||
def test_viz_node_limit_env_override_higher(restore_viz_env, monkeypatch):
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", "20000")
|
||||
assert _viz_node_limit() == 20000
|
||||
|
||||
|
||||
def test_viz_node_limit_env_override_zero_disables(restore_viz_env, monkeypatch):
|
||||
"""Setting to 0 lets users disable HTML viz unconditionally (CI runners)."""
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", "0")
|
||||
assert _viz_node_limit() == 0
|
||||
|
||||
|
||||
def test_viz_node_limit_invalid_falls_back_to_default(restore_viz_env, monkeypatch):
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", "not-an-int")
|
||||
assert _viz_node_limit() == MAX_NODES_FOR_VIZ
|
||||
|
||||
|
||||
def test_viz_node_limit_empty_falls_back_to_default(restore_viz_env, monkeypatch):
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", " ")
|
||||
assert _viz_node_limit() == MAX_NODES_FOR_VIZ
|
||||
|
||||
|
||||
def test_to_html_raises_with_lowered_limit(restore_viz_env, monkeypatch):
|
||||
"""Lowering the limit below the test graph's size triggers ValueError."""
|
||||
G = make_graph()
|
||||
communities = cluster(G)
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", "1")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp) / "graph.html"
|
||||
with pytest.raises(ValueError, match="too large for HTML viz"):
|
||||
to_html(G, communities, str(out))
|
||||
|
||||
|
||||
def test_to_html_writes_with_raised_limit(restore_viz_env, monkeypatch):
|
||||
"""Raising the limit above the graph's size lets to_html proceed normally."""
|
||||
G = make_graph()
|
||||
communities = cluster(G)
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", "100000")
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out = Path(tmp) / "graph.html"
|
||||
to_html(G, communities, str(out))
|
||||
assert out.exists()
|
||||
|
||||
|
||||
def test_to_html_member_counts_accepted():
|
||||
"""to_html accepts member_counts without raising."""
|
||||
G = make_graph()
|
||||
|
||||
Reference in New Issue
Block a user