diff --git a/src/virtualenv/discovery/windows/pep514.py b/src/virtualenv/discovery/windows/pep514.py index a75dad36d..80048f92f 100644 --- a/src/virtualenv/discovery/windows/pep514.py +++ b/src/virtualenv/discovery/windows/pep514.py @@ -56,40 +56,60 @@ def process_company(hive_name, company, root_key, default_arch): def process_tag(hive_name, company, company_key, tag, default_arch): + LOGGER.info("Processing tag %s for company %s", tag, company) with winreg.OpenKeyEx(company_key, tag) as tag_key: version = load_version_data(hive_name, company, tag, tag_key) if version is not None: # if failed to get version bail major, minor, _ = version + LOGGER.info("Found version %s.%s", major, minor) arch = load_arch_data(hive_name, company, tag, tag_key, default_arch) if arch is not None: - exe_data = load_exe(hive_name, company, company_key, tag) + LOGGER.info("Found arch %s", arch) + exe_data = load_exe(hive_name, company, company_key, tag, major) if exe_data is not None: exe, args = exe_data + LOGGER.info("Found exe %s", exe) threaded = load_threaded(hive_name, company, tag, tag_key) return company, major, minor, arch, threaded, exe, args - return None - return None - return None + return None -def load_exe(hive_name, company, company_key, tag): +def load_exe(hive_name, company, company_key, tag, major): key_path = f"{hive_name}/{company}/{tag}" + LOGGER.info("Loading exe for %s", key_path) 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") + LOGGER.info("ExecutablePath from registry: %s", exe) 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, major) + LOGGER.info("Executable from install path: %s", exe) - else: - exe = os.path.join(ip, "python.exe") if exe is not None and os.path.exists(exe): args = get_value(ip_key, "ExecutableArguments") + LOGGER.info("Found executable %s with args %s", exe, args) return exe, args msg(key_path, f"could not load exe with value {exe}") except OSError: msg(f"{key_path}/InstallPath", "missing") + LOGGER.info("Failed to load exe for %s", key_path) + return None + + +def _find_exe_in_install_path(ip_key, key_path, major): + ip = get_value(ip_key, None) + if ip is None: + msg(key_path, "no ExecutablePath or default for it") + return None + + names = ("python.exe",) + if major == 3: + names = ("python3.exe", "python.exe") + + for name in names: + exe_path = os.path.join(ip, name) + if os.path.exists(exe_path): + return exe_path return None diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index 0498352aa..03bd96b16 100644 --- a/tests/unit/discovery/windows/test_windows_pep514.py +++ b/tests/unit/discovery/windows/test_windows_pep514.py @@ -80,7 +80,7 @@ def test_pep514(): ), ("CompanyA", 3, 6, 64, False, "Z:\\CompanyA\\Python\\3.6\\python.exe", None), ("PythonCore", 2, 7, 64, False, "C:\\Python27\\python.exe", None), - ("PythonCore", 3, 7, 64, False, "C:\\Python37\\python.exe", None), + ("PythonCore", 3, 7, 64, False, "C:\\Python37\\python3.exe", None), ] @@ -100,7 +100,7 @@ def test_pep514_run(capsys, caplog): ('PythonCore', 3, 10, 32, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python310-32\\python.exe', None) ('PythonCore', 3, 12, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python.exe', None) ('PythonCore', 3, 13, 64, True, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python313\\python3.13t.exe', None) - ('PythonCore', 3, 7, 64, False, 'C:\\Python37\\python.exe', None) + ('PythonCore', 3, 7, 64, False, 'C:\\Python37\\python3.exe', None) ('PythonCore', 3, 8, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python38\\python.exe', None) ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python.exe', None) ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python.exe', None) @@ -121,3 +121,54 @@ 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 import pep514 + from virtualenv.discovery.windows.pep514 import winreg + + # Create a mock python3.exe, but no python.exe + python3_exe = tmp_path / "python3.exe" + python3_exe.touch() + mocker.patch("os.path.exists", side_effect=lambda p: str(p) == str(python3_exe)) + + # Mock winreg functions to simulate a single Python installation + mock_key = mocker.MagicMock() + + def open_key_ex(key, sub_key, *args, **kwargs): + if sub_key == r"Software\Python": + return "Python" + if key == "Python" and sub_key == "PythonCore": + return "PythonCore" + if key == "PythonCore" and sub_key == "3.7-32": + return "3.7-32" + if key == "3.7-32" and sub_key == "InstallPath": + return mock_key + raise FileNotFoundError + + mocker.patch.object(winreg, "OpenKeyEx", side_effect=open_key_ex) + + def enum_key(key, at): + if key == "Python" and at == 0: + return "PythonCore" + if key == "PythonCore" and at == 0: + return "3.7-32" + raise StopIteration + + mocker.patch.object(winreg, "EnumKey", side_effect=enum_key) + + def get_value(key, name): + if name == "ExecutablePath": + raise FileNotFoundError + if name is None: + return str(tmp_path) + return "3.7" + + mocker.patch.object(winreg, "QueryValueEx", side_effect=get_value) + mocker.patch.object(pep514, "load_arch_data", return_value=32) + mocker.patch.object(pep514, "load_threaded", return_value=False) + + interpreters = list(pep514.discover_pythons()) + + assert interpreters == [("PythonCore", 3, 7, 32, False, str(python3_exe), None)]