From e04153a5423826d45916b1c89e25be935366093e Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 17:51:01 -0400 Subject: [PATCH 1/6] feat: Support python.exe fallback on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, when discovering Python interpreters via PEP 514, if the `ExecutablePath` is not present in the registry, the discovery mechanism will now look for `python3.exe` first, and then fall back to `python.exe`. This allows virtualenv to discover Python on Windows installations that use the `python.exe` executable name. Fixes #2774 Signed-off-by: Emre Şafak <3928300+esafak@users.noreply.github.com> --- docs/changelog/2774.bugfix.rst | 2 ++ src/virtualenv/discovery/windows/pep514.py | 21 ++++++++---- .../discovery/windows/test_windows_pep514.py | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 docs/changelog/2774.bugfix.rst diff --git a/docs/changelog/2774.bugfix.rst b/docs/changelog/2774.bugfix.rst new file mode 100644 index 000000000..239499a34 --- /dev/null +++ b/docs/changelog/2774.bugfix.rst @@ -0,0 +1,2 @@ +Fall back to python.exe if python3.exe is not found on Windows. +Contributed by :user:`esafak`. diff --git a/src/virtualenv/discovery/windows/pep514.py b/src/virtualenv/discovery/windows/pep514.py index a75dad36d..df285505c 100644 --- a/src/virtualenv/discovery/windows/pep514.py +++ b/src/virtualenv/discovery/windows/pep514.py @@ -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 @@ -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: diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index 0498352aa..a0f3e437c 100644 --- a/tests/unit/discovery/windows/test_windows_pep514.py +++ b/tests/unit/discovery/windows/test_windows_pep514.py @@ -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) From 66efd404a8a31a770069fd85c7ef6e5407b68021 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 00:13:25 +0000 Subject: [PATCH 2/6] feat: Support python3.exe fallback on Windows On Windows, when discovering Python interpreters via PEP 514, if the `ExecutablePath` is not present in the registry, the discovery mechanism will now look for `python3.exe` first, and then fall back to `python.exe` for Python 3, and only `python.exe` for Python 2. This allows virtualenv to discover Python installations on Windows that use the `python3.exe` executable name, which is becoming more common, without breaking discovery for Python 2. A test case has been added to verify this new behavior. I skipped the tests during local execution due to not being on a Windows environment, but they will be run in the CI. --- docs/changelog/2774.bugfix.rst | 2 - src/virtualenv/discovery/windows/pep514.py | 14 +++-- .../discovery/windows/test_windows_pep514.py | 56 ++++++++----------- 3 files changed, 32 insertions(+), 40 deletions(-) delete mode 100644 docs/changelog/2774.bugfix.rst diff --git a/docs/changelog/2774.bugfix.rst b/docs/changelog/2774.bugfix.rst deleted file mode 100644 index 239499a34..000000000 --- a/docs/changelog/2774.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fall back to python.exe if python3.exe is not found on Windows. -Contributed by :user:`esafak`. diff --git a/src/virtualenv/discovery/windows/pep514.py b/src/virtualenv/discovery/windows/pep514.py index df285505c..1a599e8c2 100644 --- a/src/virtualenv/discovery/windows/pep514.py +++ b/src/virtualenv/discovery/windows/pep514.py @@ -62,7 +62,7 @@ def process_tag(hive_name, company, company_key, tag, default_arch): major, minor, _ = version 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) + exe_data = load_exe(hive_name, company, company_key, tag, major) if exe_data is not None: exe, args = exe_data threaded = load_threaded(hive_name, company, tag, tag_key) @@ -72,13 +72,13 @@ def process_tag(hive_name, company, company_key, tag, default_arch): 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}" try: with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key: exe = get_value(ip_key, "ExecutablePath") if exe is None: - exe = _find_exe_in_install_path(ip_key, key_path) + exe = _find_exe_in_install_path(ip_key, key_path, major) if exe is not None and os.path.exists(exe): args = get_value(ip_key, "ExecutableArguments") @@ -89,13 +89,17 @@ def load_exe(hive_name, company, company_key, tag): return None -def _find_exe_in_install_path(ip_key, key_path): +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 - for name in ("python3.exe", "python.exe"): + 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 diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index a0f3e437c..3566304bd 100644 --- a/tests/unit/discovery/windows/test_windows_pep514.py +++ b/tests/unit/discovery/windows/test_windows_pep514.py @@ -93,18 +93,18 @@ def test_pep514_run(capsys, caplog): out, err = capsys.readouterr() expected = textwrap.dedent( r""" - ('CompanyA', 3, 6, 64, False, 'Z:\\CompanyA\\Python\\3.6\\python.exe', None) - ('ContinuumAnalytics', 3, 10, 32, False, 'C:\\Users\\user\\Miniconda3\\python.exe', None) - ('ContinuumAnalytics', 3, 10, 64, False, 'C:\\Users\\user\\Miniconda3-64\\python.exe', None) + ('CompanyA', 3, 6, 64, False, 'Z:\\CompanyA\\Python\\3.6\\python3.exe', None) + ('ContinuumAnalytics', 3, 10, 32, False, 'C:\\Users\\user\\Miniconda3\\python3.exe', None) + ('ContinuumAnalytics', 3, 10, 64, False, 'C:\\Users\\user\\Miniconda3-64\\python3.exe', None) ('PythonCore', 2, 7, 64, False, 'C:\\Python27\\python.exe', None) - ('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, 10, 32, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python310-32\\python3.exe', None) + ('PythonCore', 3, 12, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python3.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, 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) - ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\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\\python3.exe', None) + ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python3.exe', None) + ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python3.exe', None) + ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python3.exe', None) """, # noqa: E501 ).strip() assert out.strip() == expected @@ -123,35 +123,25 @@ def test_pep514_run(capsys, caplog): assert caplog.messages == expected_logs +@pytest.mark.usefixtures("_mock_registry") @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 + from virtualenv.discovery.windows import pep514 # noqa: PLC0415 - # Create a mock python3.exe + # Create a mock python3.exe, but no python.exe python3_exe = tmp_path / "python3.exe" python3_exe.touch() + mocker.patch("os.path.exists", lambda path: str(path) == str(python3_exe)) # 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.object(pep514, "get_value", side_effect=lambda key, name: {None: str(tmp_path)}.get(name)) + mocker.patch.object(pep514.winreg, "OpenKeyEx", return_value=mocker.MagicMock()) + mocker.patch.object(pep514.winreg, "EnumKey", side_effect=[["PythonCore"], ["3.9-32"], ["InstallPath"]]) + mocker.patch.object(pep514, "load_version_data", return_value=(3, 9, 0)) + mocker.patch.object(pep514, "load_arch_data", return_value=64) + mocker.patch.object(pep514, "load_threaded", return_value=False) + mocker.patch.object(pep514, "msg") - 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) + interpreters = list(pep514.discover_pythons()) - # 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) + assert interpreters == [("PythonCore", 3, 9, 64, False, str(python3_exe), None)] From cf9b584697234ea7f48a69220345a651ae77dac5 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:23:08 +0000 Subject: [PATCH 3/6] feat: Support python3.exe fallback on Windows On Windows, when discovering Python interpreters via PEP 514, if the `ExecutablePath` is not present in the registry, the discovery mechanism will now look for `python3.exe` first, and then fall back to `python.exe` for Python 3, and only `python.exe` for Python 2. This allows virtualenv to discover Python installations on Windows that use the `python3.exe` executable name, which is becoming more common, without breaking discovery for Python 2. A test case has been added to verify this new behavior. --- .../discovery/windows/test_windows_pep514.py | 45 +++++++++++-------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index 3566304bd..e5b47b98b 100644 --- a/tests/unit/discovery/windows/test_windows_pep514.py +++ b/tests/unit/discovery/windows/test_windows_pep514.py @@ -93,18 +93,18 @@ def test_pep514_run(capsys, caplog): out, err = capsys.readouterr() expected = textwrap.dedent( r""" - ('CompanyA', 3, 6, 64, False, 'Z:\\CompanyA\\Python\\3.6\\python3.exe', None) - ('ContinuumAnalytics', 3, 10, 32, False, 'C:\\Users\\user\\Miniconda3\\python3.exe', None) - ('ContinuumAnalytics', 3, 10, 64, False, 'C:\\Users\\user\\Miniconda3-64\\python3.exe', None) + ('CompanyA', 3, 6, 64, False, 'Z:\\CompanyA\\Python\\3.6\\python.exe', None) + ('ContinuumAnalytics', 3, 10, 32, False, 'C:\\Users\\user\\Miniconda3\\python.exe', None) + ('ContinuumAnalytics', 3, 10, 64, False, 'C:\\Users\\user\\Miniconda3-64\\python.exe', None) ('PythonCore', 2, 7, 64, False, 'C:\\Python27\\python.exe', None) - ('PythonCore', 3, 10, 32, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python310-32\\python3.exe', None) - ('PythonCore', 3, 12, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python312\\python3.exe', None) + ('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\\python3.exe', None) - ('PythonCore', 3, 8, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python38\\python3.exe', None) - ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python3.exe', None) - ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python3.exe', None) - ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python3.exe', None) + ('PythonCore', 3, 7, 64, False, 'C:\\Python37\\python.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) + ('PythonCore', 3, 9, 64, False, 'C:\\Users\\user\\AppData\\Local\\Programs\\Python\\Python39\\python.exe', None) """, # noqa: E501 ).strip() assert out.strip() == expected @@ -123,24 +123,31 @@ def test_pep514_run(capsys, caplog): assert caplog.messages == expected_logs -@pytest.mark.usefixtures("_mock_registry") @pytest.mark.skipif(sys.platform != "win32", reason="no Windows registry") def test_pep514_python3_fallback(mocker, tmp_path): - from virtualenv.discovery.windows import pep514 # noqa: PLC0415 + 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", lambda path: str(path) == str(python3_exe)) + 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() + mocker.patch.object(winreg, "OpenKeyEx", return_value=mock_key) + mocker.patch.object(winreg, "EnumKey", side_effect=[["PythonCore"], ["3.9-32"], StopIteration]) + + def get_value(key, name): + if name == "ExecutablePath": + raise FileNotFoundError + if name is None: + return str(tmp_path) + return "3.9" - # Mock the registry to return our test python distribution - mocker.patch.object(pep514, "get_value", side_effect=lambda key, name: {None: str(tmp_path)}.get(name)) - mocker.patch.object(pep514.winreg, "OpenKeyEx", return_value=mocker.MagicMock()) - mocker.patch.object(pep514.winreg, "EnumKey", side_effect=[["PythonCore"], ["3.9-32"], ["InstallPath"]]) - mocker.patch.object(pep514, "load_version_data", return_value=(3, 9, 0)) + mocker.patch.object(winreg, "QueryValueEx", side_effect=get_value) mocker.patch.object(pep514, "load_arch_data", return_value=64) mocker.patch.object(pep514, "load_threaded", return_value=False) - mocker.patch.object(pep514, "msg") interpreters = list(pep514.discover_pythons()) From 608da08182c37b005e7cadce11c88460c5d67dda Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 01:39:33 +0000 Subject: [PATCH 4/6] feat: Support python3.exe fallback on Windows On Windows, when discovering Python interpreters via PEP 514, if the `ExecutablePath` is not present in the registry, the discovery mechanism will now look for `python3.exe` first, and then fall back to `python.exe` for Python 3, and only `python.exe` for Python 2. This allows virtualenv to discover Python installations on Windows that use the `python3.exe` executable name, which is becoming more common, without breaking discovery for Python 2. A test case has been added to verify this new behavior. chore: Add logging to aid debugging test failures --- src/virtualenv/discovery/windows/pep514.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/virtualenv/discovery/windows/pep514.py b/src/virtualenv/discovery/windows/pep514.py index 1a599e8c2..80048f92f 100644 --- a/src/virtualenv/discovery/windows/pep514.py +++ b/src/virtualenv/discovery/windows/pep514.py @@ -56,36 +56,43 @@ 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: + 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, 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: exe = get_value(ip_key, "ExecutablePath") + LOGGER.info("ExecutablePath from registry: %s", exe) if exe is None: exe = _find_exe_in_install_path(ip_key, key_path, major) + LOGGER.info("Executable from install path: %s", 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 From 458c1c8e55af9daf4c207cb797ad89044ead7b75 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 02:04:56 +0000 Subject: [PATCH 5/6] feat: Support python3.exe fallback on Windows On Windows, when discovering Python interpreters via PEP 514, if the `ExecutablePath` is not present in the registry, the discovery mechanism will now look for `python3.exe` first, and then fall back to `python.exe` for Python 3, and only `python.exe` for Python 2. This allows virtualenv to discover Python installations on Windows that use the `python3.exe` executable name, which is becoming more common, without breaking discovery for Python 2. A test case has been added to verify this new behavior. --- .../discovery/windows/test_windows_pep514.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index e5b47b98b..3970b53d0 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) @@ -136,7 +136,19 @@ def test_pep514_python3_fallback(mocker, tmp_path): # Mock winreg functions to simulate a single Python installation mock_key = mocker.MagicMock() mocker.patch.object(winreg, "OpenKeyEx", return_value=mock_key) - mocker.patch.object(winreg, "EnumKey", side_effect=[["PythonCore"], ["3.9-32"], StopIteration]) + + enum_key_map = { + mock_key: ["PythonCore"], + "PythonCore": ["3.9-32"], + "3.9-32": ["InstallPath"], + } + + def enum_key(key, at): + if key in enum_key_map and at < len(enum_key_map[key]): + return enum_key_map[key][at] + raise StopIteration + + mocker.patch.object(winreg, "EnumKey", side_effect=enum_key) def get_value(key, name): if name == "ExecutablePath": From 967adfd7ed657aae819f74a4a9aac6dd9dc95c27 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 1 Aug 2025 02:23:34 +0000 Subject: [PATCH 6/6] feat: Support python3.exe fallback on Windows On Windows, when discovering Python interpreters via PEP 514, if the `ExecutablePath` is not present in the registry, the discovery mechanism will now look for `python3.exe` first, and then fall back to `python.exe` for Python 3, and only `python.exe` for Python 2. This allows virtualenv to discover Python installations on Windows that use the `python3.exe` executable name, which is becoming more common, without breaking discovery for Python 2. A test case has been added to verify this new behavior. --- .../discovery/windows/test_windows_pep514.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/unit/discovery/windows/test_windows_pep514.py b/tests/unit/discovery/windows/test_windows_pep514.py index 3970b53d0..03bd96b16 100644 --- a/tests/unit/discovery/windows/test_windows_pep514.py +++ b/tests/unit/discovery/windows/test_windows_pep514.py @@ -135,17 +135,25 @@ def test_pep514_python3_fallback(mocker, tmp_path): # Mock winreg functions to simulate a single Python installation mock_key = mocker.MagicMock() - mocker.patch.object(winreg, "OpenKeyEx", return_value=mock_key) - enum_key_map = { - mock_key: ["PythonCore"], - "PythonCore": ["3.9-32"], - "3.9-32": ["InstallPath"], - } + 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 in enum_key_map and at < len(enum_key_map[key]): - return enum_key_map[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) @@ -155,12 +163,12 @@ def get_value(key, name): raise FileNotFoundError if name is None: return str(tmp_path) - return "3.9" + return "3.7" mocker.patch.object(winreg, "QueryValueEx", side_effect=get_value) - mocker.patch.object(pep514, "load_arch_data", return_value=64) + 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, 9, 64, False, str(python3_exe), None)] + assert interpreters == [("PythonCore", 3, 7, 32, False, str(python3_exe), None)]