Skip to content
Closed
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
42 changes: 31 additions & 11 deletions src/virtualenv/discovery/windows/pep514.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
55 changes: 53 additions & 2 deletions tests/unit/discovery/windows/test_windows_pep514.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]


Expand All @@ -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)
Expand All @@ -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)]
Loading