Skip to content
Merged
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
71 changes: 49 additions & 22 deletions desktop/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,31 +828,58 @@ def _bootstrap_python_platform(cache_file: Optional[Path]) -> str:


def _known_good_python() -> Optional[str]:
"""The pinned per-user python.org interpreter, if it is on disk and
passes the usual probe — the exact path `_winget_install_python`
installs to.

Addressed by path, NOT by re-probing: `_bootstrap_python()` tries
`py` first, and the `py` launcher resolves to the NEWEST installed
interpreter, which in the case this exists to fix is precisely the
one that has no wheels. A re-probe would hand back the broken
interpreter and the retry would fail identically."""
"""The pinned python.org interpreter, if it is on disk and passes the
usual probe.

Checks the exact per-user path ``_winget_install_python`` writes first,
then the Windows Registry (HKCU before HKLM) where the python.org
installer records the installation directory for every minor version it
manages. Both paths are exact filesystem paths — no py launcher, no
re-probing."""
if platform.system() != "Windows":
return None
exe = (Path(os.environ.get("LOCALAPPDATA", ""))
/ "Programs" / "Python" / KNOWN_GOOD_PYTHON_DIRNAME / "python.exe")
if not exe.exists():
return None
candidates = []
# Primary: the per-user path _winget_install_python writes.
per_user = (Path(os.environ.get("LOCALAPPDATA", ""))
/ "Programs" / "Python" / KNOWN_GOOD_PYTHON_DIRNAME
/ "python.exe")
if per_user.exists():
candidates.append(str(per_user))
# Secondary: the registered install path. The python.org installer
# writes SOFTWARE\Python\PythonCore\<minor>\InstallPath in HKCU
# (per-user install) and HKLM (system-wide install). Reading the
# registry directly gives the exact path without involving the py
# launcher or any re-probing. winreg is stdlib on Windows; the
# ImportError branch handles non-Windows test runners.
try:
r = subprocess.run(
[str(exe), "-c", _PYTHON_PROBE],
capture_output=True, text=True, timeout=20,
env=_child_env(), stdin=subprocess.DEVNULL,
**_win_subprocess_kwargs(),
)
return str(exe) if r.returncode == 0 else None
except Exception:
return None
import winreg # type: ignore[import]
reg_subkey = (rf"SOFTWARE\Python\PythonCore"
rf"\{KNOWN_GOOD_PYTHON_MINOR}\InstallPath")
for hive in (winreg.HKEY_CURRENT_USER, winreg.HKEY_LOCAL_MACHINE):
try:
with winreg.OpenKey(hive, reg_subkey) as k:
install_dir, _ = winreg.QueryValueEx(k, "")
exe = Path(install_dir) / "python.exe"
path = str(exe)
if path not in candidates and exe.exists():
candidates.append(path)
except OSError:
pass
except ImportError:
pass
for candidate in candidates:
try:
r = subprocess.run(
[candidate, "-c", _PYTHON_PROBE],
capture_output=True, text=True, timeout=20,
env=_child_env(), stdin=subprocess.DEVNULL,
**_win_subprocess_kwargs(),
)
if r.returncode == 0:
return candidate
except Exception:
pass
return None


def _winget_install_python(log: Callable[[str], None]) -> None:
Expand Down
70 changes: 70 additions & 0 deletions tests/test_desktop_bootstrap_resilience.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,76 @@ def test_interpreter_retry_is_windows_only(tmp_path, monkeypatch):
1, _NO_WHEEL_FOR_INTERPRETER) == (1, _NO_WHEEL_FOR_INTERPRETER)


def test_known_good_python_falls_back_to_registry_for_system_install(
tmp_path, monkeypatch):
"""Python 3.12 installed system-wide (registered in HKLM, not at the
per-user LOCALAPPDATA path) is found via the Windows Registry and
returned as an exact filesystem path — no py launcher involved."""
import types
_windows(monkeypatch)
monkeypatch.setenv("LOCALAPPDATA", "/no-such-localappdata")
monkeypatch.setattr(dapp, "_win_subprocess_kwargs", lambda: {})

system_exe = tmp_path / "python.exe"
system_exe.write_text("")

# Inject a fake winreg that reports tmp_path as the HKLM install dir.
fake_winreg = types.ModuleType("winreg")
fake_winreg.HKEY_CURRENT_USER = 0x80000001
fake_winreg.HKEY_LOCAL_MACHINE = 0x80000002

class _FakeKey:
def __enter__(self): return self
def __exit__(self, *a): return False

def _fake_open(hive, path):
if hive == fake_winreg.HKEY_LOCAL_MACHINE:
return _FakeKey()
raise OSError("not found")

def _fake_query(key, name):
# On Windows the registry stores the path with a trailing backslash;
# omit it here so Path(install_dir) / "python.exe" resolves correctly
# on the Linux CI runner (a backslash is not a separator on Linux).
return (str(tmp_path), 1)

fake_winreg.OpenKey = _fake_open
fake_winreg.QueryValueEx = _fake_query
monkeypatch.setitem(sys.modules, "winreg", fake_winreg)

import subprocess as _sp

def fake_run(cmd, **kwargs):
if cmd[0] == str(system_exe):
return _sp.CompletedProcess(cmd, 0, "", "")
return _sp.CompletedProcess(cmd, 1, "", "")

monkeypatch.setattr(dapp.subprocess, "run", fake_run)

result = dapp._known_good_python()
assert result == str(system_exe), (
"when the LOCALAPPDATA path is absent, _known_good_python() must "
"find a system-wide Python 3.12 via the Windows Registry"
)


def test_known_good_python_returns_none_when_no_candidates(monkeypatch):
"""When neither the per-user path nor any registry path provides a
usable Python 3.12, _known_good_python() returns None without spawning
any subprocess."""
_windows(monkeypatch)
monkeypatch.setenv("LOCALAPPDATA", "/no-such-localappdata")
monkeypatch.setattr(dapp, "_win_subprocess_kwargs", lambda: {})
# On Linux winreg is absent (ImportError caught inside the function),
# so no registry candidates are added. subprocess.run must not be called.
called = []
monkeypatch.setattr(dapp.subprocess, "run",
lambda *a, **k: called.append(a) or None)
result = dapp._known_good_python()
assert result is None
assert not called, "subprocess.run must not be called when no candidates exist"


def test_probe_caches_interpreter_version(tmp_path):
cache = tmp_path / "bootstrap-python.json"
py = dapp._bootstrap_python(cache)
Expand Down
Loading