Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/packaging/tags.py: 2%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is dual licensed under the terms of the Apache License, Version
2# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3# for complete details.
5from __future__ import annotations
7import logging
8import platform
9import re
10import struct
11import subprocess
12import sys
13import sysconfig
14from importlib.machinery import EXTENSION_SUFFIXES
15from typing import (
16 Iterable,
17 Iterator,
18 Sequence,
19 Tuple,
20 cast,
21)
23from . import _manylinux, _musllinux
25logger = logging.getLogger(__name__)
27PythonVersion = Sequence[int]
28AppleVersion = Tuple[int, int]
30INTERPRETER_SHORT_NAMES: dict[str, str] = {
31 "python": "py", # Generic.
32 "cpython": "cp",
33 "pypy": "pp",
34 "ironpython": "ip",
35 "jython": "jy",
36}
39_32_BIT_INTERPRETER = struct.calcsize("P") == 4
42class Tag:
43 """
44 A representation of the tag triple for a wheel.
46 Instances are considered immutable and thus are hashable. Equality checking
47 is also supported.
48 """
50 __slots__ = ["_abi", "_hash", "_interpreter", "_platform"]
52 def __init__(self, interpreter: str, abi: str, platform: str) -> None:
53 self._interpreter = interpreter.lower()
54 self._abi = abi.lower()
55 self._platform = platform.lower()
56 # The __hash__ of every single element in a Set[Tag] will be evaluated each time
57 # that a set calls its `.disjoint()` method, which may be called hundreds of
58 # times when scanning a page of links for packages with tags matching that
59 # Set[Tag]. Pre-computing the value here produces significant speedups for
60 # downstream consumers.
61 self._hash = hash((self._interpreter, self._abi, self._platform))
63 @property
64 def interpreter(self) -> str:
65 return self._interpreter
67 @property
68 def abi(self) -> str:
69 return self._abi
71 @property
72 def platform(self) -> str:
73 return self._platform
75 def __eq__(self, other: object) -> bool:
76 if not isinstance(other, Tag):
77 return NotImplemented
79 return (
80 (self._hash == other._hash) # Short-circuit ASAP for perf reasons.
81 and (self._platform == other._platform)
82 and (self._abi == other._abi)
83 and (self._interpreter == other._interpreter)
84 )
86 def __hash__(self) -> int:
87 return self._hash
89 def __str__(self) -> str:
90 return f"{self._interpreter}-{self._abi}-{self._platform}"
92 def __repr__(self) -> str:
93 return f"<{self} @ {id(self)}>"
96def parse_tag(tag: str) -> frozenset[Tag]:
97 """
98 Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
100 Returning a set is required due to the possibility that the tag is a
101 compressed tag set.
102 """
103 tags = set()
104 interpreters, abis, platforms = tag.split("-")
105 for interpreter in interpreters.split("."):
106 for abi in abis.split("."):
107 for platform_ in platforms.split("."):
108 tags.add(Tag(interpreter, abi, platform_))
109 return frozenset(tags)
112def _get_config_var(name: str, warn: bool = False) -> int | str | None:
113 value: int | str | None = sysconfig.get_config_var(name)
114 if value is None and warn:
115 logger.debug(
116 "Config variable '%s' is unset, Python ABI tag may be incorrect", name
117 )
118 return value
121def _normalize_string(string: str) -> str:
122 return string.replace(".", "_").replace("-", "_").replace(" ", "_")
125def _is_threaded_cpython(abis: list[str]) -> bool:
126 """
127 Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
129 The threaded builds are indicated by a "t" in the abiflags.
130 """
131 if len(abis) == 0:
132 return False
133 # expect e.g., cp313
134 m = re.match(r"cp\d+(.*)", abis[0])
135 if not m:
136 return False
137 abiflags = m.group(1)
138 return "t" in abiflags
141def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
142 """
143 Determine if the Python version supports abi3.
145 PEP 384 was first implemented in Python 3.2. The threaded (`--disable-gil`)
146 builds do not support abi3.
147 """
148 return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
151def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
152 py_version = tuple(py_version) # To allow for version comparison.
153 abis = []
154 version = _version_nodot(py_version[:2])
155 threading = debug = pymalloc = ucs4 = ""
156 with_debug = _get_config_var("Py_DEBUG", warn)
157 has_refcount = hasattr(sys, "gettotalrefcount")
158 # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled
159 # extension modules is the best option.
160 # https://github.com/pypa/pip/issues/3383#issuecomment-173267692
161 has_ext = "_d.pyd" in EXTENSION_SUFFIXES
162 if with_debug or (with_debug is None and (has_refcount or has_ext)):
163 debug = "d"
164 if py_version >= (3, 13) and _get_config_var("Py_GIL_DISABLED", warn):
165 threading = "t"
166 if py_version < (3, 8):
167 with_pymalloc = _get_config_var("WITH_PYMALLOC", warn)
168 if with_pymalloc or with_pymalloc is None:
169 pymalloc = "m"
170 if py_version < (3, 3):
171 unicode_size = _get_config_var("Py_UNICODE_SIZE", warn)
172 if unicode_size == 4 or (
173 unicode_size is None and sys.maxunicode == 0x10FFFF
174 ):
175 ucs4 = "u"
176 elif debug:
177 # Debug builds can also load "normal" extension modules.
178 # We can also assume no UCS-4 or pymalloc requirement.
179 abis.append(f"cp{version}{threading}")
180 abis.insert(0, f"cp{version}{threading}{debug}{pymalloc}{ucs4}")
181 return abis
184def cpython_tags(
185 python_version: PythonVersion | None = None,
186 abis: Iterable[str] | None = None,
187 platforms: Iterable[str] | None = None,
188 *,
189 warn: bool = False,
190) -> Iterator[Tag]:
191 """
192 Yields the tags for a CPython interpreter.
194 The tags consist of:
195 - cp<python_version>-<abi>-<platform>
196 - cp<python_version>-abi3-<platform>
197 - cp<python_version>-none-<platform>
198 - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2.
200 If python_version only specifies a major version then user-provided ABIs and
201 the 'none' ABItag will be used.
203 If 'abi3' or 'none' are specified in 'abis' then they will be yielded at
204 their normal position and not at the beginning.
205 """
206 if not python_version:
207 python_version = sys.version_info[:2]
209 interpreter = f"cp{_version_nodot(python_version[:2])}"
211 if abis is None:
212 if len(python_version) > 1:
213 abis = _cpython_abis(python_version, warn)
214 else:
215 abis = []
216 abis = list(abis)
217 # 'abi3' and 'none' are explicitly handled later.
218 for explicit_abi in ("abi3", "none"):
219 try:
220 abis.remove(explicit_abi)
221 except ValueError:
222 pass
224 platforms = list(platforms or platform_tags())
225 for abi in abis:
226 for platform_ in platforms:
227 yield Tag(interpreter, abi, platform_)
229 threading = _is_threaded_cpython(abis)
230 use_abi3 = _abi3_applies(python_version, threading)
231 if use_abi3:
232 yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
233 yield from (Tag(interpreter, "none", platform_) for platform_ in platforms)
235 if use_abi3:
236 for minor_version in range(python_version[1] - 1, 1, -1):
237 for platform_ in platforms:
238 version = _version_nodot((python_version[0], minor_version))
239 interpreter = f"cp{version}"
240 yield Tag(interpreter, "abi3", platform_)
243def _generic_abi() -> list[str]:
244 """
245 Return the ABI tag based on EXT_SUFFIX.
246 """
247 # The following are examples of `EXT_SUFFIX`.
248 # We want to keep the parts which are related to the ABI and remove the
249 # parts which are related to the platform:
250 # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310
251 # - mac: '.cpython-310-darwin.so' => cp310
252 # - win: '.cp310-win_amd64.pyd' => cp310
253 # - win: '.pyd' => cp37 (uses _cpython_abis())
254 # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73
255 # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib'
256 # => graalpy_38_native
258 ext_suffix = _get_config_var("EXT_SUFFIX", warn=True)
259 if not isinstance(ext_suffix, str) or ext_suffix[0] != ".":
260 raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')")
261 parts = ext_suffix.split(".")
262 if len(parts) < 3:
263 # CPython3.7 and earlier uses ".pyd" on Windows.
264 return _cpython_abis(sys.version_info[:2])
265 soabi = parts[1]
266 if soabi.startswith("cpython"):
267 # non-windows
268 abi = "cp" + soabi.split("-")[1]
269 elif soabi.startswith("cp"):
270 # windows
271 abi = soabi.split("-")[0]
272 elif soabi.startswith("pypy"):
273 abi = "-".join(soabi.split("-")[:2])
274 elif soabi.startswith("graalpy"):
275 abi = "-".join(soabi.split("-")[:3])
276 elif soabi:
277 # pyston, ironpython, others?
278 abi = soabi
279 else:
280 return []
281 return [_normalize_string(abi)]
284def generic_tags(
285 interpreter: str | None = None,
286 abis: Iterable[str] | None = None,
287 platforms: Iterable[str] | None = None,
288 *,
289 warn: bool = False,
290) -> Iterator[Tag]:
291 """
292 Yields the tags for a generic interpreter.
294 The tags consist of:
295 - <interpreter>-<abi>-<platform>
297 The "none" ABI will be added if it was not explicitly provided.
298 """
299 if not interpreter:
300 interp_name = interpreter_name()
301 interp_version = interpreter_version(warn=warn)
302 interpreter = "".join([interp_name, interp_version])
303 if abis is None:
304 abis = _generic_abi()
305 else:
306 abis = list(abis)
307 platforms = list(platforms or platform_tags())
308 if "none" not in abis:
309 abis.append("none")
310 for abi in abis:
311 for platform_ in platforms:
312 yield Tag(interpreter, abi, platform_)
315def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
316 """
317 Yields Python versions in descending order.
319 After the latest version, the major-only version will be yielded, and then
320 all previous versions of that major version.
321 """
322 if len(py_version) > 1:
323 yield f"py{_version_nodot(py_version[:2])}"
324 yield f"py{py_version[0]}"
325 if len(py_version) > 1:
326 for minor in range(py_version[1] - 1, -1, -1):
327 yield f"py{_version_nodot((py_version[0], minor))}"
330def compatible_tags(
331 python_version: PythonVersion | None = None,
332 interpreter: str | None = None,
333 platforms: Iterable[str] | None = None,
334) -> Iterator[Tag]:
335 """
336 Yields the sequence of tags that are compatible with a specific version of Python.
338 The tags consist of:
339 - py*-none-<platform>
340 - <interpreter>-none-any # ... if `interpreter` is provided.
341 - py*-none-any
342 """
343 if not python_version:
344 python_version = sys.version_info[:2]
345 platforms = list(platforms or platform_tags())
346 for version in _py_interpreter_range(python_version):
347 for platform_ in platforms:
348 yield Tag(version, "none", platform_)
349 if interpreter:
350 yield Tag(interpreter, "none", "any")
351 for version in _py_interpreter_range(python_version):
352 yield Tag(version, "none", "any")
355def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
356 if not is_32bit:
357 return arch
359 if arch.startswith("ppc"):
360 return "ppc"
362 return "i386"
365def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
366 formats = [cpu_arch]
367 if cpu_arch == "x86_64":
368 if version < (10, 4):
369 return []
370 formats.extend(["intel", "fat64", "fat32"])
372 elif cpu_arch == "i386":
373 if version < (10, 4):
374 return []
375 formats.extend(["intel", "fat32", "fat"])
377 elif cpu_arch == "ppc64":
378 # TODO: Need to care about 32-bit PPC for ppc64 through 10.2?
379 if version > (10, 5) or version < (10, 4):
380 return []
381 formats.append("fat64")
383 elif cpu_arch == "ppc":
384 if version > (10, 6):
385 return []
386 formats.extend(["fat32", "fat"])
388 if cpu_arch in {"arm64", "x86_64"}:
389 formats.append("universal2")
391 if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}:
392 formats.append("universal")
394 return formats
397def mac_platforms(
398 version: AppleVersion | None = None, arch: str | None = None
399) -> Iterator[str]:
400 """
401 Yields the platform tags for a macOS system.
403 The `version` parameter is a two-item tuple specifying the macOS version to
404 generate platform tags for. The `arch` parameter is the CPU architecture to
405 generate platform tags for. Both parameters default to the appropriate value
406 for the current system.
407 """
408 version_str, _, cpu_arch = platform.mac_ver()
409 if version is None:
410 version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
411 if version == (10, 16):
412 # When built against an older macOS SDK, Python will report macOS 10.16
413 # instead of the real version.
414 version_str = subprocess.run(
415 [
416 sys.executable,
417 "-sS",
418 "-c",
419 "import platform; print(platform.mac_ver()[0])",
420 ],
421 check=True,
422 env={"SYSTEM_VERSION_COMPAT": "0"},
423 stdout=subprocess.PIPE,
424 text=True,
425 ).stdout
426 version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
427 else:
428 version = version
429 if arch is None:
430 arch = _mac_arch(cpu_arch)
431 else:
432 arch = arch
434 if (10, 0) <= version and version < (11, 0):
435 # Prior to Mac OS 11, each yearly release of Mac OS bumped the
436 # "minor" version number. The major version was always 10.
437 major_version = 10
438 for minor_version in range(version[1], -1, -1):
439 compat_version = major_version, minor_version
440 binary_formats = _mac_binary_formats(compat_version, arch)
441 for binary_format in binary_formats:
442 yield f"macosx_{major_version}_{minor_version}_{binary_format}"
444 if version >= (11, 0):
445 # Starting with Mac OS 11, each yearly release bumps the major version
446 # number. The minor versions are now the midyear updates.
447 minor_version = 0
448 for major_version in range(version[0], 10, -1):
449 compat_version = major_version, minor_version
450 binary_formats = _mac_binary_formats(compat_version, arch)
451 for binary_format in binary_formats:
452 yield f"macosx_{major_version}_{minor_version}_{binary_format}"
454 if version >= (11, 0):
455 # Mac OS 11 on x86_64 is compatible with binaries from previous releases.
456 # Arm64 support was introduced in 11.0, so no Arm binaries from previous
457 # releases exist.
458 #
459 # However, the "universal2" binary format can have a
460 # macOS version earlier than 11.0 when the x86_64 part of the binary supports
461 # that version of macOS.
462 major_version = 10
463 if arch == "x86_64":
464 for minor_version in range(16, 3, -1):
465 compat_version = major_version, minor_version
466 binary_formats = _mac_binary_formats(compat_version, arch)
467 for binary_format in binary_formats:
468 yield f"macosx_{major_version}_{minor_version}_{binary_format}"
469 else:
470 for minor_version in range(16, 3, -1):
471 compat_version = major_version, minor_version
472 binary_format = "universal2"
473 yield f"macosx_{major_version}_{minor_version}_{binary_format}"
476def ios_platforms(
477 version: AppleVersion | None = None, multiarch: str | None = None
478) -> Iterator[str]:
479 """
480 Yields the platform tags for an iOS system.
482 :param version: A two-item tuple specifying the iOS version to generate
483 platform tags for. Defaults to the current iOS version.
484 :param multiarch: The CPU architecture+ABI to generate platform tags for -
485 (the value used by `sys.implementation._multiarch` e.g.,
486 `arm64_iphoneos` or `x84_64_iphonesimulator`). Defaults to the current
487 multiarch value.
488 """
489 if version is None:
490 # if iOS is the current platform, ios_ver *must* be defined. However,
491 # it won't exist for CPython versions before 3.13, which causes a mypy
492 # error.
493 _, release, _, _ = platform.ios_ver() # type: ignore[attr-defined, unused-ignore]
494 version = cast("AppleVersion", tuple(map(int, release.split(".")[:2])))
496 if multiarch is None:
497 multiarch = sys.implementation._multiarch
498 multiarch = multiarch.replace("-", "_")
500 ios_platform_template = "ios_{major}_{minor}_{multiarch}"
502 # Consider any iOS major.minor version from the version requested, down to
503 # 12.0. 12.0 is the first iOS version that is known to have enough features
504 # to support CPython. Consider every possible minor release up to X.9. There
505 # highest the minor has ever gone is 8 (14.8 and 15.8) but having some extra
506 # candidates that won't ever match doesn't really hurt, and it saves us from
507 # having to keep an explicit list of known iOS versions in the code. Return
508 # the results descending order of version number.
510 # If the requested major version is less than 12, there won't be any matches.
511 if version[0] < 12:
512 return
514 # Consider the actual X.Y version that was requested.
515 yield ios_platform_template.format(
516 major=version[0], minor=version[1], multiarch=multiarch
517 )
519 # Consider every minor version from X.0 to the minor version prior to the
520 # version requested by the platform.
521 for minor in range(version[1] - 1, -1, -1):
522 yield ios_platform_template.format(
523 major=version[0], minor=minor, multiarch=multiarch
524 )
526 for major in range(version[0] - 1, 11, -1):
527 for minor in range(9, -1, -1):
528 yield ios_platform_template.format(
529 major=major, minor=minor, multiarch=multiarch
530 )
533def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
534 linux = _normalize_string(sysconfig.get_platform())
535 if not linux.startswith("linux_"):
536 # we should never be here, just yield the sysconfig one and return
537 yield linux
538 return
539 if is_32bit:
540 if linux == "linux_x86_64":
541 linux = "linux_i686"
542 elif linux == "linux_aarch64":
543 linux = "linux_armv8l"
544 _, arch = linux.split("_", 1)
545 archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch])
546 yield from _manylinux.platform_tags(archs)
547 yield from _musllinux.platform_tags(archs)
548 for arch in archs:
549 yield f"linux_{arch}"
552def _generic_platforms() -> Iterator[str]:
553 yield _normalize_string(sysconfig.get_platform())
556def platform_tags() -> Iterator[str]:
557 """
558 Provides the platform tags for this installation.
559 """
560 if platform.system() == "Darwin":
561 return mac_platforms()
562 elif platform.system() == "iOS":
563 return ios_platforms()
564 elif platform.system() == "Linux":
565 return _linux_platforms()
566 else:
567 return _generic_platforms()
570def interpreter_name() -> str:
571 """
572 Returns the name of the running interpreter.
574 Some implementations have a reserved, two-letter abbreviation which will
575 be returned when appropriate.
576 """
577 name = sys.implementation.name
578 return INTERPRETER_SHORT_NAMES.get(name) or name
581def interpreter_version(*, warn: bool = False) -> str:
582 """
583 Returns the version of the running interpreter.
584 """
585 version = _get_config_var("py_version_nodot", warn=warn)
586 if version:
587 version = str(version)
588 else:
589 version = _version_nodot(sys.version_info[:2])
590 return version
593def _version_nodot(version: PythonVersion) -> str:
594 return "".join(map(str, version))
597def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
598 """
599 Returns the sequence of tag triples for the running interpreter.
601 The order of the sequence corresponds to priority order for the
602 interpreter, from most to least important.
603 """
605 interp_name = interpreter_name()
606 if interp_name == "cp":
607 yield from cpython_tags(warn=warn)
608 else:
609 yield from generic_tags()
611 if interp_name == "pp":
612 interp = "pp3"
613 elif interp_name == "cp":
614 interp = "cp" + interpreter_version(warn=warn)
615 else:
616 interp = None
617 yield from compatible_tags(interpreter=interp)