_pep514.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. """Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only."""
  2. from __future__ import annotations
  3. import logging
  4. import os
  5. import re
  6. import sys
  7. import winreg
  8. from logging import basicConfig, getLogger
  9. from typing import TYPE_CHECKING, Any, Final
  10. if TYPE_CHECKING:
  11. from collections.abc import Generator
  12. _RegistrySpec = tuple[str, int | None, int | None, int, bool, str, str | None]
  13. _LOGGER: Final[logging.Logger] = getLogger(__name__)
  14. _ARCH_RE: Final[re.Pattern[str]] = re.compile(
  15. r"""
  16. ^
  17. (\d+) # bitness number
  18. bit # literal suffix
  19. $
  20. """,
  21. re.VERBOSE,
  22. )
  23. _VERSION_RE: Final[re.Pattern[str]] = re.compile(
  24. r"""
  25. ^
  26. (\d+) # major
  27. (?:\.(\d+))? # optional minor
  28. (?:\.(\d+))? # optional micro
  29. $
  30. """,
  31. re.VERBOSE,
  32. )
  33. _THREADED_TAG_RE: Final[re.Pattern[str]] = re.compile(
  34. r"""
  35. ^
  36. \d+ # major
  37. (\.\d+){0,2} # optional minor/micro
  38. t # free-threaded flag
  39. $
  40. """,
  41. re.VERBOSE | re.IGNORECASE,
  42. )
  43. def enum_keys(key: Any) -> Generator[str, None, None]: # noqa: ANN401
  44. at = 0
  45. while True:
  46. try:
  47. yield winreg.EnumKey(key, at) # ty: ignore[unresolved-attribute]
  48. except OSError:
  49. break
  50. at += 1
  51. def get_value(key: Any, value_name: str | None) -> Any: # noqa: ANN401
  52. try:
  53. return winreg.QueryValueEx(key, value_name)[0] # ty: ignore[unresolved-attribute]
  54. except OSError:
  55. return None
  56. def discover_pythons() -> Generator[_RegistrySpec, None, None]:
  57. for hive, hive_name, key, flags, default_arch in [
  58. (winreg.HKEY_CURRENT_USER, "HKEY_CURRENT_USER", r"Software\Python", 0, 64), # ty: ignore[unresolved-attribute]
  59. (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_64KEY, 64), # ty: ignore[unresolved-attribute]
  60. (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_32KEY, 32), # ty: ignore[unresolved-attribute]
  61. ]:
  62. yield from process_set(hive, hive_name, key, flags, default_arch)
  63. def process_set(
  64. hive: int,
  65. hive_name: str,
  66. key: str,
  67. flags: int,
  68. default_arch: int,
  69. ) -> Generator[_RegistrySpec, None, None]:
  70. try:
  71. with winreg.OpenKeyEx(hive, key, 0, winreg.KEY_READ | flags) as root_key: # ty: ignore[unresolved-attribute]
  72. for company in enum_keys(root_key):
  73. if company == "PyLauncher": # reserved
  74. continue
  75. yield from process_company(hive_name, company, root_key, default_arch)
  76. except OSError:
  77. pass
  78. def process_company(
  79. hive_name: str,
  80. company: str,
  81. root_key: Any, # noqa: ANN401
  82. default_arch: int,
  83. ) -> Generator[_RegistrySpec, None, None]:
  84. with winreg.OpenKeyEx(root_key, company) as company_key: # ty: ignore[unresolved-attribute]
  85. for tag in enum_keys(company_key):
  86. spec = process_tag(hive_name, company, company_key, tag, default_arch)
  87. if spec is not None:
  88. yield spec
  89. def process_tag(hive_name: str, company: str, company_key: Any, tag: str, default_arch: int) -> _RegistrySpec | None: # noqa: ANN401
  90. with winreg.OpenKeyEx(company_key, tag) as tag_key: # ty: ignore[unresolved-attribute]
  91. version = load_version_data(hive_name, company, tag, tag_key)
  92. if version is not None: # if failed to get version bail
  93. major, minor, _ = version
  94. arch = load_arch_data(hive_name, company, tag, tag_key, default_arch)
  95. if arch is not None:
  96. exe_data = load_exe(hive_name, company, company_key, tag)
  97. if exe_data is not None:
  98. exe, args = exe_data
  99. threaded = load_threaded(hive_name, company, tag, tag_key)
  100. return company, major, minor, arch, threaded, exe, args
  101. return None
  102. return None
  103. return None
  104. def load_exe(hive_name: str, company: str, company_key: Any, tag: str) -> tuple[str, str | None] | None: # noqa: ANN401
  105. key_path = f"{hive_name}/{company}/{tag}"
  106. try:
  107. with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key: # ty: ignore[unresolved-attribute]
  108. exe = get_value(ip_key, "ExecutablePath")
  109. if exe is None:
  110. ip = get_value(ip_key, None)
  111. if ip is None:
  112. msg(key_path, "no ExecutablePath or default for it")
  113. else:
  114. exe = os.path.join(ip, "python.exe")
  115. if exe is not None and os.path.exists(exe):
  116. args = get_value(ip_key, "ExecutableArguments")
  117. return exe, args
  118. msg(key_path, f"could not load exe with value {exe}")
  119. except OSError:
  120. msg(f"{key_path}/InstallPath", "missing")
  121. return None
  122. def load_arch_data(hive_name: str, company: str, tag: str, tag_key: Any, default_arch: int) -> int | None: # noqa: ANN401
  123. arch_str = get_value(tag_key, "SysArchitecture")
  124. if arch_str is not None:
  125. key_path = f"{hive_name}/{company}/{tag}/SysArchitecture"
  126. try:
  127. return parse_arch(arch_str)
  128. except ValueError as sys_arch:
  129. msg(key_path, sys_arch)
  130. return default_arch
  131. def parse_arch(arch_str: Any) -> int: # noqa: ANN401
  132. if isinstance(arch_str, str):
  133. if match := _ARCH_RE.match(arch_str):
  134. return int(next(iter(match.groups())))
  135. error = f"invalid format {arch_str}"
  136. else:
  137. error = f"arch is not string: {arch_str!r}"
  138. raise ValueError(error)
  139. def load_version_data(
  140. hive_name: str,
  141. company: str,
  142. tag: str,
  143. tag_key: Any, # noqa: ANN401
  144. ) -> tuple[int | None, int | None, int | None] | None:
  145. for candidate, key_path in [
  146. (get_value(tag_key, "SysVersion"), f"{hive_name}/{company}/{tag}/SysVersion"),
  147. (tag, f"{hive_name}/{company}/{tag}"),
  148. ]:
  149. if candidate is not None:
  150. try:
  151. return parse_version(candidate)
  152. except ValueError as sys_version:
  153. msg(key_path, sys_version)
  154. return None
  155. def parse_version(version_str: Any) -> tuple[int | None, int | None, int | None]: # noqa: ANN401
  156. if isinstance(version_str, str):
  157. if match := _VERSION_RE.match(version_str):
  158. g1, g2, g3 = match.groups()
  159. return (
  160. int(g1) if g1 is not None else None,
  161. int(g2) if g2 is not None else None,
  162. int(g3) if g3 is not None else None,
  163. )
  164. error = f"invalid format {version_str}"
  165. else:
  166. error = f"version is not string: {version_str!r}"
  167. raise ValueError(error)
  168. def load_threaded(hive_name: str, company: str, tag: str, tag_key: Any) -> bool: # noqa: ANN401
  169. display_name = get_value(tag_key, "DisplayName")
  170. if display_name is not None:
  171. if isinstance(display_name, str):
  172. if "freethreaded" in display_name.lower():
  173. return True
  174. else:
  175. key_path = f"{hive_name}/{company}/{tag}/DisplayName"
  176. msg(key_path, f"display name is not string: {display_name!r}")
  177. return bool(_THREADED_TAG_RE.match(tag))
  178. def msg(path: str, what: object) -> None:
  179. _LOGGER.warning("PEP-514 violation in Windows Registry at %s error: %s", path, what)
  180. def _run() -> None:
  181. basicConfig()
  182. interpreters = [repr(spec) for spec in discover_pythons()]
  183. sys.stdout.write("\n".join(sorted(interpreters)))
  184. sys.stdout.write("\n")
  185. if __name__ == "__main__":
  186. _run()