Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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: 2 additions & 0 deletions docs/changelog/2774.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fall back to python.exe if python3.exe is not found on Windows.
Contributed by :user:`esafak`.
21 changes: 15 additions & 6 deletions src/virtualenv/discovery/windows/pep514.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,11 @@ def process_tag(hive_name, company, company_key, tag, default_arch):
def load_exe(hive_name, company, company_key, tag):
key_path = f"{hive_name}/{company}/{tag}"
try:
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key:
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key:
exe = get_value(ip_key, "ExecutablePath")
if exe is None:
ip = get_value(ip_key, None)
if ip is None:
msg(key_path, "no ExecutablePath or default for it")
exe = _find_exe_in_install_path(ip_key, key_path)

else:
exe = os.path.join(ip, "python.exe")
if exe is not None and os.path.exists(exe):
args = get_value(ip_key, "ExecutableArguments")
return exe, args
Expand All @@ -93,6 +89,19 @@ def load_exe(hive_name, company, company_key, tag):
return None


def _find_exe_in_install_path(ip_key, key_path):
ip = get_value(ip_key, None)
if ip is None:
msg(key_path, "no ExecutablePath or default for it")
return None

for name in ("python3.exe", "python.exe"):
exe_path = os.path.join(ip, name)
if os.path.exists(exe_path):
return exe_path
return None


def load_arch_data(hive_name, company, tag, tag_key, default_arch):
arch_str = get_value(tag_key, "SysArchitecture")
if arch_str is not None:
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/discovery/windows/test_windows_pep514.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,37 @@ def test_pep514_run(capsys, caplog):
f"{prefix}HKEY_CURRENT_USER/PythonCore/3.X error: invalid format 3.X",
]
assert caplog.messages == expected_logs


@pytest.mark.skipif(sys.platform != "win32", reason="no Windows registry")
def test_pep514_python3_fallback(mocker, tmp_path):
from virtualenv.discovery.windows.pep514 import discover_pythons, winreg # noqa: PLC0415

# Create a mock python3.exe
python3_exe = tmp_path / "python3.exe"
python3_exe.touch()

# Mock the registry to return our test python distribution
def open_key_ex(key, sub_key, *args, **kwargs):
if sub_key == r"Software\Python\PythonCore\3.9-32":
return mocker.MagicMock()
if sub_key == r"Software\Python\PythonCore\3.9-32\InstallPath":
return mocker.MagicMock()
return winreg.OpenKeyEx(key, sub_key, *args, **kwargs)

def query_value_ex(key, value_name, *args, **kwargs):
if value_name == "ExecutablePath":
return None # No executable path, forcing a fallback
if value_name is None:
return str(tmp_path)
return winreg.QueryValueEx(key, value_name, *args, **kwargs)

mocker.patch("winreg.OpenKeyEx", side_effect=open_key_ex)
mocker.patch("winreg.QueryValueEx", side_effect=query_value_ex)
mocker.patch("os.path.exists", return_value=True)

# Mock EnumKey to inject our test distribution
mocker.patch("winreg.EnumKey", return_value=["3.9-32"])

interpreters = list(discover_pythons())
assert any(str(python3_exe) in i for i in interpreters)
Loading