pep514.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. """Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only."""
  2. from __future__ import annotations
  3. import os
  4. import re
  5. import winreg
  6. from logging import basicConfig, getLogger
  7. LOGGER = getLogger(__name__)
  8. def enum_keys(key):
  9. at = 0
  10. while True:
  11. try:
  12. yield winreg.EnumKey(key, at)
  13. except OSError:
  14. break
  15. at += 1
  16. def get_value(key, value_name):
  17. try:
  18. return winreg.QueryValueEx(key, value_name)[0]
  19. except OSError:
  20. return None
  21. def discover_pythons():
  22. for hive, hive_name, key, flags, default_arch in [
  23. (winreg.HKEY_CURRENT_USER, "HKEY_CURRENT_USER", r"Software\Python", 0, 64),
  24. (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_64KEY, 64),
  25. (winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_32KEY, 32),
  26. ]:
  27. yield from process_set(hive, hive_name, key, flags, default_arch)
  28. def process_set(hive, hive_name, key, flags, default_arch):
  29. try:
  30. with winreg.OpenKeyEx(hive, key, 0, winreg.KEY_READ | flags) as root_key:
  31. for company in enum_keys(root_key):
  32. if company == "PyLauncher": # reserved
  33. continue
  34. yield from process_company(hive_name, company, root_key, default_arch)
  35. except OSError:
  36. pass
  37. def process_company(hive_name, company, root_key, default_arch):
  38. with winreg.OpenKeyEx(root_key, company) as company_key:
  39. for tag in enum_keys(company_key):
  40. spec = process_tag(hive_name, company, company_key, tag, default_arch)
  41. if spec is not None:
  42. yield spec
  43. def process_tag(hive_name, company, company_key, tag, default_arch):
  44. with winreg.OpenKeyEx(company_key, tag) as tag_key:
  45. version = load_version_data(hive_name, company, tag, tag_key)
  46. if version is not None: # if failed to get version bail
  47. major, minor, _ = version
  48. arch = load_arch_data(hive_name, company, tag, tag_key, default_arch)
  49. if arch is not None:
  50. exe_data = load_exe(hive_name, company, company_key, tag)
  51. if exe_data is not None:
  52. exe, args = exe_data
  53. threaded = load_threaded(hive_name, company, tag, tag_key)
  54. return company, major, minor, arch, threaded, exe, args
  55. return None
  56. return None
  57. return None
  58. def load_exe(hive_name, company, company_key, tag):
  59. key_path = f"{hive_name}/{company}/{tag}"
  60. try:
  61. with winreg.OpenKeyEx(company_key, rf"{tag}\InstallPath") as ip_key, ip_key:
  62. exe = get_value(ip_key, "ExecutablePath")
  63. if exe is None:
  64. ip = get_value(ip_key, None)
  65. if ip is None:
  66. msg(key_path, "no ExecutablePath or default for it")
  67. else:
  68. exe = os.path.join(ip, "python.exe")
  69. if exe is not None and os.path.exists(exe):
  70. args = get_value(ip_key, "ExecutableArguments")
  71. return exe, args
  72. msg(key_path, f"could not load exe with value {exe}")
  73. except OSError:
  74. msg(f"{key_path}/InstallPath", "missing")
  75. return None
  76. def load_arch_data(hive_name, company, tag, tag_key, default_arch):
  77. arch_str = get_value(tag_key, "SysArchitecture")
  78. if arch_str is not None:
  79. key_path = f"{hive_name}/{company}/{tag}/SysArchitecture"
  80. try:
  81. return parse_arch(arch_str)
  82. except ValueError as sys_arch:
  83. msg(key_path, sys_arch)
  84. return default_arch
  85. def parse_arch(arch_str):
  86. if isinstance(arch_str, str):
  87. match = re.match(r"^(\d+)bit$", arch_str)
  88. if match:
  89. return int(next(iter(match.groups())))
  90. error = f"invalid format {arch_str}"
  91. else:
  92. error = f"arch is not string: {arch_str!r}"
  93. raise ValueError(error)
  94. def load_version_data(hive_name, company, tag, tag_key):
  95. for candidate, key_path in [
  96. (get_value(tag_key, "SysVersion"), f"{hive_name}/{company}/{tag}/SysVersion"),
  97. (tag, f"{hive_name}/{company}/{tag}"),
  98. ]:
  99. if candidate is not None:
  100. try:
  101. return parse_version(candidate)
  102. except ValueError as sys_version:
  103. msg(key_path, sys_version)
  104. return None
  105. def parse_version(version_str):
  106. if isinstance(version_str, str):
  107. match = re.match(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?$", version_str)
  108. if match:
  109. return tuple(int(i) if i is not None else None for i in match.groups())
  110. error = f"invalid format {version_str}"
  111. else:
  112. error = f"version is not string: {version_str!r}"
  113. raise ValueError(error)
  114. def load_threaded(hive_name, company, tag, tag_key):
  115. display_name = get_value(tag_key, "DisplayName")
  116. if display_name is not None:
  117. if isinstance(display_name, str):
  118. if "freethreaded" in display_name.lower():
  119. return True
  120. else:
  121. key_path = f"{hive_name}/{company}/{tag}/DisplayName"
  122. msg(key_path, f"display name is not string: {display_name!r}")
  123. return bool(re.match(r"^\d+(\.\d+){0,2}t$", tag, flags=re.IGNORECASE))
  124. def msg(path, what):
  125. LOGGER.warning("PEP-514 violation in Windows Registry at %s error: %s", path, what)
  126. def _run():
  127. basicConfig()
  128. interpreters = [repr(spec) for spec in discover_pythons()]
  129. print("\n".join(sorted(interpreters))) # noqa: T201
  130. if __name__ == "__main__":
  131. _run()