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]