Skip to content
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

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9583587
prototyping pythonfinder pep514 support
matteius Apr 24, 2024
9f2b259
winreg is only available on, you guessed it -- windows
matteius Apr 25, 2024
00cc9f4
Merge branch 'main' into pythonfinder-pep514
matteius Apr 27, 2024
ec17820
Merge branch 'main' into pythonfinder-pep514
matteius Apr 27, 2024
9a84b15
Merge branch 'main' into pythonfinder-pep514
matteius Apr 27, 2024
6e13272
Update pipenv/vendor/pythonfinder/models/python.py
matteius Apr 29, 2024
af74dea
Update pipenv/vendor/pythonfinder/models/python.py
matteius May 2, 2024
3b3fb18
Update pipenv/vendor/pythonfinder/models/python.py
matteius May 2, 2024
914c925
Update pipenv/vendor/pythonfinder/models/python.py
matteius May 2, 2024
3123708
Update pipenv/vendor/pythonfinder/models/python.py
matteius May 2, 2024
987eb1b
Consider the three base registry keys.
matteius May 23, 2024
f09f46d
Refactor to follow the PEP 514 specification more closely, handling t…
matteius May 23, 2024
cee98c2
Merge branch 'main' into pythonfinder-pep514
matteius May 29, 2024
1ad4809
Update pipenv/vendor/pythonfinder/models/python.py
matteius May 29, 2024
7739889
Merge branch 'pythonfinder-pep514' of github.com:pypa/pipenv into pyt…
matteius May 29, 2024
1735d1a
Apply PR freedback to continue loop; add WindowsLauncherEntry to inst…
matteius May 29, 2024
d634f1f
Try actually wiring up the windows finder -- its not being invoked.
matteius May 29, 2024
a26d677
check pt progress (working on power shell, but pyenv thing needs work…
matteius May 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions pipenv/vendor/pythonfinder/models/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -201,13 +201,22 @@ 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:
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)
Expand Down Expand Up @@ -252,6 +261,25 @@ 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(
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)
path_entry = PathEntry.create(
path=launcher_entry.install_path,
is_root=True,
only_python=True,
)
windows_finder._versions[version.version_tuple] = path_entry
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
Expand Down
168 changes: 141 additions & 27 deletions pipenv/vendor/pythonfinder/models/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@
logger = logging.getLogger(__name__)


@dataclasses.dataclass
class WindowsLauncherEntry:
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
class PythonFinder(PathEntry):
root: Path = field(default_factory=Path)
Expand Down Expand Up @@ -103,7 +117,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():
Expand Down Expand Up @@ -147,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():
Expand Down Expand Up @@ -275,6 +294,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)

Expand Down Expand Up @@ -302,6 +322,111 @@ 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
import platform

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:
root_key = winreg.OpenKey(hive, key_path)
except FileNotFoundError:
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

try:
value, value_type = winreg.QueryValueEx(key, value_name)
if value_type != winreg.REG_SZ:
return default
return value
except FileNotFoundError:
return default


@dataclasses.dataclass
class PythonVersion:
Expand Down Expand Up @@ -577,39 +702,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
Expand Down
2 changes: 1 addition & 1 deletion pipenv/vendor/pythonfinder/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading