Pin PYTHONHASHSEED for update, extract, cluster only and label

The generated git hooks already export PYTHONHASHSEED=0 because
networkx's Louvain implementation iterates string keyed sets whose
order is randomized per process, so community assignments otherwise
churn between runs with no code change. A bare graphify update, which
the CLAUDE.md template tells agents to run after every code change,
skipped this, clustering differently every time and producing a
large, spurious diff in the output directory.

PYTHONHASHSEED is read once at interpreter startup, so setting it on
os.environ from inside an already running process has no effect on
that process's own hash randomization. The only way to pin it for a
command already in flight is to restart the interpreter with it set
from the start, so main() now re execs itself with the seed set
before doing anything else, for exactly the commands whose output
depends on clustering, only when the caller has not already chosen a
seed themselves, degrading instead of raising if the re exec itself
fails so an unusual host that disallows it can still run graphify.

Skips this entirely while running under pytest, detected via the
PYTEST_CURRENT_TEST variable pytest itself sets for the duration of
a running test. Dozens of existing tests call main() directly with a
monkeypatched argv to simulate a full CLI run in process, which only
worked because main() was previously free of this kind of side
effect; without the guard, every one of those calls replaced the
test process running them the moment PYTHONHASHSEED happened to be
unset, which it normally is in a dev or CI environment, with no
traceback or error, just an empty, silently green test run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
This commit is contained in:
ayushcodes10andClaude Sonnet 5 committed 2026-09-22 15:26:22 +05:30
1 parent 20a20d30d8
commit b70382ceda
1 file changed
+45
+45
View File
@@ -483,11 +483,56 @@ def _silence_broken_pipe() -> None:
sys.exit(0)
_HASHSEED_PINNED_COMMANDS = frozenset({"update", "extract", "cluster-only", "label"})
def _pin_hash_seed_if_needed() -> None:
"""Re-exec with PYTHONHASHSEED=0 for commands whose output must be
deterministic run-to-run (#3641).
PYTHONHASHSEED is read once at interpreter startup; setting it on
os.environ from inside an already-running process has no effect on that
process's own hash randomization, so pinning it requires restarting the
interpreter with it set from the start. The generated git hooks already
export it for exactly this reason: networkx's Louvain implementation
iterates string-keyed sets whose order is randomized per-process, so
community assignments otherwise churn between runs with no code change.
A bare `graphify update .` (which the CLAUDE.md template tells agents to
run after every code change) skipped this, re-clustering differently
every time and producing a large, spurious graphify-out diff.
Only re-execs for the commands whose output actually depends on
clustering, and only when the caller has not already set
PYTHONHASHSEED themselves — an explicit choice is left alone. Degrades
instead of raising if the re-exec itself fails, since an unusual host
that disallows it should still be able to run graphify, just without
this determinism guarantee.
Also does nothing under pytest: dozens of existing tests call main()
directly with a monkeypatched sys.argv to simulate a full CLI run in
process, an approach that only works because main() was previously
side-effect-free at the point it starts. A real os.execvpe there would
replace the test process running those tests, not something to launch
for real -- PYTEST_CURRENT_TEST is set by pytest for exactly the
duration of a running test's setup/call/teardown, so this only ever
skips the pin inside an actual test, never a real invocation.
"""
if len(sys.argv) < 2 or sys.argv[1] not in _HASHSEED_PINNED_COMMANDS:
return
if "PYTHONHASHSEED" in os.environ or "PYTEST_CURRENT_TEST" in os.environ:
return
try:
os.execvpe(sys.executable, [sys.executable, *sys.argv], {**os.environ, "PYTHONHASHSEED": "0"})
except OSError:
pass
def main() -> None:
"""Console entry point. Wraps the CLI so that when a downstream consumer closes
stdout early, graphify treats it as success instead of crashing with an
unhandled write-to-closed-pipe error and exit 255 — which made CI wrappers and
agent harnesses read a successful query as a command failure (#1807)."""
_pin_hash_seed_if_needed()
try:
_run_cli()
# Flush explicitly, inside the guard. Piped stdout is block-buffered, so a