From 9583587f66ac857060ac25a9bbeff97450f392df Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 24 Apr 2024 19:59:35 -0400 Subject: [PATCH 01/13] prototyping pythonfinder pep514 support --- pipenv/vendor/pythonfinder/models/python.py | 89 +++++++++++++++------ 1 file changed, 63 insertions(+), 26 deletions(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index bd7a589751..c077b94b05 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -5,6 +5,7 @@ import os import platform import sys +import winreg from collections import defaultdict from dataclasses import field from functools import cached_property @@ -39,6 +40,14 @@ logger = logging.getLogger(__name__) +class WindowsLauncherEntry: + def __init__(self, version: Version, install_path: str, executable_path: str, company: Optional[str]): + self.version = version + self.install_path = install_path + self.executable_path = executable_path + self.company = company + + @dataclasses.dataclass class PythonFinder(PathEntry): root: Path = field(default_factory=Path) @@ -103,7 +112,13 @@ def version_from_bin_dir(cls, entry) -> PathEntry | None: py_version = next(iter(entry.find_all_python_versions()), None) return py_version - def _iter_version_bases(self) -> Iterator[tuple[Path, PathEntry]]: + def _iter_version_bases(self): + # Yield versions from the Windows launcher + if os.name == "nt": + for launcher_entry in self.find_python_versions_from_windows_launcher(): + yield (launcher_entry.install_path, launcher_entry) + + # Yield versions from the existing logic for p in self.get_version_order(): bin_dir = self.get_bin_dir(p) if bin_dir.exists() and bin_dir.is_dir(): @@ -275,6 +290,7 @@ def find_python_version( :returns: A :class:`~pythonfinder.models.PathEntry` instance matching the version requested. """ + def sub_finder(obj): return obj.find_python_version(major, minor, patch, pre, dev, arch, name) @@ -302,6 +318,38 @@ def which(self, name) -> PathEntry | None: non_empty_match = next(iter(m for m in matches if m is not None), None) return non_empty_match + def find_python_versions_from_windows_launcher(self): + # Open the registry key for Python launcher + key_path = r"Software\Python\PythonCore" + try: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key: + num_subkeys, _, _ = winreg.QueryInfoKey(key) + + for i in range(num_subkeys): + version_key_name = winreg.EnumKey(key, i) + version_key_path = f"{key_path}\\{version_key_name}" + + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, version_key_path) as version_key: + try: + install_path = winreg.QueryValue(version_key, "InstallPath") + executable_path = winreg.QueryValue(version_key, "ExecutablePath") + company = winreg.QueryValue(version_key, "Company") + except FileNotFoundError: + continue + + version = Version(version_key_name) + launcher_entry = WindowsLauncherEntry( + version=version, + install_path=install_path, + executable_path=executable_path, + company=company, + ) + yield launcher_entry + + except FileNotFoundError: + # Python launcher registry key not found, no Python versions registered + return + @dataclasses.dataclass class PythonVersion: @@ -577,39 +625,28 @@ def parse_executable(cls, path) -> dict[str, str | int | Version | None]: return result_dict @classmethod - def from_windows_launcher( - cls, launcher_entry, name=None, company=None - ) -> PythonVersion: + def from_windows_launcher(cls, launcher_entry, name=None, company=None): """Create a new PythonVersion instance from a Windows Launcher Entry - :param launcher_entry: A python launcher environment object. + :param launcher_entry: A WindowsLauncherEntry object. :param Optional[str] name: The name of the distribution. :param Optional[str] company: The name of the distributing company. :return: An instance of a PythonVersion. """ - creation_dict = cls.parse(launcher_entry.info.version) - base_path = ensure_path(launcher_entry.info.install_path.__getattr__("")) - default_path = base_path / "python.exe" - if not default_path.exists(): - default_path = base_path / "Scripts" / "python.exe" - exe_path = ensure_path( - getattr(launcher_entry.info.install_path, "executable_path", default_path) - ) - company = getattr(launcher_entry, "company", guess_company(exe_path)) - creation_dict.update( - { - "architecture": getattr( - launcher_entry.info, "sys_architecture", SYSTEM_ARCH - ), - "executable": exe_path, - "name": name, - "company": company, - } + py_version = cls.create( + major=launcher_entry.version.major, + minor=launcher_entry.version.minor, + patch=launcher_entry.version.micro, + is_prerelease=launcher_entry.version.is_prerelease, + is_devrelease=launcher_entry.version.is_devrelease, + is_debug=False, # Assuming debug information is not available from the registry + architecture=None, # Assuming architecture information is not available from the registry + executable=launcher_entry.executable_path, + company=launcher_entry.company, ) - py_version = cls.create(**creation_dict) - comes_from = PathEntry.create(exe_path, only_python=True, name=name) + comes_from = PathEntry.create(launcher_entry.executable_path, only_python=True, name=name) py_version.comes_from = comes_from - py_version.name = comes_from.name + py_version.name = name return py_version @classmethod From 9f2b259d87cec0580239754e691116b15a56dec2 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 24 Apr 2024 20:06:26 -0400 Subject: [PATCH 02/13] winreg is only available on, you guessed it -- windows --- pipenv/vendor/pythonfinder/models/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index c077b94b05..2aa96de30e 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -5,7 +5,6 @@ import os import platform import sys -import winreg from collections import defaultdict from dataclasses import field from functools import cached_property @@ -322,6 +321,7 @@ def find_python_versions_from_windows_launcher(self): # Open the registry key for Python launcher key_path = r"Software\Python\PythonCore" try: + import winreg with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key: num_subkeys, _, _ = winreg.QueryInfoKey(key) From 6e13272f52d79f125664bd9045ee0f91723c4f03 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 29 Apr 2024 19:21:56 -0400 Subject: [PATCH 03/13] Update pipenv/vendor/pythonfinder/models/python.py Accept PR suggestion Co-authored-by: Paul "TBBle" Hampson --- pipenv/vendor/pythonfinder/models/python.py | 28 +++++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index e546b073af..c0026f2a20 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -327,22 +327,34 @@ def find_python_versions_from_windows_launcher(self): for i in range(num_subkeys): version_key_name = winreg.EnumKey(key, i) - version_key_path = f"{key_path}\\{version_key_name}" - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, version_key_path) as version_key: + with winreg.OpenKey(key, version_key_name) as version_key: try: - install_path = winreg.QueryValue(version_key, "InstallPath") - executable_path = winreg.QueryValue(version_key, "ExecutablePath") - company = winreg.QueryValue(version_key, "Company") + install_path_key = winreg.OpenKey(version_key, "InstallPath") + try: + executable_path = winreg.QueryValue(install_path_key, "ExecutablePath") + except FileNotFoundError: + # TODO: Only a valid default for PythonCore, otherwise skip. + executable_path = winreg.QueryValue(install_path_key, None)+"\\python.exe" except FileNotFoundError: continue - version = Version(version_key_name) + try: + version = Version(winreg.QueryValue(key, "SysVersion")) + except FileNotFoundError: + # TODO: Only a valid default for PythonCore, otherwise unknown so will need to be probed later. + version = Version(version_key_name) + + try: + architecture = winreg.QueryValue(key, "SysArchitecture") + except FileNotFoundError: + # TODO: Implement PEP-514 defaults for architecture for PythonCore based on key and OS architecture. + architecture = None + launcher_entry = WindowsLauncherEntry( version=version, - install_path=install_path, executable_path=executable_path, - company=company, + company="PythonCore", ) yield launcher_entry From af74deac9bebaee6899cf3b032b3e973ec5ec12a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 2 May 2024 05:37:36 -0400 Subject: [PATCH 04/13] Update pipenv/vendor/pythonfinder/models/python.py Co-authored-by: Paul "TBBle" Hampson --- pipenv/vendor/pythonfinder/models/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index c0026f2a20..ae56c11939 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -45,7 +45,7 @@ def __init__(self, version: Version, install_path: str, executable_path: str, co self.install_path = install_path self.executable_path = executable_path self.company = company - + self.architecture = architecture @dataclasses.dataclass class PythonFinder(PathEntry): From 3b3fb18de4cd07c49833c08ae1bba21a69acc34d Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 2 May 2024 05:37:47 -0400 Subject: [PATCH 05/13] Update pipenv/vendor/pythonfinder/models/python.py Co-authored-by: Paul "TBBle" Hampson --- pipenv/vendor/pythonfinder/models/python.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index ae56c11939..1c62b47139 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -355,6 +355,7 @@ def find_python_versions_from_windows_launcher(self): version=version, executable_path=executable_path, company="PythonCore", + architecture=architecture, ) yield launcher_entry From 914c925a4b73277a8cefbe913355d5a58bac7df4 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 2 May 2024 05:38:03 -0400 Subject: [PATCH 06/13] Update pipenv/vendor/pythonfinder/models/python.py Co-authored-by: Paul "TBBle" Hampson --- pipenv/vendor/pythonfinder/models/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 1c62b47139..d3e5be4597 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -653,7 +653,7 @@ def from_windows_launcher(cls, launcher_entry, name=None, company=None): is_prerelease=launcher_entry.version.is_prerelease, is_devrelease=launcher_entry.version.is_devrelease, is_debug=False, # Assuming debug information is not available from the registry - architecture=None, # Assuming architecture information is not available from the registry + architecture=launcher_entry.architecture, executable=launcher_entry.executable_path, company=launcher_entry.company, ) From 31237083a5761b4e048525fabe4b2c4686410b56 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 2 May 2024 05:38:10 -0400 Subject: [PATCH 07/13] Update pipenv/vendor/pythonfinder/models/python.py Co-authored-by: Paul "TBBle" Hampson --- pipenv/vendor/pythonfinder/models/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index d3e5be4597..b227affc04 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -40,7 +40,7 @@ class WindowsLauncherEntry: - def __init__(self, version: Version, install_path: str, executable_path: str, company: Optional[str]): + def __init__(self, version: Version, install_path: str, executable_path: str, company: str, architecture: Optional[str]): self.version = version self.install_path = install_path self.executable_path = executable_path From 987eb1bf4f54083867a90edcc53e32deb0728b02 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 23 May 2024 06:58:33 -0400 Subject: [PATCH 08/13] Consider the three base registry keys. --- pipenv/vendor/pythonfinder/models/python.py | 84 +++++++++++---------- 1 file changed, 45 insertions(+), 39 deletions(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index b227affc04..6ef633d7c5 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -319,49 +319,55 @@ def which(self, name) -> PathEntry | None: def find_python_versions_from_windows_launcher(self): # Open the registry key for Python launcher - key_path = r"Software\Python\PythonCore" - try: - import winreg - with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) as key: - num_subkeys, _, _ = winreg.QueryInfoKey(key) + import winreg + registry_keys = [ + (winreg.HKEY_CURRENT_USER, r"Software\Python"), + (winreg.HKEY_LOCAL_MACHINE, r"Software\Python\PythonCore"), + (winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\Python\PythonCore") + ] + for key, key_path in registry_keys: + try: + with winreg.OpenKey(key, key_path) as key: + num_subkeys, _, _ = winreg.QueryInfoKey(key) - for i in range(num_subkeys): - version_key_name = winreg.EnumKey(key, i) + for i in range(num_subkeys): + version_key_name = winreg.EnumKey(key, i) + + with winreg.OpenKey(key, version_key_name) as version_key: + try: + install_path_key = winreg.OpenKey(version_key, "InstallPath") + try: + executable_path = winreg.QueryValue(install_path_key, "ExecutablePath") + except FileNotFoundError: + # TODO: Only a valid default for PythonCore, otherwise skip. + executable_path = winreg.QueryValue(install_path_key, None)+"\\python.exe" + except FileNotFoundError: + continue + + try: + version = Version(winreg.QueryValue(key, "SysVersion")) + except FileNotFoundError: + # TODO: Only a valid default for PythonCore, otherwise unknown so will need to be probed later. + version = Version(version_key_name) - with winreg.OpenKey(key, version_key_name) as version_key: - try: - install_path_key = winreg.OpenKey(version_key, "InstallPath") try: - executable_path = winreg.QueryValue(install_path_key, "ExecutablePath") + architecture = winreg.QueryValue(key, "SysArchitecture") except FileNotFoundError: - # TODO: Only a valid default for PythonCore, otherwise skip. - executable_path = winreg.QueryValue(install_path_key, None)+"\\python.exe" - except FileNotFoundError: - continue - - try: - version = Version(winreg.QueryValue(key, "SysVersion")) - except FileNotFoundError: - # TODO: Only a valid default for PythonCore, otherwise unknown so will need to be probed later. - version = Version(version_key_name) - - try: - architecture = winreg.QueryValue(key, "SysArchitecture") - except FileNotFoundError: - # TODO: Implement PEP-514 defaults for architecture for PythonCore based on key and OS architecture. - architecture = None - - launcher_entry = WindowsLauncherEntry( - version=version, - executable_path=executable_path, - company="PythonCore", - architecture=architecture, - ) - yield launcher_entry - - except FileNotFoundError: - # Python launcher registry key not found, no Python versions registered - return + # TODO: Implement PEP-514 defaults for architecture for PythonCore based on key and OS architecture. + architecture = None + + launcher_entry = WindowsLauncherEntry( + version=version, + executable_path=executable_path, + company="PythonCore", + architecture=architecture, + ) + yield launcher_entry + + except FileNotFoundError: + # Python launcher registry key not found, no Python versions registered + pass + return None @dataclasses.dataclass From f09f46d505fdf75c0fcb7fcf385eb4fa34028495 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 23 May 2024 07:04:07 -0400 Subject: [PATCH 09/13] Refactor to follow the PEP 514 specification more closely, handling the various registry values and fallback rules for PythonCore entries. --- pipenv/vendor/pythonfinder/models/python.py | 108 ++++++++++++-------- 1 file changed, 66 insertions(+), 42 deletions(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 6ef633d7c5..6b94613fca 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -318,56 +318,80 @@ def which(self, name) -> PathEntry | None: return non_empty_match def find_python_versions_from_windows_launcher(self): - # Open the registry key for Python launcher import winreg + registry_keys = [ (winreg.HKEY_CURRENT_USER, r"Software\Python"), - (winreg.HKEY_LOCAL_MACHINE, r"Software\Python\PythonCore"), - (winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\Python\PythonCore") + (winreg.HKEY_LOCAL_MACHINE, r"Software\Python"), + (winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\Python") ] - for key, key_path in registry_keys: + + for hive, key_path in registry_keys: try: - with winreg.OpenKey(key, key_path) as key: - num_subkeys, _, _ = winreg.QueryInfoKey(key) - - for i in range(num_subkeys): - version_key_name = winreg.EnumKey(key, i) - - with winreg.OpenKey(key, version_key_name) as version_key: - try: - install_path_key = winreg.OpenKey(version_key, "InstallPath") - try: - executable_path = winreg.QueryValue(install_path_key, "ExecutablePath") - except FileNotFoundError: - # TODO: Only a valid default for PythonCore, otherwise skip. - executable_path = winreg.QueryValue(install_path_key, None)+"\\python.exe" - except FileNotFoundError: - continue - - try: - version = Version(winreg.QueryValue(key, "SysVersion")) - except FileNotFoundError: - # TODO: Only a valid default for PythonCore, otherwise unknown so will need to be probed later. - version = Version(version_key_name) - - try: - architecture = winreg.QueryValue(key, "SysArchitecture") - except FileNotFoundError: - # TODO: Implement PEP-514 defaults for architecture for PythonCore based on key and OS architecture. - architecture = None - - launcher_entry = WindowsLauncherEntry( - version=version, - executable_path=executable_path, - company="PythonCore", - architecture=architecture, - ) - yield launcher_entry + with winreg.OpenKey(hive, key_path) as root_key: + num_companies, _, _ = winreg.QueryInfoKey(root_key) + + for i in range(num_companies): + company = winreg.EnumKey(root_key, i) + if company == "PyLauncher": + continue + + with winreg.OpenKey(root_key, company) as company_key: + num_tags, _, _ = winreg.QueryInfoKey(company_key) + + for j in range(num_tags): + tag = winreg.EnumKey(company_key, j) + + with winreg.OpenKey(company_key, tag) as tag_key: + display_name = self._get_registry_value(tag_key, "DisplayName", + default=f"Python {tag}") + support_url = self._get_registry_value(tag_key, "SupportUrl", + default="http://www.python.org/") + version = self._get_registry_value(tag_key, "Version", default=tag[:3]) + sys_version = self._get_registry_value(tag_key, "SysVersion", default=tag[:3]) + sys_architecture = self._get_registry_value(tag_key, "SysArchitecture") + + if company == "PythonCore" and not sys_architecture: + # TODO: Implement PEP-514 defaults for architecture for PythonCore based on key and OS architecture. + sys_architecture = None + + try: + with winreg.OpenKey(tag_key, "InstallPath") as install_path_key: + install_path = self._get_registry_value(install_path_key, None) + executable_path = self._get_registry_value(install_path_key, + "ExecutablePath") + windowed_executable_path = self._get_registry_value(install_path_key, + "WindowedExecutablePath") + + if company == "PythonCore" and not executable_path: + executable_path = os.path.join(install_path, "python.exe") + if company == "PythonCore" and not windowed_executable_path: + windowed_executable_path = os.path.join(install_path, "pythonw.exe") + + launcher_entry = WindowsLauncherEntry( + version=Version(sys_version), + executable_path=executable_path, + windowed_executable_path=windowed_executable_path, + company=company, + tag=tag, + display_name=display_name, + support_url=support_url, + architecture=sys_architecture, + ) + yield launcher_entry + + except FileNotFoundError: + continue except FileNotFoundError: - # Python launcher registry key not found, no Python versions registered pass - return None + + def _get_registry_value(self, key, value_name, default=None): + try: + import winreg + return winreg.QueryValue(key, value_name) + except FileNotFoundError: + return default @dataclasses.dataclass From 1ad4809677f9fa14b11f5468e5f74188797c0052 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 28 May 2024 22:14:38 -0400 Subject: [PATCH 10/13] Update pipenv/vendor/pythonfinder/models/python.py Co-authored-by: Paul "TBBle" Hampson --- pipenv/vendor/pythonfinder/models/python.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 6b94613fca..bd600ee27f 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -389,7 +389,10 @@ def find_python_versions_from_windows_launcher(self): def _get_registry_value(self, key, value_name, default=None): try: import winreg - return winreg.QueryValue(key, value_name) + # TODO: Not specified in PEP-514, but probably should reject any value that isn't `winreg.REG_SZ`. + # PEP-514's (faulty) example code uses `winreg.QueryValue`, which can only return `winreg.REG_SZ` values. + # They document "string value" which could match `winreg.REG_SZ` or `winreg.REG_EXPAND_SZ`. + return winreg.QueryValueEx(key, value_name)[0] except FileNotFoundError: return default From 1735d1a080409d3d53a688e3ad3815130e0bb121 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 28 May 2024 22:25:44 -0400 Subject: [PATCH 11/13] Apply PR freedback to continue loop; add WindowsLauncherEntry to install_path; etc and refactor methods to be more readable. --- pipenv/vendor/pythonfinder/models/python.py | 150 ++++++++++++-------- 1 file changed, 88 insertions(+), 62 deletions(-) diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index bd600ee27f..20bfe2c08f 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -319,6 +319,7 @@ def which(self, name) -> PathEntry | None: def find_python_versions_from_windows_launcher(self): import winreg + import platform registry_keys = [ (winreg.HKEY_CURRENT_USER, r"Software\Python"), @@ -328,71 +329,96 @@ def find_python_versions_from_windows_launcher(self): for hive, key_path in registry_keys: try: - with winreg.OpenKey(hive, key_path) as root_key: - num_companies, _, _ = winreg.QueryInfoKey(root_key) - - for i in range(num_companies): - company = winreg.EnumKey(root_key, i) - if company == "PyLauncher": - continue - - with winreg.OpenKey(root_key, company) as company_key: - num_tags, _, _ = winreg.QueryInfoKey(company_key) - - for j in range(num_tags): - tag = winreg.EnumKey(company_key, j) - - with winreg.OpenKey(company_key, tag) as tag_key: - display_name = self._get_registry_value(tag_key, "DisplayName", - default=f"Python {tag}") - support_url = self._get_registry_value(tag_key, "SupportUrl", - default="http://www.python.org/") - version = self._get_registry_value(tag_key, "Version", default=tag[:3]) - sys_version = self._get_registry_value(tag_key, "SysVersion", default=tag[:3]) - sys_architecture = self._get_registry_value(tag_key, "SysArchitecture") - - if company == "PythonCore" and not sys_architecture: - # TODO: Implement PEP-514 defaults for architecture for PythonCore based on key and OS architecture. - sys_architecture = None - - try: - with winreg.OpenKey(tag_key, "InstallPath") as install_path_key: - install_path = self._get_registry_value(install_path_key, None) - executable_path = self._get_registry_value(install_path_key, - "ExecutablePath") - windowed_executable_path = self._get_registry_value(install_path_key, - "WindowedExecutablePath") - - if company == "PythonCore" and not executable_path: - executable_path = os.path.join(install_path, "python.exe") - if company == "PythonCore" and not windowed_executable_path: - windowed_executable_path = os.path.join(install_path, "pythonw.exe") - - launcher_entry = WindowsLauncherEntry( - version=Version(sys_version), - executable_path=executable_path, - windowed_executable_path=windowed_executable_path, - company=company, - tag=tag, - display_name=display_name, - support_url=support_url, - architecture=sys_architecture, - ) - yield launcher_entry - - except FileNotFoundError: - continue - + root_key = winreg.OpenKey(hive, key_path) except FileNotFoundError: - pass + continue + + num_companies, _, _ = winreg.QueryInfoKey(root_key) + + for i in range(num_companies): + company = winreg.EnumKey(root_key, i) + if company == "PyLauncher": + continue + + company_key = winreg.OpenKey(root_key, company) + num_tags, _, _ = winreg.QueryInfoKey(company_key) + + for j in range(num_tags): + tag = winreg.EnumKey(company_key, j) + tag_key = winreg.OpenKey(company_key, tag) + + display_name = self._get_win_registry_value(tag_key, "DisplayName", default=f"Python {tag}") + support_url = self._get_win_registry_value(tag_key, "SupportUrl", default="http://www.python.org/") + sys_version = self._get_win_registry_value(tag_key, "SysVersion") + sys_architecture = self._get_win_registry_value(tag_key, "SysArchitecture") + + if company == "PythonCore" and not sys_architecture: + sys_architecture = self._get_python_win_core_architecture(key_path, hive, platform) + + launcher_entry = self._create_win_launcher_entry(tag_key, company, tag, display_name, support_url, + sys_version, sys_architecture) + if launcher_entry: + yield launcher_entry + + tag_key.Close() + + company_key.Close() + + root_key.Close() + + def _get_python_win_core_architecture(self, key_path, hive, platform): + import winreg + if key_path == r"Software\Wow6432Node\Python" or not platform.machine().endswith('64'): + return "32bit" + elif hive == winreg.HKEY_LOCAL_MACHINE: + return "64bit" + else: + return None + + def _create_win_launcher_entry(self, tag_key, company, tag, display_name, support_url, sys_version, sys_architecture): + import winreg + try: + install_path_key = winreg.OpenKey(tag_key, "InstallPath") + except FileNotFoundError: + return None + + install_path = self._get_win_registry_value(install_path_key, None) + executable_path = self._get_win_registry_value(install_path_key, "ExecutablePath") + windowed_executable_path = self._get_win_registry_value(install_path_key, "WindowedExecutablePath") + + if company == "PythonCore": + if not executable_path and install_path: + executable_path = os.path.join(install_path, "python.exe") + if not windowed_executable_path and install_path: + windowed_executable_path = os.path.join(install_path, "pythonw.exe") + + if not install_path or not executable_path: + install_path_key.Close() + return None + + launcher_entry = WindowsLauncherEntry( + version=Version(sys_version), + executable_path=executable_path, + windowed_executable_path=windowed_executable_path, + company=company, + tag=tag, + display_name=display_name, + support_url=support_url, + architecture=sys_architecture, + install_path=install_path, + ) + + install_path_key.Close() + return launcher_entry + + def _get_win_registry_value(self, key, value_name, default=None): + import winreg - def _get_registry_value(self, key, value_name, default=None): try: - import winreg - # TODO: Not specified in PEP-514, but probably should reject any value that isn't `winreg.REG_SZ`. - # PEP-514's (faulty) example code uses `winreg.QueryValue`, which can only return `winreg.REG_SZ` values. - # They document "string value" which could match `winreg.REG_SZ` or `winreg.REG_EXPAND_SZ`. - return winreg.QueryValueEx(key, value_name)[0] + value, value_type = winreg.QueryValueEx(key, value_name) + if value_type != winreg.REG_SZ: + return default + return value except FileNotFoundError: return default From d634f1f3766b0113c546a20c19f14d800475b4f8 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 28 May 2024 23:03:21 -0400 Subject: [PATCH 12/13] Try actually wiring up the windows finder -- its not being invoked. --- pipenv/vendor/pythonfinder/models/path.py | 25 ++++++++++++++++++++- pipenv/vendor/pythonfinder/models/python.py | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/pipenv/vendor/pythonfinder/models/path.py b/pipenv/vendor/pythonfinder/models/path.py index 572dec7e3d..2962a5e6be 100644 --- a/pipenv/vendor/pythonfinder/models/path.py +++ b/pipenv/vendor/pythonfinder/models/path.py @@ -32,7 +32,7 @@ resolve_path, ) from .mixins import PathEntry -from .python import PythonFinder +from .python import PythonFinder, PythonVersion def exists_and_is_accessible(path): @@ -201,6 +201,15 @@ def _run_setup(self) -> SystemPath: # Handle virtual environment and system paths self._handle_virtualenv_and_system_paths() + # Setup Windows launcher finder + self._setup_windows_launcher() + + # Setup ASDF finder + self._setup_asdf() + + # Setup pyenv finder + self._setup_pyenv() + return self def _get_last_instance(self, path) -> int: @@ -252,6 +261,20 @@ def _remove_path(self, path) -> SystemPath: self.path_order = new_order return self + def _setup_windows_launcher(self) -> SystemPath: + if os.name == "nt": + windows_finder = PythonFinder.create() + for launcher_entry in windows_finder.find_python_versions_from_windows_launcher(): + version = PythonVersion.from_windows_launcher(launcher_entry) + windows_finder.versions[version.version_tuple] = PathEntry.create( + path=launcher_entry.install_path, + is_root=True, + only_python=True, + pythons={launcher_entry.install_path: version}, + ) + self._register_finder("windows", windows_finder) + return self + def _setup_asdf(self) -> SystemPath: if "asdf" in self.finders and self.asdf_finder is not None: return self diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 20bfe2c08f..60b4417c73 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -47,6 +47,7 @@ def __init__(self, version: Version, install_path: str, executable_path: str, co self.company = company self.architecture = architecture + @dataclasses.dataclass class PythonFinder(PathEntry): root: Path = field(default_factory=Path) From a26d67784ac014c009c18b154e008a56e60922c8 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 28 May 2024 23:19:59 -0400 Subject: [PATCH 13/13] check pt progress (working on power shell, but pyenv thing needs work for git bash). --- pipenv/vendor/pythonfinder/models/path.py | 13 +++++++++---- pipenv/vendor/pythonfinder/models/python.py | 18 +++++++++++------- pipenv/vendor/pythonfinder/utils.py | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/pipenv/vendor/pythonfinder/models/path.py b/pipenv/vendor/pythonfinder/models/path.py index 2962a5e6be..6a15dcdfd3 100644 --- a/pipenv/vendor/pythonfinder/models/path.py +++ b/pipenv/vendor/pythonfinder/models/path.py @@ -216,7 +216,7 @@ def _get_last_instance(self, path) -> int: reversed_paths = reversed(self.path_order) paths = [resolve_path(p) for p in reversed_paths] normalized_target = resolve_path(path) - last_instance = next(iter(p for p in paths if normalized_target in p), None) + last_instance = next(iter(p for p in paths if normalized_target == p), None) if last_instance is None: raise ValueError(f"No instance found on path for target: {path!s}") path_index = self.path_order.index(last_instance) @@ -263,15 +263,20 @@ def _remove_path(self, path) -> SystemPath: def _setup_windows_launcher(self) -> SystemPath: if os.name == "nt": - windows_finder = PythonFinder.create() + windows_finder = PythonFinder.create( + root=Path("."), # Use appropriate root directory for Windows launcher + sort_function=None, # Provide a sorting function if needed + version_glob_path="python*", # Adjust the glob pattern if necessary + ignore_unsupported=True, + ) for launcher_entry in windows_finder.find_python_versions_from_windows_launcher(): version = PythonVersion.from_windows_launcher(launcher_entry) - windows_finder.versions[version.version_tuple] = PathEntry.create( + path_entry = PathEntry.create( path=launcher_entry.install_path, is_root=True, only_python=True, - pythons={launcher_entry.install_path: version}, ) + windows_finder._versions[version.version_tuple] = path_entry self._register_finder("windows", windows_finder) return self diff --git a/pipenv/vendor/pythonfinder/models/python.py b/pipenv/vendor/pythonfinder/models/python.py index 60b4417c73..ac5656078d 100644 --- a/pipenv/vendor/pythonfinder/models/python.py +++ b/pipenv/vendor/pythonfinder/models/python.py @@ -39,13 +39,18 @@ logger = logging.getLogger(__name__) +@dataclasses.dataclass class WindowsLauncherEntry: - def __init__(self, version: Version, install_path: str, executable_path: str, company: str, architecture: Optional[str]): - self.version = version - self.install_path = install_path - self.executable_path = executable_path - self.company = company - self.architecture = architecture + version: Version + install_path: str + executable_path: str + windowed_executable_path: str + company: str + architecture: Optional[str] + display_name: Optional[str] + support_url: Optional[str] + tag: Optional[str] + @dataclasses.dataclass @@ -162,7 +167,6 @@ def _iter_versions(self) -> Iterator[tuple[Path, PathEntry, tuple]]: ) yield (base_path, entry, version_tuple) - @cached_property def versions(self) -> DefaultDict[tuple, PathEntry]: if not self._versions: for _, entry, version_tuple in self._iter_versions(): diff --git a/pipenv/vendor/pythonfinder/utils.py b/pipenv/vendor/pythonfinder/utils.py index 4c4e0bd586..f4845a7c6b 100644 --- a/pipenv/vendor/pythonfinder/utils.py +++ b/pipenv/vendor/pythonfinder/utils.py @@ -339,7 +339,7 @@ def split_version_and_name( def is_in_path(path, parent): - return resolve_path(str(path)).startswith(resolve_path(str(parent))) + return str(resolve_path(str(path))).startswith(str(resolve_path(str(parent)))) def expand_paths(path, only_python=True) -> Iterator: