Skip to content

Commit e04153a

Browse files
google-labs-jules[bot]esafak
authored andcommitted
feat: Support python.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`. This allows virtualenv to discover Python on Windows installations that use the `python.exe` executable name. Fixes #2774 Signed-off-by: Emre Şafak <[email protected]>
1 parent fb2ba1c commit e04153a

File tree

3 files changed

+51
-6
lines changed

3 files changed

+51
-6
lines changed

docs/changelog/2774.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fall back to python.exe if python3.exe is not found on Windows.
2+
Contributed by :user:`esafak`.

src/virtualenv/discovery/windows/pep514.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,11 @@ def process_tag(hive_name, company, company_key, tag, default_arch):
7575
def load_exe(hive_name, company, company_key, tag):
7676
key_path = f"{hive_name}/{company}/{tag}"
7777
try:
78-
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key:
78+
with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key:
7979
exe = get_value(ip_key, "ExecutablePath")
8080
if exe is None:
81-
ip = get_value(ip_key, None)
82-
if ip is None:
83-
msg(key_path, "no ExecutablePath or default for it")
81+
exe = _find_exe_in_install_path(ip_key, key_path)
8482

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

9591

92+
def _find_exe_in_install_path(ip_key, key_path):
93+
ip = get_value(ip_key, None)
94+
if ip is None:
95+
msg(key_path, "no ExecutablePath or default for it")
96+
return None
97+
98+
for name in ("python3.exe", "python.exe"):
99+
exe_path = os.path.join(ip, name)
100+
if os.path.exists(exe_path):
101+
return exe_path
102+
return None
103+
104+
96105
def load_arch_data(hive_name, company, tag, tag_key, default_arch):
97106
arch_str = get_value(tag_key, "SysArchitecture")
98107
if arch_str is not None:

tests/unit/discovery/windows/test_windows_pep514.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,37 @@ def test_pep514_run(capsys, caplog):
121121
f"{prefix}HKEY_CURRENT_USER/PythonCore/3.X error: invalid format 3.X",
122122
]
123123
assert caplog.messages == expected_logs
124+
125+
126+
@pytest.mark.skipif(sys.platform != "win32", reason="no Windows registry")
127+
def test_pep514_python3_fallback(mocker, tmp_path):
128+
from virtualenv.discovery.windows.pep514 import discover_pythons, winreg # noqa: PLC0415
129+
130+
# Create a mock python3.exe
131+
python3_exe = tmp_path / "python3.exe"
132+
python3_exe.touch()
133+
134+
# Mock the registry to return our test python distribution
135+
def open_key_ex(key, sub_key, *args, **kwargs):
136+
if sub_key == r"Software\Python\PythonCore\3.9-32":
137+
return mocker.MagicMock()
138+
if sub_key == r"Software\Python\PythonCore\3.9-32\InstallPath":
139+
return mocker.MagicMock()
140+
return winreg.OpenKeyEx(key, sub_key, *args, **kwargs)
141+
142+
def query_value_ex(key, value_name, *args, **kwargs):
143+
if value_name == "ExecutablePath":
144+
return None # No executable path, forcing a fallback
145+
if value_name is None:
146+
return str(tmp_path)
147+
return winreg.QueryValueEx(key, value_name, *args, **kwargs)
148+
149+
mocker.patch("winreg.OpenKeyEx", side_effect=open_key_ex)
150+
mocker.patch("winreg.QueryValueEx", side_effect=query_value_ex)
151+
mocker.patch("os.path.exists", return_value=True)
152+
153+
# Mock EnumKey to inject our test distribution
154+
mocker.patch("winreg.EnumKey", return_value=["3.9-32"])
155+
156+
interpreters = list(discover_pythons())
157+
assert any(str(python3_exe) in i for i in interpreters)

0 commit comments

Comments
 (0)