Spaces:
Sleeping
Sleeping
# 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. | |
import logging | |
import platform | |
import sys | |
import sysconfig | |
from importlib.machinery import EXTENSION_SUFFIXES | |
from typing import ( | |
Dict, | |
FrozenSet, | |
Iterable, | |
Iterator, | |
List, | |
Optional, | |
Sequence, | |
Tuple, | |
Union, | |
cast, | |
) | |
from . import _manylinux, _musllinux | |
logger = logging.getLogger(__name__) | |
PythonVersion = Sequence[int] | |
MacVersion = Tuple[int, int] | |
INTERPRETER_SHORT_NAMES: Dict[str, str] = { | |
"python": "py", # Generic. | |
"cpython": "cp", | |
"pypy": "pp", | |
"ironpython": "ip", | |
"jython": "jy", | |
} | |
_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 | |
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__ = ["_interpreter", "_abi", "_platform", "_hash"] | |
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) -> Union[int, str, None]: | |
value = 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("-", "_") | |
def _abi3_applies(python_version: PythonVersion) -> bool: | |
""" | |
Determine if the Python version supports abi3. | |
PEP 384 was first implemented in Python 3.2. | |
""" | |
return len(python_version) > 1 and tuple(python_version) >= (3, 2) | |
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]) | |
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, 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}") | |
abis.insert( | |
0, | |
"cp{version}{debug}{pymalloc}{ucs4}".format( | |
version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 | |
), | |
) | |
return abis | |
def cpython_tags( | |
python_version: Optional[PythonVersion] = None, | |
abis: Optional[Iterable[str]] = None, | |
platforms: Optional[Iterable[str]] = 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_) | |
if _abi3_applies(python_version): | |
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) | |
yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) | |
if _abi3_applies(python_version): | |
for minor_version in range(python_version[1] - 1, 1, -1): | |
for platform_ in platforms: | |
interpreter = "cp{version}".format( | |
version=_version_nodot((python_version[0], minor_version)) | |
) | |
yield Tag(interpreter, "abi3", platform_) | |
def _generic_abi() -> Iterator[str]: | |
abi = sysconfig.get_config_var("SOABI") | |
if abi: | |
yield _normalize_string(abi) | |
def generic_tags( | |
interpreter: Optional[str] = None, | |
abis: Optional[Iterable[str]] = None, | |
platforms: Optional[Iterable[str]] = 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() | |
platforms = list(platforms or platform_tags()) | |
abis = list(abis) | |
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: Optional[PythonVersion] = None, | |
interpreter: Optional[str] = None, | |
platforms: Optional[Iterable[str]] = 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: MacVersion, 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: Optional[MacVersion] = None, arch: Optional[str] = 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("MacVersion", 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. | |
for minor_version in range(version[1], -1, -1): | |
compat_version = 10, minor_version | |
binary_formats = _mac_binary_formats(compat_version, arch) | |
for binary_format in binary_formats: | |
yield "macosx_{major}_{minor}_{binary_format}".format( | |
major=10, minor=minor_version, binary_format=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. | |
for major_version in range(version[0], 10, -1): | |
compat_version = major_version, 0 | |
binary_formats = _mac_binary_formats(compat_version, arch) | |
for binary_format in binary_formats: | |
yield "macosx_{major}_{minor}_{binary_format}".format( | |
major=major_version, minor=0, binary_format=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. | |
if arch == "x86_64": | |
for minor_version in range(16, 3, -1): | |
compat_version = 10, minor_version | |
binary_formats = _mac_binary_formats(compat_version, arch) | |
for binary_format in binary_formats: | |
yield "macosx_{major}_{minor}_{binary_format}".format( | |
major=compat_version[0], | |
minor=compat_version[1], | |
binary_format=binary_format, | |
) | |
else: | |
for minor_version in range(16, 3, -1): | |
compat_version = 10, minor_version | |
binary_format = "universal2" | |
yield "macosx_{major}_{minor}_{binary_format}".format( | |
major=compat_version[0], | |
minor=compat_version[1], | |
binary_format=binary_format, | |
) | |
def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: | |
linux = _normalize_string(sysconfig.get_platform()) | |
if is_32bit: | |
if linux == "linux_x86_64": | |
linux = "linux_i686" | |
elif linux == "linux_aarch64": | |
linux = "linux_armv7l" | |
_, arch = linux.split("_", 1) | |
yield from _manylinux.platform_tags(linux, arch) | |
yield from _musllinux.platform_tags(arch) | |
yield linux | |
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() == "Linux": | |
return _linux_platforms() | |
else: | |
return _generic_platforms() | |
def interpreter_name() -> str: | |
""" | |
Returns the name of the running interpreter. | |
""" | |
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": | |
yield from compatible_tags(interpreter="pp3") | |
else: | |
yield from compatible_tags() | |