Spaces:
Build error
Build error
| # This file is dual licensed under the terms of the Apache License, Version | |
| # 2.0, and the BSD License. See the LICENSE file in the root of this repository | |
| # for complete details. | |
| from __future__ import annotations | |
| import logging | |
| import platform | |
| import re | |
| import struct | |
| import subprocess | |
| import sys | |
| import sysconfig | |
| from importlib.machinery import EXTENSION_SUFFIXES | |
| from typing import ( | |
| Iterable, | |
| Iterator, | |
| Sequence, | |
| Tuple, | |
| cast, | |
| ) | |
| from . import _manylinux, _musllinux | |
| logger = logging.getLogger(__name__) | |
| PythonVersion = Sequence[int] | |
| AppleVersion = Tuple[int, int] | |
| INTERPRETER_SHORT_NAMES: dict[str, str] = { | |
| "python": "py", # Generic. | |
| "cpython": "cp", | |
| "pypy": "pp", | |
| "ironpython": "ip", | |
| "jython": "jy", | |
| } | |
| _32_BIT_INTERPRETER = struct.calcsize("P") == 4 | |
| class Tag: | |
| """ | |
| A representation of the tag triple for a wheel. | |
| Instances are considered immutable and thus are hashable. Equality checking | |
| is also supported. | |
| """ | |
| __slots__ = ["_abi", "_hash", "_interpreter", "_platform"] | |
| def __init__(self, interpreter: str, abi: str, platform: str) -> None: | |
| self._interpreter = interpreter.lower() | |
| self._abi = abi.lower() | |
| self._platform = platform.lower() | |
| # The __hash__ of every single element in a Set[Tag] will be evaluated each time | |
| # that a set calls its `.disjoint()` method, which may be called hundreds of | |
| # times when scanning a page of links for packages with tags matching that | |
| # Set[Tag]. Pre-computing the value here produces significant speedups for | |
| # downstream consumers. | |
| self._hash = hash((self._interpreter, self._abi, self._platform)) | |
| def interpreter(self) -> str: | |
| return self._interpreter | |
| def abi(self) -> str: | |
| return self._abi | |
| def platform(self) -> str: | |
| return self._platform | |
| def __eq__(self, other: object) -> bool: | |
| if not isinstance(other, Tag): | |
| return NotImplemented | |
| return ( | |
| (self._hash == other._hash) # Short-circuit ASAP for perf reasons. | |
| and (self._platform == other._platform) | |
| and (self._abi == other._abi) | |
| and (self._interpreter == other._interpreter) | |
| ) | |
| def __hash__(self) -> int: | |
| return self._hash | |
| def __str__(self) -> str: | |
| return f"{self._interpreter}-{self._abi}-{self._platform}" | |
| def __repr__(self) -> str: | |
| return f"<{self} @ {id(self)}>" | |
| def parse_tag(tag: str) -> frozenset[Tag]: | |
| """ | |
| Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. | |
| Returning a set is required due to the possibility that the tag is a | |
| compressed tag set. | |
| """ | |
| tags = set() | |
| interpreters, abis, platforms = tag.split("-") | |
| for interpreter in interpreters.split("."): | |
| for abi in abis.split("."): | |
| for platform_ in platforms.split("."): | |
| tags.add(Tag(interpreter, abi, platform_)) | |
| return frozenset(tags) | |
| def _get_config_var(name: str, warn: bool = False) -> int | str | None: | |
| value: int | str | None = sysconfig.get_config_var(name) | |
| if value is None and warn: | |
| logger.debug( | |
| "Config variable '%s' is unset, Python ABI tag may be incorrect", name | |
| ) | |
| return value | |
| def _normalize_string(string: str) -> str: | |
| return string.replace(".", "_").replace("-", "_").replace(" ", "_") | |
| def _is_threaded_cpython(abis: list[str]) -> bool: | |
| """ | |
| Determine if the ABI corresponds to a threaded (`--disable-gil`) build. | |
| The threaded builds are indicated by a "t" in the abiflags. | |
| """ | |
| if len(abis) == 0: | |
| return False | |
| # expect e.g., cp313 | |
| m = re.match(r"cp\d+(.*)", abis[0]) | |
| if not m: | |
| return False | |
| abiflags = m.group(1) | |
| return "t" in abiflags | |
| def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool: | |
| """ | |
| Determine if the Python version supports abi3. | |
| PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`) | |
| builds do not support abi3. | |
| """ | |
| return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading | |
| def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]: | |
| py_version = tuple(py_version) # To allow for version comparison. | |
| abis = [] | |
| version = _version_nodot(py_version[:2]) | |
| threading = debug = pymalloc = ucs4 = "" | |
| with_debug = _get_config_var("Py_DEBUG", warn) | |
| has_refcount = hasattr(sys, "gettotalrefcount") | |
| # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled | |
| # extension modules is the best option. | |
| # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 | |
| has_ext = "_d.pyd" in EXTENSION_SUFFIXES | |
| if with_debug or (with_debug is None and (has_refcount or has_ext)): | |
| debug = "d" | |
| if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn): | |
| threading = "t" | |
| if py_version < (3, 8): | |
| with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) | |
| if with_pymalloc or with_pymalloc is None: | |
| pymalloc = "m" | |
| if py_version < (3, 3): | |
| unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) | |
| if unicode_size == 4 or ( | |
| unicode_size is None and sys.maxunicode == 0x10FFFF | |
| ): | |
| ucs4 = "u" | |
| elif debug: | |
| # Debug builds can also load "normal" extension modules. | |
| # We can also assume no UCS-4 or pymalloc requirement. | |
| abis.append(f"cp{version}{threading}") | |
| abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}") | |
| return abis | |
| def cpython_tags( | |
| python_version: PythonVersion | None = None, | |
| abis: Iterable[str] | None = None, | |
| platforms: Iterable[str] | None = None, | |
| *, | |
| warn: bool = False, | |
| ) -> Iterator[Tag]: | |
| """ | |
| Yields the tags for a CPython interpreter. | |
| The tags consist of: | |
| - cp<python_version>-<abi>-<platform> | |
| - cp<python_version>-abi3-<platform> | |
| - cp<python_version>-none-<platform> | |
| - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2. | |
| If python_version only specifies a major version then user-provided ABIs and | |
| the 'none' ABItag will be used. | |
| If 'abi3' or 'none' are specified in 'abis' then they will be yielded at | |
| their normal position and not at the beginning. | |
| """ | |
| if not python_version: | |
| python_version = sys.version_info[:2] | |
| interpreter = f"cp{_version_nodot(python_version[:2])}" | |
| if abis is None: | |
| if len(python_version) > 1: | |
| abis = _cpython_abis(python_version, warn) | |
| else: | |
| abis = [] | |
| abis = list(abis) | |
| # 'abi3' and 'none' are explicitly handled later. | |
| for explicit_abi in ("abi3", "none"): | |
| try: | |
| abis.remove(explicit_abi) | |
| except ValueError: | |
| pass | |
| platforms = list(platforms or platform_tags()) | |
| for abi in abis: | |
| for platform_ in platforms: | |
| yield Tag(interpreter, abi, platform_) | |
| threading = _is_threaded_cpython(abis) | |
| use_abi3 = _abi3_applies(python_version, threading) | |
| if use_abi3: | |
| yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) | |
| yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) | |
| if use_abi3: | |
| for minor_version in range(python_version[1] - 1, 1, -1): | |
| for platform_ in platforms: | |
| version = _version_nodot((python_version[0], minor_version)) | |
| interpreter = f"cp{version}" | |
| yield Tag(interpreter, "abi3", platform_) | |
| def _generic_abi() -> list[str]: | |
| """ | |
| Return the ABI tag based on EXT_SUFFIX. | |
| """ | |
| # The following are examples of `EXT_SUFFIX`. | |
| # We want to keep the parts which are related to the ABI and remove the | |
| # parts which are related to the platform: | |
| # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 | |
| # - mac: '.cpython-310-darwin.so' => cp310 | |
| # - win: '.cp310-win_amd64.pyd' => cp310 | |
| # - win: '.pyd' => cp37 (uses _cpython_abis()) | |
| # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 | |
| # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' | |
| # => graalpy_38_native | |
| ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) | |
| if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": | |
| raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") | |
| parts = ext_suffix.split(".") | |
| if len(parts) < 3: | |
| # CPython3.7 and earlier uses ".pyd" on Windows. | |
| return _cpython_abis(sys.version_info[:2]) | |
| soabi = parts[1] | |
| if soabi.startswith("cpython"): | |
| # non-windows | |
| abi = "cp" + soabi.split("-")[1] | |
| elif soabi.startswith("cp"): | |
| # windows | |
| abi = soabi.split("-")[0] | |
| elif soabi.startswith("pypy"): | |
| abi = "-".join(soabi.split("-")[:2]) | |
| elif soabi.startswith("graalpy"): | |
| abi = "-".join(soabi.split("-")[:3]) | |
| elif soabi: | |
| # pyston, ironpython, others? | |
| abi = soabi | |
| else: | |
| return [] | |
| return [_normalize_string(abi)] | |
| def generic_tags( | |
| interpreter: str | None = None, | |
| abis: Iterable[str] | None = None, | |
| platforms: Iterable[str] | None = None, | |
| *, | |
| warn: bool = False, | |
| ) -> Iterator[Tag]: | |
| """ | |
| Yields the tags for a generic interpreter. | |
| The tags consist of: | |
| - <interpreter>-<abi>-<platform> | |
| The "none" ABI will be added if it was not explicitly provided. | |
| """ | |
| if not interpreter: | |
| interp_name = interpreter_name() | |
| interp_version = interpreter_version(warn=warn) | |
| interpreter = "".join([interp_name, interp_version]) | |
| if abis is None: | |
| abis = _generic_abi() | |
| else: | |
| abis = list(abis) | |
| platforms = list(platforms or platform_tags()) | |
| if "none" not in abis: | |
| abis.append("none") | |
| for abi in abis: | |
| for platform_ in platforms: | |
| yield Tag(interpreter, abi, platform_) | |
| def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: | |
| """ | |
| Yields Python versions in descending order. | |
| After the latest version, the major-only version will be yielded, and then | |
| all previous versions of that major version. | |
| """ | |
| if len(py_version) > 1: | |
| yield f"py{_version_nodot(py_version[:2])}" | |
| yield f"py{py_version[0]}" | |
| if len(py_version) > 1: | |
| for minor in range(py_version[1] - 1, -1, -1): | |
| yield f"py{_version_nodot((py_version[0], minor))}" | |
| def compatible_tags( | |
| python_version: PythonVersion | None = None, | |
| interpreter: str | None = None, | |
| platforms: Iterable[str] | None = None, | |
| ) -> Iterator[Tag]: | |
| """ | |
| Yields the sequence of tags that are compatible with a specific version of Python. | |
| The tags consist of: | |
| - py*-none-<platform> | |
| - <interpreter>-none-any # ... if `interpreter` is provided. | |
| - py*-none-any | |
| """ | |
| if not python_version: | |
| python_version = sys.version_info[:2] | |
| platforms = list(platforms or platform_tags()) | |
| for version in _py_interpreter_range(python_version): | |
| for platform_ in platforms: | |
| yield Tag(version, "none", platform_) | |
| if interpreter: | |
| yield Tag(interpreter, "none", "any") | |
| for version in _py_interpreter_range(python_version): | |
| yield Tag(version, "none", "any") | |
| def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: | |
| if not is_32bit: | |
| return arch | |
| if arch.startswith("ppc"): | |
| return "ppc" | |
| return "i386" | |
| def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]: | |
| formats = [cpu_arch] | |
| if cpu_arch == "x86_64": | |
| if version < (10, 4): | |
| return [] | |
| formats.extend(["intel", "fat64", "fat32"]) | |
| elif cpu_arch == "i386": | |
| if version < (10, 4): | |
| return [] | |
| formats.extend(["intel", "fat32", "fat"]) | |
| elif cpu_arch == "ppc64": | |
| # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? | |
| if version > (10, 5) or version < (10, 4): | |
| return [] | |
| formats.append("fat64") | |
| elif cpu_arch == "ppc": | |
| if version > (10, 6): | |
| return [] | |
| formats.extend(["fat32", "fat"]) | |
| if cpu_arch in {"arm64", "x86_64"}: | |
| formats.append("universal2") | |
| if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: | |
| formats.append("universal") | |
| return formats | |
| def mac_platforms( | |
| version: AppleVersion | None = None, arch: str | None = None | |
| ) -> Iterator[str]: | |
| """ | |
| Yields the platform tags for a macOS system. | |
| The `version` parameter is a two-item tuple specifying the macOS version to | |
| generate platform tags for. The `arch` parameter is the CPU architecture to | |
| generate platform tags for. Both parameters default to the appropriate value | |
| for the current system. | |
| """ | |
| version_str, _, cpu_arch = platform.mac_ver() | |
| if version is None: | |
| version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2]))) | |
| if version == (10, 16): | |
| # When built against an older macOS SDK, Python will report macOS 10.16 | |
| # instead of the real version. | |
| version_str = subprocess.run( | |
| [ | |
| sys.executable, | |
| "-sS", | |
| "-c", | |
| "import platform; print(platform.mac_ver()[0])", | |
| ], | |
| check=True, | |
| env={"SYSTEM_VERSION_COMPAT": "0"}, | |
| stdout=subprocess.PIPE, | |
| text=True, | |
| ).stdout | |
| version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2]))) | |
| else: | |
| version = version | |
| if arch is None: | |
| arch = _mac_arch(cpu_arch) | |
| else: | |
| arch = arch | |
| if (10, 0) <= version and version < (11, 0): | |
| # Prior to Mac OS 11, each yearly release of Mac OS bumped the | |
| # "minor" version number. The major version was always 10. | |
| major_version = 10 | |
| for minor_version in range(version[1], -1, -1): | |
| compat_version = major_version, minor_version | |
| binary_formats = _mac_binary_formats(compat_version, arch) | |
| for binary_format in binary_formats: | |
| yield f"macosx_{major_version}_{minor_version}_{binary_format}" | |
| if version >= (11, 0): | |
| # Starting with Mac OS 11, each yearly release bumps the major version | |
| # number. The minor versions are now the midyear updates. | |
| minor_version = 0 | |
| for major_version in range(version[0], 10, -1): | |
| compat_version = major_version, minor_version | |
| binary_formats = _mac_binary_formats(compat_version, arch) | |
| for binary_format in binary_formats: | |
| yield f"macosx_{major_version}_{minor_version}_{binary_format}" | |
| if version >= (11, 0): | |
| # Mac OS 11 on x86_64 is compatible with binaries from previous releases. | |
| # Arm64 support was introduced in 11.0, so no Arm binaries from previous | |
| # releases exist. | |
| # | |
| # However, the "universal2" binary format can have a | |
| # macOS version earlier than 11.0 when the x86_64 part of the binary supports | |
| # that version of macOS. | |
| major_version = 10 | |
| if arch == "x86_64": | |
| for minor_version in range(16, 3, -1): | |
| compat_version = major_version, minor_version | |
| binary_formats = _mac_binary_formats(compat_version, arch) | |
| for binary_format in binary_formats: | |
| yield f"macosx_{major_version}_{minor_version}_{binary_format}" | |
| else: | |
| for minor_version in range(16, 3, -1): | |
| compat_version = major_version, minor_version | |
| binary_format = "universal2" | |
| yield f"macosx_{major_version}_{minor_version}_{binary_format}" | |
| def ios_platforms( | |
| version: AppleVersion | None = None, multiarch: str | None = None | |
| ) -> Iterator[str]: | |
| """ | |
| Yields the platform tags for an iOS system. | |
| :param version: A two-item tuple specifying the iOS version to generate | |
| platform tags for. Defaults to the current iOS version. | |
| :param multiarch: The CPU architecture+ABI to generate platform tags for - | |
| (the value used by `sys.implementation._multiarch` e.g., | |
| `arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current | |
| multiarch value. | |
| """ | |
| if version is None: | |
| # if iOS is the current platform, ios_ver *must* be defined. However, | |
| # it won't exist for CPython versions before 3.13, which causes a mypy | |
| # error. | |
| _, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore] | |
| version = cast("AppleVersion", tuple(map(int, release.split(".")[:2]))) | |
| if multiarch is None: | |
| multiarch = sys.implementation._multiarch | |
| multiarch = multiarch.replace("-", "_") | |
| ios_platform_template = "ios_{major}_{minor}_{multiarch}" | |
| # Consider any iOS major.minor version from the version requested, down to | |
| # 12.0. 12.0 is the first iOS version that is known to have enough features | |
| # to support CPython. Consider every possible minor release up to X.9. There | |
| # highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra | |
| # candidates that won't ever match doesn't really hurt, and it saves us from | |
| # having to keep an explicit list of known iOS versions in the code. Return | |
| # the results descending order of version number. | |
| # If the requested major version is less than 12, there won't be any matches. | |
| if version[0] < 12: | |
| return | |
| # Consider the actual X.Y version that was requested. | |
| yield ios_platform_template.format( | |
| major=version[0], minor=version[1], multiarch=multiarch | |
| ) | |
| # Consider every minor version from X.0 to the minor version prior to the | |
| # version requested by the platform. | |
| for minor in range(version[1] - 1, -1, -1): | |
| yield ios_platform_template.format( | |
| major=version[0], minor=minor, multiarch=multiarch | |
| ) | |
| for major in range(version[0] - 1, 11, -1): | |
| for minor in range(9, -1, -1): | |
| yield ios_platform_template.format( | |
| major=major, minor=minor, multiarch=multiarch | |
| ) | |
| def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: | |
| linux = _normalize_string(sysconfig.get_platform()) | |
| if not linux.startswith("linux_"): | |
| # we should never be here, just yield the sysconfig one and return | |
| yield linux | |
| return | |
| if is_32bit: | |
| if linux == "linux_x86_64": | |
| linux = "linux_i686" | |
| elif linux == "linux_aarch64": | |
| linux = "linux_armv8l" | |
| _, arch = linux.split("_", 1) | |
| archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) | |
| yield from _manylinux.platform_tags(archs) | |
| yield from _musllinux.platform_tags(archs) | |
| for arch in archs: | |
| yield f"linux_{arch}" | |
| def _generic_platforms() -> Iterator[str]: | |
| yield _normalize_string(sysconfig.get_platform()) | |
| def platform_tags() -> Iterator[str]: | |
| """ | |
| Provides the platform tags for this installation. | |
| """ | |
| if platform.system() == "Darwin": | |
| return mac_platforms() | |
| elif platform.system() == "iOS": | |
| return ios_platforms() | |
| elif platform.system() == "Linux": | |
| return _linux_platforms() | |
| else: | |
| return _generic_platforms() | |
| def interpreter_name() -> str: | |
| """ | |
| Returns the name of the running interpreter. | |
| Some implementations have a reserved, two-letter abbreviation which will | |
| be returned when appropriate. | |
| """ | |
| name = sys.implementation.name | |
| return INTERPRETER_SHORT_NAMES.get(name) or name | |
| def interpreter_version(*, warn: bool = False) -> str: | |
| """ | |
| Returns the version of the running interpreter. | |
| """ | |
| version = _get_config_var("py_version_nodot", warn=warn) | |
| if version: | |
| version = str(version) | |
| else: | |
| version = _version_nodot(sys.version_info[:2]) | |
| return version | |
| def _version_nodot(version: PythonVersion) -> str: | |
| return "".join(map(str, version)) | |
| def sys_tags(*, warn: bool = False) -> Iterator[Tag]: | |
| """ | |
| Returns the sequence of tag triples for the running interpreter. | |
| The order of the sequence corresponds to priority order for the | |
| interpreter, from most to least important. | |
| """ | |
| interp_name = interpreter_name() | |
| if interp_name == "cp": | |
| yield from cpython_tags(warn=warn) | |
| else: | |
| yield from generic_tags() | |
| if interp_name == "pp": | |
| interp = "pp3" | |
| elif interp_name == "cp": | |
| interp = "cp" + interpreter_version(warn=warn) | |
| else: | |
| interp = None | |
| yield from compatible_tags(interpreter=interp) | |