215 lines
7.8 KiB
Python
215 lines
7.8 KiB
Python
|
from __future__ import annotations
|
||
|
|
||
|
import dataclasses
|
||
|
import operator
|
||
|
from typing import Any, Iterable
|
||
|
|
||
|
from .environment import set_asdf_paths, set_pyenv_paths
|
||
|
from .exceptions import InvalidPythonVersion
|
||
|
from .models.path import PathEntry, SystemPath
|
||
|
from .models.python import PythonVersion
|
||
|
from .utils import version_re
|
||
|
|
||
|
|
||
|
@dataclasses.dataclass(unsafe_hash=True)
|
||
|
class Finder:
|
||
|
path: str | None = None
|
||
|
system: bool = False
|
||
|
global_search: bool = True
|
||
|
ignore_unsupported: bool = True
|
||
|
sort_by_path: bool = False
|
||
|
system_path: SystemPath | None = dataclasses.field(default=None, init=False)
|
||
|
|
||
|
def __post_init__(self):
|
||
|
self.system_path = self.create_system_path()
|
||
|
|
||
|
def create_system_path(self) -> SystemPath:
|
||
|
# Implementation of set_asdf_paths and set_pyenv_paths might need to be adapted.
|
||
|
set_asdf_paths()
|
||
|
set_pyenv_paths()
|
||
|
return SystemPath.create(
|
||
|
path=self.path,
|
||
|
system=self.system,
|
||
|
global_search=self.global_search,
|
||
|
ignore_unsupported=self.ignore_unsupported,
|
||
|
)
|
||
|
|
||
|
def which(self, exe) -> PathEntry | None:
|
||
|
return self.system_path.which(exe)
|
||
|
|
||
|
@classmethod
|
||
|
def parse_major(
|
||
|
cls,
|
||
|
major: str | None,
|
||
|
minor: int | None = None,
|
||
|
patch: int | None = None,
|
||
|
pre: bool | None = None,
|
||
|
dev: bool | None = None,
|
||
|
arch: str | None = None,
|
||
|
) -> dict[str, Any]:
|
||
|
major_is_str = major and isinstance(major, str)
|
||
|
is_num = (
|
||
|
major
|
||
|
and major_is_str
|
||
|
and all(part.isdigit() for part in major.split(".")[:2])
|
||
|
)
|
||
|
major_has_arch = (
|
||
|
arch is None
|
||
|
and major
|
||
|
and major_is_str
|
||
|
and "-" in major
|
||
|
and major[0].isdigit()
|
||
|
)
|
||
|
name = None
|
||
|
if major and major_has_arch:
|
||
|
orig_string = f"{major!s}"
|
||
|
major, _, arch = major.rpartition("-")
|
||
|
if arch:
|
||
|
arch = arch.lower().lstrip("x").replace("bit", "")
|
||
|
if not (arch.isdigit() and (int(arch) & int(arch) - 1) == 0):
|
||
|
major = orig_string
|
||
|
arch = None
|
||
|
else:
|
||
|
arch = f"{arch}bit"
|
||
|
try:
|
||
|
version_dict = PythonVersion.parse(major)
|
||
|
except (ValueError, InvalidPythonVersion):
|
||
|
if name is None:
|
||
|
name = f"{major!s}"
|
||
|
major = None
|
||
|
version_dict = {}
|
||
|
elif major and major[0].isalpha():
|
||
|
return {"major": None, "name": major, "arch": arch}
|
||
|
elif major and is_num:
|
||
|
match = version_re.match(major)
|
||
|
version_dict = match.groupdict() if match else {}
|
||
|
version_dict.update(
|
||
|
{
|
||
|
"is_prerelease": bool(version_dict.get("prerel", False)),
|
||
|
"is_devrelease": bool(version_dict.get("dev", False)),
|
||
|
}
|
||
|
)
|
||
|
else:
|
||
|
version_dict = {
|
||
|
"major": major,
|
||
|
"minor": minor,
|
||
|
"patch": patch,
|
||
|
"pre": pre,
|
||
|
"dev": dev,
|
||
|
"arch": arch,
|
||
|
}
|
||
|
if not version_dict.get("arch") and arch:
|
||
|
version_dict["arch"] = arch
|
||
|
version_dict["minor"] = (
|
||
|
int(version_dict["minor"]) if version_dict.get("minor") is not None else minor
|
||
|
)
|
||
|
version_dict["patch"] = (
|
||
|
int(version_dict["patch"]) if version_dict.get("patch") is not None else patch
|
||
|
)
|
||
|
version_dict["major"] = (
|
||
|
int(version_dict["major"]) if version_dict.get("major") is not None else major
|
||
|
)
|
||
|
if not (version_dict["major"] or version_dict.get("name")):
|
||
|
version_dict["major"] = major
|
||
|
if name:
|
||
|
version_dict["name"] = name
|
||
|
return version_dict
|
||
|
|
||
|
def find_python_version(
|
||
|
self,
|
||
|
major: str | int | None = None,
|
||
|
minor: int | None = None,
|
||
|
patch: int | None = None,
|
||
|
pre: bool | None = None,
|
||
|
dev: bool | None = None,
|
||
|
arch: str | None = None,
|
||
|
name: str | None = None,
|
||
|
sort_by_path: bool = False,
|
||
|
) -> PathEntry | None:
|
||
|
"""
|
||
|
Find the python version which corresponds most closely to the version requested.
|
||
|
|
||
|
:param major: The major version to look for, or the full version, or the name of the target version.
|
||
|
:param minor: The minor version. If provided, disables string-based lookups from the major version field.
|
||
|
:param patch: The patch version.
|
||
|
:param pre: If provided, specifies whether to search pre-releases.
|
||
|
:param dev: If provided, whether to search dev-releases.
|
||
|
:param arch: If provided, which architecture to search.
|
||
|
:param name: *Name* of the target python, e.g. ``anaconda3-5.3.0``
|
||
|
:param sort_by_path: Whether to sort by path -- default sort is by version(default: False)
|
||
|
:return: A new *PathEntry* pointer at a matching python version, if one can be located.
|
||
|
"""
|
||
|
minor = int(minor) if minor is not None else minor
|
||
|
patch = int(patch) if patch is not None else patch
|
||
|
|
||
|
if (
|
||
|
isinstance(major, str)
|
||
|
and pre is None
|
||
|
and minor is None
|
||
|
and dev is None
|
||
|
and patch is None
|
||
|
):
|
||
|
version_dict = self.parse_major(major, minor=minor, patch=patch, arch=arch)
|
||
|
major = version_dict["major"]
|
||
|
minor = version_dict.get("minor", minor)
|
||
|
patch = version_dict.get("patch", patch)
|
||
|
arch = version_dict.get("arch", arch)
|
||
|
name = version_dict.get("name", name)
|
||
|
_pre = version_dict.get("is_prerelease", pre)
|
||
|
pre = bool(_pre) if _pre is not None else pre
|
||
|
_dev = version_dict.get("is_devrelease", dev)
|
||
|
dev = bool(_dev) if _dev is not None else dev
|
||
|
if "architecture" in version_dict and isinstance(
|
||
|
version_dict["architecture"], str
|
||
|
):
|
||
|
arch = version_dict["architecture"]
|
||
|
return self.system_path.find_python_version(
|
||
|
major=major,
|
||
|
minor=minor,
|
||
|
patch=patch,
|
||
|
pre=pre,
|
||
|
dev=dev,
|
||
|
arch=arch,
|
||
|
name=name,
|
||
|
sort_by_path=sort_by_path,
|
||
|
)
|
||
|
|
||
|
def find_all_python_versions(
|
||
|
self,
|
||
|
major: str | int | None = None,
|
||
|
minor: int | None = None,
|
||
|
patch: int | None = None,
|
||
|
pre: bool | None = None,
|
||
|
dev: bool | None = None,
|
||
|
arch: str | None = None,
|
||
|
name: str | None = None,
|
||
|
) -> list[PathEntry]:
|
||
|
version_sort = operator.attrgetter("as_python.version_sort")
|
||
|
python_version_dict = getattr(self.system_path, "python_version_dict", {})
|
||
|
if python_version_dict:
|
||
|
paths = (
|
||
|
path
|
||
|
for version in python_version_dict.values()
|
||
|
for path in version
|
||
|
if path is not None and path.as_python
|
||
|
)
|
||
|
path_list = sorted(paths, key=version_sort, reverse=True)
|
||
|
return path_list
|
||
|
versions = self.system_path.find_all_python_versions(
|
||
|
major=major, minor=minor, patch=patch, pre=pre, dev=dev, arch=arch, name=name
|
||
|
)
|
||
|
if not isinstance(versions, Iterable):
|
||
|
versions = [versions]
|
||
|
path_list = sorted(
|
||
|
filter(lambda v: v and v.as_python, versions), key=version_sort, reverse=True
|
||
|
)
|
||
|
path_map = {}
|
||
|
for p in path_list:
|
||
|
try:
|
||
|
resolved_path = p.path.resolve(strict=True)
|
||
|
except (OSError, RuntimeError):
|
||
|
resolved_path = p.path.absolute()
|
||
|
if resolved_path not in path_map:
|
||
|
path_map[resolved_path] = p
|
||
|
return [path_map[p] for p in path_map]
|