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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
- Feature: five new language extractors — **COBOL** (`.cbl`/`.cob`/`.cobol`/`.cpy`; programs, paragraphs, `PERFORM`/`CALL`/`COPY`, pure-regex, no new dependency) (#3713, thanks @Abdul535), **VB.NET** (`.vb`; case-insensitive types/methods, `Inherits`/`Implements`/`Handles`) (#3717, thanks @Abdul535), **R** (`.r`/`.R`; assignment-form function defs, `library`/`source`, S4/R6 classes) (#3715, thanks @Abdul535), **Solidity** (`.sol`; contracts/interfaces/libraries, `is` inheritance, imports, modifiers) (#3716, thanks @Abdul535), and **Erlang** (`.erl`/`.hrl`/`.escript`; modules, functions by arity, behaviours, local + remote `foo:bar()` calls) (#3714, thanks @Abdul535). The R and Erlang grammars ship via the `r`/`erlang` extras (they have no standalone PyPI wheel); Solidity and VB.NET have their own extras.
- Feature: Go interface method requirements now resolve to the interface, and their parameter/return types emit `references` edges (#3672, #3737, thanks @rajatnagda45, @oleksii-tumanov).
- Feature: a Rust `self.method()` call resolves across files for simple generic impls (`impl<T> Foo<T>`) (#3653, thanks @oleksii-tumanov).
- Fix: `graph.json` is now deterministic across runs — `update`/`extract`/`cluster-only`/`label` pin `PYTHONHASHSEED` via a one-time re-exec, so hash-seed-sensitive community detection (Leiden/Louvain) produces identical output run-to-run and matches hook-triggered rebuilds (#3743, #3641, thanks @ayushcodes10).
- Fix: `graph.json` is now deterministic across runs — `update`/`extract`/`cluster-only`/`label` pin `PYTHONHASHSEED` via a one-time re-exec, so hash-seed-sensitive community detection (Leiden/Louvain) produces identical output run-to-run and matches hook-triggered rebuilds. The re-exec now runs as `python -m graphify` instead of replaying `argv[0]` as a script path, since a uv/pip/pipx console-script launcher on Windows is a native `.exe` with no `.py` content — every affected command previously failed outright with "can't open file" there (#3743, #3641, #3779, thanks @ayushcodes10).
- Fix: a same-relation edge collision now keeps the higher-confidence edge (EXTRACTED over INFERRED) instead of resolving by arrival order (#3711, thanks @shobhitagnihotri69).
- Fix: a spec-conformant method-node duplicate (dropped class segment / leading dot) is now deduplicated onto its canonical AST node, gated on a method-shaped label and a single unambiguous candidate (#3719, #3705, thanks @shobhitagnihotri69).
- Fix: intra-module Go `imports_from` edges now repoint onto the imported package's real file nodes instead of dangling at a `go_pkg_` sink; external/stdlib imports stay external (#3748, thanks @carterko23).
Expand Down
12 changes: 11 additions & 1 deletion graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,17 @@ def _pin_hash_seed_if_needed() -> None:
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"})
# Re-exec as a module (-m graphify) rather than replaying sys.argv[0]
# as a script path: that works for a POSIX console-script wrapper or
# a `python -m graphify` invocation, but a uv/pip/pipx console-script
# launcher on Windows is a native .exe with no .py content, so
# `python.exe <that .exe path>` fails outright with "can't open
# file" -- every command this function touches (#3779).
os.execvpe(
sys.executable,
[sys.executable, "-m", "graphify", *sys.argv[1:]],
{**os.environ, "PYTHONHASHSEED": "0"},
)
except OSError:
pass

Expand Down
15 changes: 14 additions & 1 deletion tests/test_pin_hash_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,23 @@ def test_reexecs_for_hash_sensitive_commands_when_unset():
for cmd in ("update", "extract", "cluster-only", "label"):
outcome = _run_probe(["graphify", cmd, "."])
assert outcome["called"], f"{cmd} must re-exec with PYTHONHASHSEED pinned"
assert outcome["argv"] == [sys.executable, "graphify", cmd, "."]
assert outcome["argv"] == [sys.executable, "-m", "graphify", cmd, "."]
assert outcome["env_hashseed"] == "0"


def test_reexec_does_not_depend_on_argv0_being_a_runnable_script():
"""#3779: a uv/pip/pipx console-script launcher on Windows is a native
.exe with no .py content, so `python.exe <that .exe path>` fails
outright with "can't open file" the moment argv[0] is replayed as a
script path. Re-execing via `-m graphify` never touches argv[0] at
all, so a launcher stub that isn't even a real file must not matter."""
outcome = _run_probe(["/some/launcher/stub/with/no/py/content", "update", "."])
assert outcome["called"]
assert outcome["argv"] == [sys.executable, "-m", "graphify", "update", "."], (
"the launcher stub path must never appear in the re-exec argv"
)


def test_does_not_reexec_when_already_set():
outcome = _run_probe(["graphify", "update", "."], extra_env={"PYTHONHASHSEED": "1"})
assert not outcome["called"], "an explicit PYTHONHASHSEED must never be overridden"
Expand Down
Loading