-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
prototyping pythonfinder pep514 support #6140
base: main
Are you sure you want to change the base?
Changes from 12 commits
9583587
9f2b259
00cc9f4
ec17820
9a84b15
6e13272
af74dea
3b3fb18
914c925
3123708
987eb1b
f09f46d
cee98c2
1ad4809
7739889
1735d1a
d634f1f
a26d677
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,14 @@ | |
logger = logging.getLogger(__name__) | ||
|
||
|
||
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 | ||
|
||
@dataclasses.dataclass | ||
class PythonFinder(PathEntry): | ||
root: Path = field(default_factory=Path) | ||
|
@@ -103,7 +111,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 +289,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 +317,82 @@ 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): | ||
import winreg | ||
|
||
registry_keys = [ | ||
(winreg.HKEY_CURRENT_USER, r"Software\Python"), | ||
(winreg.HKEY_LOCAL_MACHINE, r"Software\Python"), | ||
(winreg.HKEY_LOCAL_MACHINE, r"Software\Wow6432Node\Python") | ||
] | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Back-compat logic should be something like: import platform
if key_path == r"Software\Wow6432Node\Python" or not platform.machine().endswith('64'):
# 32-bit registry tree or 32-bit OS: https://stackoverflow.com/a/12578715/166389
sys_architecture = "32bit"
elif hive == winreg.HKEY_LOCAL_MACHINE:
# 64-bit OS and system-wide install that's not in the 32-bit registry tree
sys_architecture = "64bit"
else:
# User install on a 64-bit machine: Unknown architecture, we'd need to run the executable to find out.
sys_architecture = None We could probably capture the default value right at the top of the outer loop, since it depends only on |
||
|
||
try: | ||
with winreg.OpenKey(tag_key, "InstallPath") as install_path_key: | ||
install_path = self._get_registry_value(install_path_key, None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably need to continue if I suppose the Maybe That said, if the expectation is that this code can spit out invalid |
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An empty |
||
windowed_executable_path=windowed_executable_path, | ||
company=company, | ||
tag=tag, | ||
display_name=display_name, | ||
support_url=support_url, | ||
architecture=sys_architecture, | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't capture |
||
yield launcher_entry | ||
|
||
except FileNotFoundError: | ||
continue | ||
|
||
except FileNotFoundError: | ||
pass | ||
|
||
def _get_registry_value(self, key, value_name, default=None): | ||
try: | ||
import winreg | ||
return winreg.QueryValue(key, value_name) | ||
matteius marked this conversation as resolved.
Show resolved
Hide resolved
|
||
except FileNotFoundError: | ||
return default | ||
|
||
|
||
@dataclasses.dataclass | ||
class PythonVersion: | ||
|
@@ -577,39 +668,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=launcher_entry.architecture, | ||
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 | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These defaults are only applicable for
company == "PythonCore"
. It might make sense to have these all default to None and have a single block which populates all missing values for PythonCore only in one hit. (Or perhaps, just add these to the block that already exists inside theInstallPath
block.)I'd also suggest annotating the Version and SysVersion defaults to be clear that they are correct per-spec. Casual inspection of the code would note that Python 3.10 and Python 3.11 will both produce default Version and SysVersion of "3.1", which is clearly wrong. (But knowing that the defaults are only needed for Python installs older than Python 3.5 means that this case should never happen, but you can't tell that from the code.)