| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- """Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only."""
- from __future__ import annotations
- import logging
- import os
- import re
- import sys
- import winreg
- from logging import basicConfig, getLogger
- from typing import TYPE_CHECKING, Any, Final
- if TYPE_CHECKING:
- from collections.abc import Generator
- _RegistrySpec = tuple[str, int | None, int | None, int, bool, str, str | None]
- _LOGGER: Final[logging.Logger] = getLogger(__name__)
- _ARCH_RE: Final[re.Pattern[str]] = re.compile(
- r"""
- ^
- (\d+) # bitness number
- bit # literal suffix
- $
- """,
- re.VERBOSE,
- )
- _VERSION_RE: Final[re.Pattern[str]] = re.compile(
- r"""
- ^
- (\d+) # major
- (?:\.(\d+))? # optional minor
- (?:\.(\d+))? # optional micro
- $
- """,
- re.VERBOSE,
- )
- _THREADED_TAG_RE: Final[re.Pattern[str]] = re.compile(
- r"""
- ^
- \d+ # major
- (\.\d+){0,2} # optional minor/micro
- t # free-threaded flag
- $
- """,
- re.VERBOSE | re.IGNORECASE,
- )
- def enum_keys(key: Any) -> Generator[str, None, None]: # noqa: ANN401
- at = 0
- while True:
- try:
- yield winreg.EnumKey(key, at) # ty: ignore[unresolved-attribute]
- except OSError:
- break
- at += 1
- def get_value(key: Any, value_name: str | None) -> Any: # noqa: ANN401
- try:
- return winreg.QueryValueEx(key, value_name)[0] # ty: ignore[unresolved-attribute]
- except OSError:
- return None
- def discover_pythons() -> Generator[_RegistrySpec, None, None]:
- for hive, hive_name, key, flags, default_arch in [
- (winreg.HKEY_CURRENT_USER, "HKEY_CURRENT_USER", r"Software\Python", 0, 64), # ty: ignore[unresolved-attribute]
- (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_64KEY, 64), # ty: ignore[unresolved-attribute]
- (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_32KEY, 32), # ty: ignore[unresolved-attribute]
- ]:
- yield from process_set(hive, hive_name, key, flags, default_arch)
- def process_set(
- hive: int,
- hive_name: str,
- key: str,
- flags: int,
- default_arch: int,
- ) -> Generator[_RegistrySpec, None, None]:
- try:
- with winreg.OpenKeyEx(hive, key, 0, winreg.KEY_READ | flags) as root_key: # ty: ignore[unresolved-attribute]
- for company in enum_keys(root_key):
- if company == "PyLauncher": # reserved
- continue
- yield from process_company(hive_name, company, root_key, default_arch)
- except OSError:
- pass
- def process_company(
- hive_name: str,
- company: str,
- root_key: Any, # noqa: ANN401
- default_arch: int,
- ) -> Generator[_RegistrySpec, None, None]:
- with winreg.OpenKeyEx(root_key, company) as company_key: # ty: ignore[unresolved-attribute]
- for tag in enum_keys(company_key):
- spec = process_tag(hive_name, company, company_key, tag, default_arch)
- if spec is not None:
- yield spec
- def process_tag(hive_name: str, company: str, company_key: Any, tag: str, default_arch: int) -> _RegistrySpec | None: # noqa: ANN401
- with winreg.OpenKeyEx(company_key, tag) as tag_key: # ty: ignore[unresolved-attribute]
- version = load_version_data(hive_name, company, tag, tag_key)
- if version is not None: # if failed to get version bail
- 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)
- if exe_data is not None:
- exe, args = exe_data
- threaded = load_threaded(hive_name, company, tag, tag_key)
- return company, major, minor, arch, threaded, exe, args
- return None
- return None
- return None
- def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[str, str | None] | None: # noqa: ANN401
- key_path = f"{hive_name}/{company}/{tag}"
- try:
- with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
- 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")
- 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
- msg(key_path, f"could not load exe with value {exe}")
- except OSError:
- msg(f"{key_path}/InstallPath", "missing")
- return None
- def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # noqa: ANN401
- arch_str = get_value(tag_key, "SysArchitecture")
- if arch_str is not None:
- key_path = f"{hive_name}/{company}/{tag}/SysArchitecture"
- try:
- return parse_arch(arch_str)
- except ValueError as sys_arch:
- msg(key_path, sys_arch)
- return default_arch
- def parse_arch(arch_str: Any) -> int: # noqa: ANN401
- if isinstance(arch_str, str):
- if match := _ARCH_RE.match(arch_str):
- return int(next(iter(match.groups())))
- error = f"invalid format {arch_str}"
- else:
- error = f"arch is not string: {arch_str!r}"
- raise ValueError(error)
- def load_version_data(
- hive_name: str,
- company: str,
- tag: str,
- tag_key: Any, # noqa: ANN401
- ) -> tuple[int | None, int | None, int | None] | None:
- for candidate, key_path in [
- (get_value(tag_key, "SysVersion"), f"{hive_name}/{company}/{tag}/SysVersion"),
- (tag, f"{hive_name}/{company}/{tag}"),
- ]:
- if candidate is not None:
- try:
- return parse_version(candidate)
- except ValueError as sys_version:
- msg(key_path, sys_version)
- return None
- def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]: # noqa: ANN401
- if isinstance(version_str, str):
- if match := _VERSION_RE.match(version_str):
- g1, g2, g3 = match.groups()
- return (
- int(g1) if g1 is not None else None,
- int(g2) if g2 is not None else None,
- int(g3) if g3 is not None else None,
- )
- error = f"invalid format {version_str}"
- else:
- error = f"version is not string: {version_str!r}"
- raise ValueError(error)
- def load_threaded(hive_name: str, company: str, tag: str, tag_key: Any) -> bool: # noqa: ANN401
- display_name = get_value(tag_key, "DisplayName")
- if display_name is not None:
- if isinstance(display_name, str):
- if "freethreaded" in display_name.lower():
- return True
- else:
- key_path = f"{hive_name}/{company}/{tag}/DisplayName"
- msg(key_path, f"display name is not string: {display_name!r}")
- return bool(_THREADED_TAG_RE.match(tag))
- def msg(path: str, what: object) -> None:
- _LOGGER.warning("PEP-514 violation in Windows Registry at %s error: %s", path, what)
- def _run() -> None:
- basicConfig()
- interpreters = [repr(spec) for spec in discover_pythons()]
- sys.stdout.write("\n".join(sorted(interpreters)))
- sys.stdout.write("\n")
- if __name__ == "__main__":
- _run()
|