Skip to content
Open
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
17 changes: 13 additions & 4 deletions graphify/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,20 @@
done
fi
# Last resort: try python3 / python (works for system/venv installs on PATH).
# Under Git for Windows `python3` is usually the Microsoft Store App Execution Alias
# (python.org's installer ships no python3.exe): running it flashes a console window
# for ~1s and exits 9009, on every commit, checkout and merge. Try `python` first
# there (#4563).
if [ -z "$GRAPHIFY_PYTHON" ]; then
if command -v python3 >/dev/null 2>&1 && python3 -c "$_GFY_PROBE" 2>/dev/null; then
GRAPHIFY_PYTHON="python3"
elif command -v python >/dev/null 2>&1 && python -c "$_GFY_PROBE" 2>/dev/null; then
GRAPHIFY_PYTHON="python"
if [ -n "${WINDIR:-}" ] || [ -n "${MSYSTEM:-}" ]; then
_GFY_FIRST="python"; _GFY_SECOND="python3"
else
_GFY_FIRST="python3"; _GFY_SECOND="python"
fi
if command -v "$_GFY_FIRST" >/dev/null 2>&1 && "$_GFY_FIRST" -c "$_GFY_PROBE" 2>/dev/null; then
GRAPHIFY_PYTHON="$_GFY_FIRST"
elif command -v "$_GFY_SECOND" >/dev/null 2>&1 && "$_GFY_SECOND" -c "$_GFY_PROBE" 2>/dev/null; then
GRAPHIFY_PYTHON="$_GFY_SECOND"
else
echo "[graphify hook] could not locate a Python with graphify installed. Add the graphify bin dir to PATH or re-run 'graphify hook install' from the env where graphify lives." >&2
exit 0
Expand Down
97 changes: 97 additions & 0 deletions tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2040,3 +2040,100 @@ def _git(*args):
got = [_unquote_git_path(l) for l in raw]
assert sorted(got) == sorted(names)
assert all((tmp_path / g).is_file() for g in got)


# ── #4563: the last-resort probe must not run the Microsoft Store python3 alias first on Windows ──

def _last_resort_run(tmp_path, env_extra=None, python3="stub", python="real"):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _last_resort_run()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

"""Run the emitted _PYTHON_DETECT under a real sh on a machine where probes 1-4 all
miss (temp HOME, dead pin, no launcher) and only `python3` / `python` on PATH remain.

PATH holds nothing but the two fake dirs, so no real interpreter can be found by
accident. python3 / python are each one of: "stub" (the Store alias: logs its call,
exits 9009), "real" (logs, then execs the interpreter running this test, so the
find_spec probe passes), "none" (cannot import graphify), or None (not on PATH).
Returns (CompletedProcess, [names called, in order])."""
import graphify
from graphify.hooks import _PYTHON_DETECT

home = tmp_path / "home"
home.mkdir()
calls = tmp_path / "calls.log"
store, bindir = tmp_path / "WindowsApps", tmp_path / "bin"
store.mkdir()
bindir.mkdir()

def fake(directory, name, kind):
if kind is None:
return
body = {
"stub": "exit 9009\n",
"real": f'exec "{sys.executable}" "$@"\n',
"none": "exit 1\n",
}[kind]
py = directory / name
py.write_text(f'#!/bin/sh\necho {name} >> "$GFY_CALLS"\n{body}', encoding="utf-8", newline="\n")
py.chmod(0o755)

fake(store, "python3", python3) # the stub sits first on PATH, as WindowsApps does
fake(bindir, "python", python)
script = tmp_path / "detect_run.sh"
script.write_text(_PYTHON_DETECT + '\necho "RESOLVED=$GRAPHIFY_PYTHON"\n', encoding="utf-8", newline="\n")
env = {k: v for k, v in os.environ.items() if k not in ("WINDIR", "MSYSTEM", "UV_TOOL_DIR")}
env.update(HOME=str(home), GFY_CALLS=str(calls), PATH=os.pathsep.join([str(store), str(bindir)]),
PYTHONPATH=str(Path(graphify.__file__).resolve().parent.parent))
env.update(env_extra or {})
res = subprocess.run([shutil.which("sh"), script.name], capture_output=True, text=True,
cwd=str(tmp_path), env=env)
return res, (calls.read_text(encoding="utf-8").split() if calls.exists() else [])


_NEEDS_SH = pytest.mark.skipif(shutil.which("sh") is None or os.name == "nt",
reason="runs the emitted probe chain under a POSIX sh with sh-script fakes")


@_NEEDS_SH
@pytest.mark.parametrize("env", [{"MSYSTEM": "MINGW64"}, {"WINDIR": "C:\\Windows"}], ids=["msystem", "windir"])
def test_windows_never_runs_the_store_python3_alias_when_python_works(tmp_path, env):
"""#4563: on Windows `python3` is usually the Microsoft Store App Execution Alias
(no python3.exe ships with python.org's installer). Running it flashes a console
window for ~1s and exits 9009, on every commit, checkout and merge. Under Git for
Windows `python` is tried first, so the stub is never reached."""
res, calls = _last_resort_run(tmp_path, env)
assert res.returncode == 0, res.stderr
assert "RESOLVED=python\n" in res.stdout, res.stdout + res.stderr
assert calls == ["python"], f"the Store alias was run: {calls}"


@_NEEDS_SH
def test_outside_windows_python3_is_still_tried_first(tmp_path):
"""Linux and macOS keep the order they had: python3, then python."""
res, calls = _last_resort_run(tmp_path)
assert res.returncode == 0, res.stderr
assert "RESOLVED=python\n" in res.stdout, res.stdout + res.stderr
assert calls == ["python3", "python"]


@_NEEDS_SH
def test_windows_falls_back_to_python3_when_python_lacks_graphify(tmp_path):
res, calls = _last_resort_run(tmp_path, {"MSYSTEM": "MINGW64"}, python3="real", python="none")
assert res.returncode == 0, res.stderr
assert "RESOLVED=python3\n" in res.stdout, res.stdout + res.stderr
assert calls == ["python", "python3"], f"python is tried first, python3 after it: {calls}"


@_NEEDS_SH
def test_windows_with_no_working_python_still_fails_loudly_and_exits_zero(tmp_path):
res, calls = _last_resort_run(tmp_path, {"MSYSTEM": "MINGW64"}, python="none")
assert res.returncode == 0, res.stderr
assert "could not locate a Python" in res.stderr
assert res.stdout == "", "the chain must stop at the warning"
assert sorted(calls) == ["python", "python3"]


@pytest.mark.parametrize("name,script", _HOOK_SCRIPTS, ids=[n for n, _ in _HOOK_SCRIPTS])
def test_every_hook_embeds_the_probe_chain_unchanged(name, script):
"""All four generated hooks splice in the same _PYTHON_DETECT, so the behavioural
tests above are the behaviour of the real hooks (#4563)."""
from graphify.hooks import _PYTHON_DETECT
assert _PYTHON_DETECT in script, f"{name} does not embed _PYTHON_DETECT"
Loading